import { app } from 'electron'; import * as fs from 'fs'; import * as path from 'path'; export type AutoUpdateMode = 'auto' | 'off' | 'version'; export interface LocalApiSettings { enabled: boolean; port: number; exposeOnLan: boolean; scalarEnabled: boolean; allowedSignalingServers: string[]; } export interface DesktopSettings { autoUpdateMode: AutoUpdateMode; autoStart: boolean; closeToTray: boolean; hardwareAcceleration: boolean; localApi: LocalApiSettings; manifestUrls: string[]; preferredVersion: string | null; vaapiVideoEncode: boolean; } export interface DesktopSettingsSnapshot extends DesktopSettings { runtimeHardwareAcceleration: boolean; restartRequired: boolean; } const DEFAULT_LOCAL_API_SETTINGS: LocalApiSettings = { enabled: false, port: 17878, exposeOnLan: false, scalarEnabled: false, allowedSignalingServers: [] }; const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = { autoUpdateMode: 'auto', autoStart: true, closeToTray: true, hardwareAcceleration: true, localApi: { ...DEFAULT_LOCAL_API_SETTINGS }, manifestUrls: [], preferredVersion: null, vaapiVideoEncode: false }; function normalizeAutoUpdateMode(value: unknown): AutoUpdateMode { return value === 'off' || value === 'version' ? value : 'auto'; } function normalizePreferredVersion(value: unknown): string | null { return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null; } function normalizeManifestUrls(value: unknown): string[] { if (!Array.isArray(value)) { return []; } const manifestUrls: string[] = []; for (const entry of value) { if (typeof entry !== 'string') { continue; } const nextUrl = entry.trim(); if (!nextUrl || manifestUrls.includes(nextUrl)) { continue; } manifestUrls.push(nextUrl); } return manifestUrls; } function normalizePort(value: unknown, fallback: number): number { if (typeof value !== 'number' || !Number.isFinite(value)) { return fallback; } const port = Math.floor(value); if (port < 1 || port > 65535) { return fallback; } return port; } function normalizeAllowedSignalingServers(value: unknown): string[] { if (!Array.isArray(value)) { return []; } const urls: string[] = []; for (const entry of value) { if (typeof entry !== 'string') { continue; } const trimmed = entry.trim().replace(/\/+$/u, ''); if (!trimmed || urls.includes(trimmed)) { continue; } if (!/^https?:\/\//iu.test(trimmed)) { continue; } urls.push(trimmed); } return urls; } function normalizeLocalApiSettings(value: unknown): LocalApiSettings { const source = (value && typeof value === 'object') ? value as Partial : {}; return { enabled: typeof source.enabled === 'boolean' ? source.enabled : DEFAULT_LOCAL_API_SETTINGS.enabled, port: normalizePort(source.port, DEFAULT_LOCAL_API_SETTINGS.port), exposeOnLan: typeof source.exposeOnLan === 'boolean' ? source.exposeOnLan : DEFAULT_LOCAL_API_SETTINGS.exposeOnLan, scalarEnabled: typeof source.scalarEnabled === 'boolean' ? source.scalarEnabled : DEFAULT_LOCAL_API_SETTINGS.scalarEnabled, allowedSignalingServers: normalizeAllowedSignalingServers(source.allowedSignalingServers) }; } export function getDesktopSettingsSnapshot(): DesktopSettingsSnapshot { const storedSettings = readDesktopSettings(); const runtimeHardwareAcceleration = app.isHardwareAccelerationEnabled(); return { ...storedSettings, runtimeHardwareAcceleration, restartRequired: storedSettings.hardwareAcceleration !== runtimeHardwareAcceleration }; } export function readDesktopSettings(): DesktopSettings { const filePath = getDesktopSettingsPath(); try { if (!fs.existsSync(filePath)) { return { ...DEFAULT_DESKTOP_SETTINGS }; } const raw = fs.readFileSync(filePath, 'utf8'); const parsed = JSON.parse(raw) as Partial; return { autoUpdateMode: normalizeAutoUpdateMode(parsed.autoUpdateMode), autoStart: typeof parsed.autoStart === 'boolean' ? parsed.autoStart : DEFAULT_DESKTOP_SETTINGS.autoStart, closeToTray: typeof parsed.closeToTray === 'boolean' ? parsed.closeToTray : DEFAULT_DESKTOP_SETTINGS.closeToTray, vaapiVideoEncode: typeof parsed.vaapiVideoEncode === 'boolean' ? parsed.vaapiVideoEncode : DEFAULT_DESKTOP_SETTINGS.vaapiVideoEncode, hardwareAcceleration: typeof parsed.hardwareAcceleration === 'boolean' ? parsed.hardwareAcceleration : DEFAULT_DESKTOP_SETTINGS.hardwareAcceleration, localApi: normalizeLocalApiSettings(parsed.localApi), manifestUrls: normalizeManifestUrls(parsed.manifestUrls), preferredVersion: normalizePreferredVersion(parsed.preferredVersion) }; } catch { return { ...DEFAULT_DESKTOP_SETTINGS }; } } export function updateDesktopSettings(patch: Partial): DesktopSettingsSnapshot { const previousSettings = readDesktopSettings(); const mergedSettings = { ...previousSettings, ...patch, localApi: patch.localApi ? { ...previousSettings.localApi, ...patch.localApi } : previousSettings.localApi }; const nextSettings: DesktopSettings = { autoUpdateMode: normalizeAutoUpdateMode(mergedSettings.autoUpdateMode), autoStart: typeof mergedSettings.autoStart === 'boolean' ? mergedSettings.autoStart : DEFAULT_DESKTOP_SETTINGS.autoStart, closeToTray: typeof mergedSettings.closeToTray === 'boolean' ? mergedSettings.closeToTray : DEFAULT_DESKTOP_SETTINGS.closeToTray, hardwareAcceleration: typeof mergedSettings.hardwareAcceleration === 'boolean' ? mergedSettings.hardwareAcceleration : DEFAULT_DESKTOP_SETTINGS.hardwareAcceleration, localApi: normalizeLocalApiSettings(mergedSettings.localApi), manifestUrls: normalizeManifestUrls(mergedSettings.manifestUrls), preferredVersion: normalizePreferredVersion(mergedSettings.preferredVersion), vaapiVideoEncode: typeof mergedSettings.vaapiVideoEncode === 'boolean' ? mergedSettings.vaapiVideoEncode : DEFAULT_DESKTOP_SETTINGS.vaapiVideoEncode }; const filePath = getDesktopSettingsPath(); fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, JSON.stringify(nextSettings, null, 2), 'utf8'); return getDesktopSettingsSnapshot(); } function getDesktopSettingsPath(): string { return path.join(app.getPath('userData'), 'desktop-settings.json'); }