From c862c2fe033a657b5744e88c5490546a47bba407 Mon Sep 17 00:00:00 2001 From: Myx Date: Wed, 18 Mar 2026 23:46:16 +0100 Subject: [PATCH] Auto start with system --- electron/app/auto-start.ts | 58 ++++++++++++ electron/app/lifecycle.ts | 2 + electron/desktop-settings.ts | 8 ++ electron/ipc/system.ts | 2 + electron/preload.ts | 3 + package-lock.json | 50 +++++++++- package.json | 2 + .../core/services/settings-modal.service.ts | 6 +- .../general-settings.component.html | 47 ++++++++++ .../general-settings.component.ts | 92 +++++++++++++++++++ .../settings-modal.component.html | 6 ++ .../settings-modal.component.ts | 5 + 12 files changed, 274 insertions(+), 7 deletions(-) create mode 100644 electron/app/auto-start.ts create mode 100644 src/app/features/settings/settings-modal/general-settings/general-settings.component.html create mode 100644 src/app/features/settings/settings-modal/general-settings/general-settings.component.ts diff --git a/electron/app/auto-start.ts b/electron/app/auto-start.ts new file mode 100644 index 0000000..e29bcdb --- /dev/null +++ b/electron/app/auto-start.ts @@ -0,0 +1,58 @@ +import { app } from 'electron'; +import AutoLaunch from 'auto-launch'; +import { readDesktopSettings } from '../desktop-settings'; + +let autoLauncher: AutoLaunch | null = null; + +function resolveLaunchPath(): string { + // AppImage runs from a temporary mount; APPIMAGE points to the real file path. + const appImagePath = process.platform === 'linux' + ? String(process.env['APPIMAGE'] || '').trim() + : ''; + + return appImagePath || process.execPath; +} + +function getAutoLauncher(): AutoLaunch | null { + if (!app.isPackaged) { + return null; + } + + if (!autoLauncher) { + autoLauncher = new AutoLaunch({ + name: app.getName(), + path: resolveLaunchPath() + }); + } + + return autoLauncher; +} + +async function setAutoStartEnabled(enabled: boolean): Promise { + const launcher = getAutoLauncher(); + + if (!launcher) { + return; + } + + const currentlyEnabled = await launcher.isEnabled(); + + if (currentlyEnabled === enabled) { + return; + } + + if (enabled) { + await launcher.enable(); + return; + } + + await launcher.disable(); +} + +export async function synchronizeAutoStartSetting(enabled = readDesktopSettings().autoStart): Promise { + try { + await setAutoStartEnabled(enabled); + } catch { + // Auto-launch integration should never block app startup or settings saves. + } +} diff --git a/electron/app/lifecycle.ts b/electron/app/lifecycle.ts index ff7af02..4c0c9b4 100644 --- a/electron/app/lifecycle.ts +++ b/electron/app/lifecycle.ts @@ -1,6 +1,7 @@ import { app, BrowserWindow } from 'electron'; import { cleanupLinuxScreenShareAudioRouting } from '../audio/linux-screen-share-routing'; import { initializeDesktopUpdater, shutdownDesktopUpdater } from '../update/desktop-updater'; +import { synchronizeAutoStartSetting } from './auto-start'; import { initializeDatabase, destroyDatabase, @@ -24,6 +25,7 @@ export function registerAppLifecycle(): void { setupCqrsHandlers(); setupWindowControlHandlers(); setupSystemHandlers(); + await synchronizeAutoStartSetting(); initializeDesktopUpdater(); await createWindow(); diff --git a/electron/desktop-settings.ts b/electron/desktop-settings.ts index 0831e76..a27c2d4 100644 --- a/electron/desktop-settings.ts +++ b/electron/desktop-settings.ts @@ -6,6 +6,7 @@ export type AutoUpdateMode = 'auto' | 'off' | 'version'; export interface DesktopSettings { autoUpdateMode: AutoUpdateMode; + autoStart: boolean; hardwareAcceleration: boolean; manifestUrls: string[]; preferredVersion: string | null; @@ -19,6 +20,7 @@ export interface DesktopSettingsSnapshot extends DesktopSettings { const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = { autoUpdateMode: 'auto', + autoStart: true, hardwareAcceleration: true, manifestUrls: [], preferredVersion: null, @@ -81,6 +83,9 @@ export function readDesktopSettings(): DesktopSettings { return { autoUpdateMode: normalizeAutoUpdateMode(parsed.autoUpdateMode), + autoStart: typeof parsed.autoStart === 'boolean' + ? parsed.autoStart + : DEFAULT_DESKTOP_SETTINGS.autoStart, vaapiVideoEncode: typeof parsed.vaapiVideoEncode === 'boolean' ? parsed.vaapiVideoEncode : DEFAULT_DESKTOP_SETTINGS.vaapiVideoEncode, @@ -102,6 +107,9 @@ export function updateDesktopSettings(patch: Partial): DesktopS }; const nextSettings: DesktopSettings = { autoUpdateMode: normalizeAutoUpdateMode(mergedSettings.autoUpdateMode), + autoStart: typeof mergedSettings.autoStart === 'boolean' + ? mergedSettings.autoStart + : DEFAULT_DESKTOP_SETTINGS.autoStart, hardwareAcceleration: typeof mergedSettings.hardwareAcceleration === 'boolean' ? mergedSettings.hardwareAcceleration : DEFAULT_DESKTOP_SETTINGS.hardwareAcceleration, diff --git a/electron/ipc/system.ts b/electron/ipc/system.ts index 2e90e2a..9350c7f 100644 --- a/electron/ipc/system.ts +++ b/electron/ipc/system.ts @@ -31,6 +31,7 @@ import { type DesktopUpdateServerContext } from '../update/desktop-updater'; import { consumePendingDeepLink } from '../app/deep-links'; +import { synchronizeAutoStartSetting } from '../app/auto-start'; const DEFAULT_MIME_TYPE = 'application/octet-stream'; const FILE_CLIPBOARD_FORMATS = [ @@ -329,6 +330,7 @@ export function setupSystemHandlers(): void { ipcMain.handle('set-desktop-settings', async (_event, patch: Partial) => { const snapshot = updateDesktopSettings(patch); + await synchronizeAutoStartSetting(snapshot.autoStart); await handleDesktopSettingsChanged(); return snapshot; }); diff --git a/electron/preload.ts b/electron/preload.ts index 3488cb8..6204189 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -119,6 +119,7 @@ export interface ElectronAPI { consumePendingDeepLink: () => Promise; getDesktopSettings: () => Promise<{ autoUpdateMode: 'auto' | 'off' | 'version'; + autoStart: boolean; hardwareAcceleration: boolean; manifestUrls: string[]; preferredVersion: string | null; @@ -132,12 +133,14 @@ export interface ElectronAPI { onAutoUpdateStateChanged: (listener: (state: DesktopUpdateState) => void) => () => void; setDesktopSettings: (patch: { autoUpdateMode?: 'auto' | 'off' | 'version'; + autoStart?: boolean; hardwareAcceleration?: boolean; manifestUrls?: string[]; preferredVersion?: string | null; vaapiVideoEncode?: boolean; }) => Promise<{ autoUpdateMode: 'auto' | 'off' | 'version'; + autoStart: boolean; hardwareAcceleration: boolean; manifestUrls: string[]; preferredVersion: string | null; diff --git a/package-lock.json b/package-lock.json index 73b82ce..ac94883 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@spartan-ng/cli": "^0.0.1-alpha.589", "@spartan-ng/ui-core": "^0.0.1-alpha.380", "@timephy/rnnoise-wasm": "^1.0.0", + "auto-launch": "^5.0.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cytoscape": "^3.33.1", @@ -45,11 +46,12 @@ }, "devDependencies": { "@angular/build": "^21.0.4", - "@angular/cli": "^21.2.1", + "@angular/cli": "^21.0.4", "@angular/compiler-cli": "^21.0.0", "@eslint/js": "^9.39.3", "@stylistic/eslint-plugin-js": "^4.4.1", "@stylistic/eslint-plugin-ts": "^4.4.1", + "@types/auto-launch": "^5.0.5", "@types/simple-peer": "^9.11.9", "@types/uuid": "^10.0.0", "angular-eslint": "21.2.0", @@ -10816,6 +10818,13 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/auto-launch": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/auto-launch/-/auto-launch-5.0.5.tgz", + "integrity": "sha512-/nGvQZSzM/pvCMCh4Gt2kIeiUmOP/cKGJbjlInI+A+5MoV/7XmT56DJ6EU8bqc3+ItxEe4UC2GVspmPzcCc8cg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -12875,6 +12884,11 @@ "node": ">= 6.0.0" } }, + "node_modules/applescript": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/applescript/-/applescript-1.0.0.tgz", + "integrity": "sha512-yvtNHdWvtbYEiIazXAdp/NY+BBb65/DAseqlNiJQjOx9DynuzOYDbVLBJvuc0ve0VL9x6B3OHF6eH52y9hCBtQ==" + }, "node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", @@ -12968,6 +12982,22 @@ "node": ">= 4.0.0" } }, + "node_modules/auto-launch": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/auto-launch/-/auto-launch-5.0.6.tgz", + "integrity": "sha512-OgxiAm4q9EBf9EeXdPBiVNENaWE3jUZofwrhAkWjHDYGezu1k3FRZHU8V2FBxGuSJOHzKmTJEd0G7L7/0xDGFA==", + "license": "MIT", + "dependencies": { + "applescript": "^1.0.0", + "mkdirp": "^0.5.1", + "path-is-absolute": "^1.0.0", + "untildify": "^3.0.2", + "winreg": "1.2.4" + }, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/autoprefixer": { "version": "10.4.23", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.23.tgz", @@ -22285,9 +22315,7 @@ "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "minimist": "^1.2.6" }, @@ -23745,7 +23773,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -29571,6 +29598,15 @@ "node": ">= 0.8" } }, + "node_modules/untildify": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/untildify/-/untildify-3.0.3.tgz", + "integrity": "sha512-iSk/J8efr8uPT/Z4eSUywnqyrQU7DSdMfdqK4iWEaUVVmcP5JcnpRqmVMwcwcnmI1ATFNgC5V90u09tBynNFKA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/upath": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", @@ -31161,6 +31197,12 @@ "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "license": "MIT" }, + "node_modules/winreg": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz", + "integrity": "sha512-IHpzORub7kYlb8A43Iig3reOvlcBJGX9gZ0WycHhghHtA65X0LYnMRuJs+aH1abVnMJztQkvQNlltnbPi5aGIA==", + "license": "BSD-2-Clause" + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index d10ab70..31b61e4 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "@spartan-ng/cli": "^0.0.1-alpha.589", "@spartan-ng/ui-core": "^0.0.1-alpha.380", "@timephy/rnnoise-wasm": "^1.0.0", + "auto-launch": "^5.0.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cytoscape": "^3.33.1", @@ -96,6 +97,7 @@ "@eslint/js": "^9.39.3", "@stylistic/eslint-plugin-js": "^4.4.1", "@stylistic/eslint-plugin-ts": "^4.4.1", + "@types/auto-launch": "^5.0.5", "@types/simple-peer": "^9.11.9", "@types/uuid": "^10.0.0", "angular-eslint": "21.2.0", diff --git a/src/app/core/services/settings-modal.service.ts b/src/app/core/services/settings-modal.service.ts index 82ed72c..6168d34 100644 --- a/src/app/core/services/settings-modal.service.ts +++ b/src/app/core/services/settings-modal.service.ts @@ -1,13 +1,13 @@ import { Injectable, signal } from '@angular/core'; -export type SettingsPage = 'network' | 'voice' | 'updates' | 'debugging' | 'server' | 'members' | 'bans' | 'permissions'; +export type SettingsPage = 'general' | 'network' | 'voice' | 'updates' | 'debugging' | 'server' | 'members' | 'bans' | 'permissions'; @Injectable({ providedIn: 'root' }) export class SettingsModalService { readonly isOpen = signal(false); - readonly activePage = signal('network'); + readonly activePage = signal('general'); readonly targetServerId = signal(null); - open(page: SettingsPage = 'network', serverId?: string): void { + open(page: SettingsPage = 'general', serverId?: string): void { this.activePage.set(page); this.targetServerId.set(serverId ?? null); this.isOpen.set(true); diff --git a/src/app/features/settings/settings-modal/general-settings/general-settings.component.html b/src/app/features/settings/settings-modal/general-settings/general-settings.component.html new file mode 100644 index 0000000..29c2ed1 --- /dev/null +++ b/src/app/features/settings/settings-modal/general-settings/general-settings.component.html @@ -0,0 +1,47 @@ +
+
+
+ +

Application

+
+ +
+
+
+

Launch on system startup

+ + @if (isElectron) { +

Automatically start MetoYou when you sign in

+ } @else { +

This setting is only available in the desktop app.

+ } +
+ + +
+
+
+
diff --git a/src/app/features/settings/settings-modal/general-settings/general-settings.component.ts b/src/app/features/settings/settings-modal/general-settings/general-settings.component.ts new file mode 100644 index 0000000..6c88464 --- /dev/null +++ b/src/app/features/settings/settings-modal/general-settings/general-settings.component.ts @@ -0,0 +1,92 @@ +/* eslint-disable @typescript-eslint/member-ordering */ +import { + Component, + inject, + signal +} from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { lucidePower } from '@ng-icons/lucide'; + +import { PlatformService } from '../../../../core/services/platform.service'; + +interface DesktopSettingsSnapshot { + autoStart: boolean; +} + +interface GeneralSettingsElectronApi { + getDesktopSettings?: () => Promise; + setDesktopSettings?: (patch: { autoStart?: boolean }) => Promise; +} + +type GeneralSettingsWindow = Window & { + electronAPI?: GeneralSettingsElectronApi; +}; + +@Component({ + selector: 'app-general-settings', + standalone: true, + imports: [CommonModule, NgIcon], + viewProviders: [ + provideIcons({ + lucidePower + }) + ], + templateUrl: './general-settings.component.html' +}) +export class GeneralSettingsComponent { + private platform = inject(PlatformService); + + readonly isElectron = this.platform.isElectron; + autoStart = signal(false); + savingAutoStart = signal(false); + + constructor() { + if (this.isElectron) { + void this.loadDesktopSettings(); + } + } + + async onAutoStartChange(event: Event): Promise { + const input = event.target as HTMLInputElement; + const enabled = !!input.checked; + const api = this.getElectronApi(); + + if (!this.isElectron || !api?.setDesktopSettings) { + input.checked = this.autoStart(); + return; + } + + this.savingAutoStart.set(true); + + try { + const snapshot = await api.setDesktopSettings({ autoStart: enabled }); + + this.autoStart.set(snapshot.autoStart); + } catch { + input.checked = this.autoStart(); + } finally { + this.savingAutoStart.set(false); + } + } + + private async loadDesktopSettings(): Promise { + const api = this.getElectronApi(); + + if (!api?.getDesktopSettings) { + return; + } + + try { + const snapshot = await api.getDesktopSettings(); + + this.autoStart.set(snapshot.autoStart); + } catch {} + } + + private getElectronApi(): GeneralSettingsElectronApi | null { + return typeof window !== 'undefined' + ? (window as GeneralSettingsWindow).electronAPI ?? null + : null; + } +} diff --git a/src/app/features/settings/settings-modal/settings-modal.component.html b/src/app/features/settings/settings-modal/settings-modal.component.html index 6e9bb2c..c886703 100644 --- a/src/app/features/settings/settings-modal/settings-modal.component.html +++ b/src/app/features/settings/settings-modal/settings-modal.component.html @@ -116,6 +116,9 @@

@switch (activePage()) { + @case ('general') { + General + } @case ('network') { Network } @@ -157,6 +160,9 @@
@switch (activePage()) { + @case ('general') { + + } @case ('network') { } diff --git a/src/app/features/settings/settings-modal/settings-modal.component.ts b/src/app/features/settings/settings-modal/settings-modal.component.ts index ed25b61..2182739 100644 --- a/src/app/features/settings/settings-modal/settings-modal.component.ts +++ b/src/app/features/settings/settings-modal/settings-modal.component.ts @@ -31,6 +31,7 @@ import { Room, UserRole } from '../../../core/models/index'; import { findRoomMember } from '../../../store/rooms/room-members.helpers'; import { WebRTCService } from '../../../core/services/webrtc.service'; +import { GeneralSettingsComponent } from './general-settings/general-settings.component'; import { NetworkSettingsComponent } from './network-settings/network-settings.component'; import { VoiceSettingsComponent } from './voice-settings/voice-settings.component'; import { ServerSettingsComponent } from './server-settings/server-settings.component'; @@ -48,6 +49,7 @@ import { THIRD_PARTY_LICENSES, type ThirdPartyLicense } from './third-party-lice CommonModule, FormsModule, NgIcon, + GeneralSettingsComponent, NetworkSettingsComponent, VoiceSettingsComponent, UpdatesSettingsComponent, @@ -89,6 +91,9 @@ export class SettingsModalComponent { activePage = this.modal.activePage; readonly globalPages: { id: SettingsPage; label: string; icon: string }[] = [ + { id: 'general', + label: 'General', + icon: 'lucideSettings' }, { id: 'network', label: 'Network', icon: 'lucideGlobe' },