feat: Close to tray
This commit is contained in:
@@ -7,7 +7,13 @@ import {
|
|||||||
destroyDatabase,
|
destroyDatabase,
|
||||||
getDataSource
|
getDataSource
|
||||||
} from '../db/database';
|
} from '../db/database';
|
||||||
import { createWindow, getDockIconPath } from '../window/create-window';
|
import {
|
||||||
|
createWindow,
|
||||||
|
getDockIconPath,
|
||||||
|
getMainWindow,
|
||||||
|
prepareWindowForAppQuit,
|
||||||
|
showMainWindow
|
||||||
|
} from '../window/create-window';
|
||||||
import {
|
import {
|
||||||
setupCqrsHandlers,
|
setupCqrsHandlers,
|
||||||
setupSystemHandlers,
|
setupSystemHandlers,
|
||||||
@@ -30,8 +36,13 @@ export function registerAppLifecycle(): void {
|
|||||||
await createWindow();
|
await createWindow();
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
|
if (getMainWindow()) {
|
||||||
|
void showMainWindow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0)
|
if (BrowserWindow.getAllWindows().length === 0)
|
||||||
createWindow();
|
void createWindow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -41,6 +52,8 @@ export function registerAppLifecycle(): void {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.on('before-quit', async (event) => {
|
app.on('before-quit', async (event) => {
|
||||||
|
prepareWindowForAppQuit();
|
||||||
|
|
||||||
if (getDataSource()?.isInitialized) {
|
if (getDataSource()?.isInitialized) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
shutdownDesktopUpdater();
|
shutdownDesktopUpdater();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export type AutoUpdateMode = 'auto' | 'off' | 'version';
|
|||||||
export interface DesktopSettings {
|
export interface DesktopSettings {
|
||||||
autoUpdateMode: AutoUpdateMode;
|
autoUpdateMode: AutoUpdateMode;
|
||||||
autoStart: boolean;
|
autoStart: boolean;
|
||||||
|
closeToTray: boolean;
|
||||||
hardwareAcceleration: boolean;
|
hardwareAcceleration: boolean;
|
||||||
manifestUrls: string[];
|
manifestUrls: string[];
|
||||||
preferredVersion: string | null;
|
preferredVersion: string | null;
|
||||||
@@ -21,6 +22,7 @@ export interface DesktopSettingsSnapshot extends DesktopSettings {
|
|||||||
const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
|
const DEFAULT_DESKTOP_SETTINGS: DesktopSettings = {
|
||||||
autoUpdateMode: 'auto',
|
autoUpdateMode: 'auto',
|
||||||
autoStart: true,
|
autoStart: true,
|
||||||
|
closeToTray: true,
|
||||||
hardwareAcceleration: true,
|
hardwareAcceleration: true,
|
||||||
manifestUrls: [],
|
manifestUrls: [],
|
||||||
preferredVersion: null,
|
preferredVersion: null,
|
||||||
@@ -86,6 +88,9 @@ export function readDesktopSettings(): DesktopSettings {
|
|||||||
autoStart: typeof parsed.autoStart === 'boolean'
|
autoStart: typeof parsed.autoStart === 'boolean'
|
||||||
? parsed.autoStart
|
? parsed.autoStart
|
||||||
: DEFAULT_DESKTOP_SETTINGS.autoStart,
|
: DEFAULT_DESKTOP_SETTINGS.autoStart,
|
||||||
|
closeToTray: typeof parsed.closeToTray === 'boolean'
|
||||||
|
? parsed.closeToTray
|
||||||
|
: DEFAULT_DESKTOP_SETTINGS.closeToTray,
|
||||||
vaapiVideoEncode: typeof parsed.vaapiVideoEncode === 'boolean'
|
vaapiVideoEncode: typeof parsed.vaapiVideoEncode === 'boolean'
|
||||||
? parsed.vaapiVideoEncode
|
? parsed.vaapiVideoEncode
|
||||||
: DEFAULT_DESKTOP_SETTINGS.vaapiVideoEncode,
|
: DEFAULT_DESKTOP_SETTINGS.vaapiVideoEncode,
|
||||||
@@ -110,6 +115,9 @@ export function updateDesktopSettings(patch: Partial<DesktopSettings>): DesktopS
|
|||||||
autoStart: typeof mergedSettings.autoStart === 'boolean'
|
autoStart: typeof mergedSettings.autoStart === 'boolean'
|
||||||
? mergedSettings.autoStart
|
? mergedSettings.autoStart
|
||||||
: DEFAULT_DESKTOP_SETTINGS.autoStart,
|
: DEFAULT_DESKTOP_SETTINGS.autoStart,
|
||||||
|
closeToTray: typeof mergedSettings.closeToTray === 'boolean'
|
||||||
|
? mergedSettings.closeToTray
|
||||||
|
: DEFAULT_DESKTOP_SETTINGS.closeToTray,
|
||||||
hardwareAcceleration: typeof mergedSettings.hardwareAcceleration === 'boolean'
|
hardwareAcceleration: typeof mergedSettings.hardwareAcceleration === 'boolean'
|
||||||
? mergedSettings.hardwareAcceleration
|
? mergedSettings.hardwareAcceleration
|
||||||
: DEFAULT_DESKTOP_SETTINGS.hardwareAcceleration,
|
: DEFAULT_DESKTOP_SETTINGS.hardwareAcceleration,
|
||||||
|
|||||||
@@ -34,7 +34,11 @@ import {
|
|||||||
} from '../update/desktop-updater';
|
} from '../update/desktop-updater';
|
||||||
import { consumePendingDeepLink } from '../app/deep-links';
|
import { consumePendingDeepLink } from '../app/deep-links';
|
||||||
import { synchronizeAutoStartSetting } from '../app/auto-start';
|
import { synchronizeAutoStartSetting } from '../app/auto-start';
|
||||||
import { getMainWindow, getWindowIconPath } from '../window/create-window';
|
import {
|
||||||
|
getMainWindow,
|
||||||
|
getWindowIconPath,
|
||||||
|
updateCloseToTraySetting
|
||||||
|
} from '../window/create-window';
|
||||||
|
|
||||||
const DEFAULT_MIME_TYPE = 'application/octet-stream';
|
const DEFAULT_MIME_TYPE = 'application/octet-stream';
|
||||||
const FILE_CLIPBOARD_FORMATS = [
|
const FILE_CLIPBOARD_FORMATS = [
|
||||||
@@ -407,6 +411,7 @@ export function setupSystemHandlers(): void {
|
|||||||
const snapshot = updateDesktopSettings(patch);
|
const snapshot = updateDesktopSettings(patch);
|
||||||
|
|
||||||
await synchronizeAutoStartSetting(snapshot.autoStart);
|
await synchronizeAutoStartSetting(snapshot.autoStart);
|
||||||
|
updateCloseToTraySetting(snapshot.closeToTray);
|
||||||
await handleDesktopSettingsChanged();
|
await handleDesktopSettingsChanged();
|
||||||
return snapshot;
|
return snapshot;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export interface ElectronAPI {
|
|||||||
getDesktopSettings: () => Promise<{
|
getDesktopSettings: () => Promise<{
|
||||||
autoUpdateMode: 'auto' | 'off' | 'version';
|
autoUpdateMode: 'auto' | 'off' | 'version';
|
||||||
autoStart: boolean;
|
autoStart: boolean;
|
||||||
|
closeToTray: boolean;
|
||||||
hardwareAcceleration: boolean;
|
hardwareAcceleration: boolean;
|
||||||
manifestUrls: string[];
|
manifestUrls: string[];
|
||||||
preferredVersion: string | null;
|
preferredVersion: string | null;
|
||||||
@@ -157,6 +158,7 @@ export interface ElectronAPI {
|
|||||||
setDesktopSettings: (patch: {
|
setDesktopSettings: (patch: {
|
||||||
autoUpdateMode?: 'auto' | 'off' | 'version';
|
autoUpdateMode?: 'auto' | 'off' | 'version';
|
||||||
autoStart?: boolean;
|
autoStart?: boolean;
|
||||||
|
closeToTray?: boolean;
|
||||||
hardwareAcceleration?: boolean;
|
hardwareAcceleration?: boolean;
|
||||||
manifestUrls?: string[];
|
manifestUrls?: string[];
|
||||||
preferredVersion?: string | null;
|
preferredVersion?: string | null;
|
||||||
@@ -164,6 +166,7 @@ export interface ElectronAPI {
|
|||||||
}) => Promise<{
|
}) => Promise<{
|
||||||
autoUpdateMode: 'auto' | 'off' | 'version';
|
autoUpdateMode: 'auto' | 'off' | 'version';
|
||||||
autoStart: boolean;
|
autoStart: boolean;
|
||||||
|
closeToTray: boolean;
|
||||||
hardwareAcceleration: boolean;
|
hardwareAcceleration: boolean;
|
||||||
manifestUrls: string[];
|
manifestUrls: string[];
|
||||||
preferredVersion: string | null;
|
preferredVersion: string | null;
|
||||||
|
|||||||
@@ -2,13 +2,19 @@ import {
|
|||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
desktopCapturer,
|
desktopCapturer,
|
||||||
|
Menu,
|
||||||
session,
|
session,
|
||||||
shell
|
shell,
|
||||||
|
Tray
|
||||||
} from 'electron';
|
} from 'electron';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
import { readDesktopSettings } from '../desktop-settings';
|
||||||
|
|
||||||
let mainWindow: BrowserWindow | null = null;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
let tray: Tray | null = null;
|
||||||
|
let closeToTrayEnabled = true;
|
||||||
|
let appQuitting = false;
|
||||||
|
|
||||||
const WINDOW_STATE_CHANGED_CHANNEL = 'window-state-changed';
|
const WINDOW_STATE_CHANGED_CHANNEL = 'window-state-changed';
|
||||||
|
|
||||||
@@ -40,12 +46,107 @@ export function getDockIconPath(): string | undefined {
|
|||||||
return getExistingAssetPath('macos', '1024x1024.png');
|
return getExistingAssetPath('macos', '1024x1024.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTrayIconPath(): string | undefined {
|
||||||
|
if (process.platform === 'win32')
|
||||||
|
return getExistingAssetPath('windows', 'icon.ico');
|
||||||
|
|
||||||
|
return getExistingAssetPath('icon.png');
|
||||||
|
}
|
||||||
|
|
||||||
export { getWindowIconPath };
|
export { getWindowIconPath };
|
||||||
|
|
||||||
export function getMainWindow(): BrowserWindow | null {
|
export function getMainWindow(): BrowserWindow | null {
|
||||||
return mainWindow;
|
return mainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function destroyTray(): void {
|
||||||
|
if (!tray) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tray.destroy();
|
||||||
|
tray = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestAppQuit(): void {
|
||||||
|
prepareWindowForAppQuit();
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureTray(): void {
|
||||||
|
if (tray) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trayIconPath = getTrayIconPath();
|
||||||
|
|
||||||
|
if (!trayIconPath) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tray = new Tray(trayIconPath);
|
||||||
|
tray.setToolTip('MetoYou');
|
||||||
|
tray.setContextMenu(
|
||||||
|
Menu.buildFromTemplate([
|
||||||
|
{
|
||||||
|
label: 'Open MetoYou',
|
||||||
|
click: () => {
|
||||||
|
void showMainWindow();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Close MetoYou',
|
||||||
|
click: () => {
|
||||||
|
requestAppQuit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
|
tray.on('click', () => {
|
||||||
|
void showMainWindow();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideWindowToTray(): void {
|
||||||
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow.hide();
|
||||||
|
emitWindowState();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateCloseToTraySetting(enabled: boolean): void {
|
||||||
|
closeToTrayEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function prepareWindowForAppQuit(): void {
|
||||||
|
appQuitting = true;
|
||||||
|
destroyTray();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function showMainWindow(): Promise<void> {
|
||||||
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||||
|
await createWindow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mainWindow.isMinimized()) {
|
||||||
|
mainWindow.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mainWindow.isVisible()) {
|
||||||
|
mainWindow.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow.focus();
|
||||||
|
emitWindowState();
|
||||||
|
}
|
||||||
|
|
||||||
function emitWindowState(): void {
|
function emitWindowState(): void {
|
||||||
if (!mainWindow || mainWindow.isDestroyed()) {
|
if (!mainWindow || mainWindow.isDestroyed()) {
|
||||||
return;
|
return;
|
||||||
@@ -60,6 +161,9 @@ function emitWindowState(): void {
|
|||||||
export async function createWindow(): Promise<void> {
|
export async function createWindow(): Promise<void> {
|
||||||
const windowIconPath = getWindowIconPath();
|
const windowIconPath = getWindowIconPath();
|
||||||
|
|
||||||
|
closeToTrayEnabled = readDesktopSettings().closeToTray;
|
||||||
|
ensureTray();
|
||||||
|
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1400,
|
width: 1400,
|
||||||
height: 900,
|
height: 900,
|
||||||
@@ -120,6 +224,15 @@ export async function createWindow(): Promise<void> {
|
|||||||
await mainWindow.loadFile(path.join(__dirname, '..', '..', 'client', 'browser', 'index.html'));
|
await mainWindow.loadFile(path.join(__dirname, '..', '..', 'client', 'browser', 'index.html'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mainWindow.on('close', (event) => {
|
||||||
|
if (appQuitting || !closeToTrayEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
hideWindowToTray();
|
||||||
|
});
|
||||||
|
|
||||||
mainWindow.on('closed', () => {
|
mainWindow.on('closed', () => {
|
||||||
mainWindow = null;
|
mainWindow = null;
|
||||||
});
|
});
|
||||||
@@ -141,6 +254,14 @@ export async function createWindow(): Promise<void> {
|
|||||||
emitWindowState();
|
emitWindowState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
mainWindow.on('show', () => {
|
||||||
|
emitWindowState();
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.on('hide', () => {
|
||||||
|
emitWindowState();
|
||||||
|
});
|
||||||
|
|
||||||
emitWindowState();
|
emitWindowState();
|
||||||
|
|
||||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export interface DesktopUpdateState {
|
|||||||
export interface DesktopSettingsSnapshot {
|
export interface DesktopSettingsSnapshot {
|
||||||
autoUpdateMode: AutoUpdateMode;
|
autoUpdateMode: AutoUpdateMode;
|
||||||
autoStart: boolean;
|
autoStart: boolean;
|
||||||
|
closeToTray: boolean;
|
||||||
hardwareAcceleration: boolean;
|
hardwareAcceleration: boolean;
|
||||||
manifestUrls: string[];
|
manifestUrls: string[];
|
||||||
preferredVersion: string | null;
|
preferredVersion: string | null;
|
||||||
@@ -99,6 +100,7 @@ export interface DesktopSettingsSnapshot {
|
|||||||
export interface DesktopSettingsPatch {
|
export interface DesktopSettingsPatch {
|
||||||
autoUpdateMode?: AutoUpdateMode;
|
autoUpdateMode?: AutoUpdateMode;
|
||||||
autoStart?: boolean;
|
autoStart?: boolean;
|
||||||
|
closeToTray?: boolean;
|
||||||
hardwareAcceleration?: boolean;
|
hardwareAcceleration?: boolean;
|
||||||
manifestUrls?: string[];
|
manifestUrls?: string[];
|
||||||
preferredVersion?: string | null;
|
preferredVersion?: string | null;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export enum AppSound {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Path prefix for audio assets (served from the `assets/audio/` folder). */
|
/** Path prefix for audio assets (served from the `assets/audio/` folder). */
|
||||||
const AUDIO_BASE = '/assets/audio';
|
const AUDIO_BASE = 'assets/audio';
|
||||||
/** File extension used for all sound-effect assets. */
|
/** File extension used for all sound-effect assets. */
|
||||||
const AUDIO_EXT = 'wav';
|
const AUDIO_EXT = 'wav';
|
||||||
/** localStorage key for persisting notification volume. */
|
/** localStorage key for persisting notification volume. */
|
||||||
@@ -36,6 +36,8 @@ export class NotificationAudioService {
|
|||||||
/** Pre-loaded audio buffers keyed by {@link AppSound}. */
|
/** Pre-loaded audio buffers keyed by {@link AppSound}. */
|
||||||
private readonly cache = new Map<AppSound, HTMLAudioElement>();
|
private readonly cache = new Map<AppSound, HTMLAudioElement>();
|
||||||
|
|
||||||
|
private readonly sources = new Map<AppSound, string>();
|
||||||
|
|
||||||
/** Reactive notification volume (0 - 1), persisted to localStorage. */
|
/** Reactive notification volume (0 - 1), persisted to localStorage. */
|
||||||
readonly notificationVolume = signal(this.loadVolume());
|
readonly notificationVolume = signal(this.loadVolume());
|
||||||
|
|
||||||
@@ -46,13 +48,22 @@ export class NotificationAudioService {
|
|||||||
/** Eagerly create (and start loading) an {@link HTMLAudioElement} for every known sound. */
|
/** Eagerly create (and start loading) an {@link HTMLAudioElement} for every known sound. */
|
||||||
private preload(): void {
|
private preload(): void {
|
||||||
for (const sound of Object.values(AppSound)) {
|
for (const sound of Object.values(AppSound)) {
|
||||||
const audio = new Audio(`${AUDIO_BASE}/${sound}.${AUDIO_EXT}`);
|
const src = this.resolveAudioUrl(sound);
|
||||||
|
const audio = new Audio();
|
||||||
|
|
||||||
audio.preload = 'auto';
|
audio.preload = 'auto';
|
||||||
|
audio.src = src;
|
||||||
|
audio.load();
|
||||||
|
|
||||||
|
this.sources.set(sound, src);
|
||||||
this.cache.set(sound, audio);
|
this.cache.set(sound, audio);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private resolveAudioUrl(sound: AppSound): string {
|
||||||
|
return new URL(`${AUDIO_BASE}/${sound}.${AUDIO_EXT}`, document.baseURI).toString();
|
||||||
|
}
|
||||||
|
|
||||||
/** Read persisted volume from localStorage, falling back to the default. */
|
/** Read persisted volume from localStorage, falling back to the default. */
|
||||||
private loadVolume(): number {
|
private loadVolume(): number {
|
||||||
try {
|
try {
|
||||||
@@ -96,8 +107,9 @@ export class NotificationAudioService {
|
|||||||
*/
|
*/
|
||||||
play(sound: AppSound, volumeOverride?: number): void {
|
play(sound: AppSound, volumeOverride?: number): void {
|
||||||
const cached = this.cache.get(sound);
|
const cached = this.cache.get(sound);
|
||||||
|
const src = this.sources.get(sound);
|
||||||
|
|
||||||
if (!cached)
|
if (!cached || !src)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const vol = volumeOverride ?? this.notificationVolume();
|
const vol = volumeOverride ?? this.notificationVolume();
|
||||||
@@ -105,12 +117,23 @@ export class NotificationAudioService {
|
|||||||
if (vol === 0)
|
if (vol === 0)
|
||||||
return; // skip playback when muted
|
return; // skip playback when muted
|
||||||
|
|
||||||
|
if (cached.readyState === HTMLMediaElement.HAVE_NOTHING) {
|
||||||
|
cached.load();
|
||||||
|
}
|
||||||
|
|
||||||
// Clone so overlapping plays don't cut each other off.
|
// Clone so overlapping plays don't cut each other off.
|
||||||
const clone = cached.cloneNode(true) as HTMLAudioElement;
|
const clone = cached.cloneNode(true) as HTMLAudioElement;
|
||||||
|
|
||||||
|
clone.preload = 'auto';
|
||||||
clone.volume = Math.max(0, Math.min(1, vol));
|
clone.volume = Math.max(0, Math.min(1, vol));
|
||||||
clone.play().catch(() => {
|
clone.play().catch(() => {
|
||||||
/* swallow autoplay errors */
|
const fallback = new Audio(src);
|
||||||
|
|
||||||
|
fallback.preload = 'auto';
|
||||||
|
fallback.volume = clone.volume;
|
||||||
|
fallback.play().catch(() => {
|
||||||
|
/* swallow autoplay errors */
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,39 +8,77 @@
|
|||||||
<h4 class="text-sm font-semibold text-foreground">Application</h4>
|
<h4 class="text-sm font-semibold text-foreground">Application</h4>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="space-y-3">
|
||||||
class="rounded-lg border border-border bg-secondary/20 p-4 transition-opacity"
|
<div
|
||||||
[class.opacity-60]="!isElectron"
|
class="rounded-lg border border-border bg-secondary/20 p-4 transition-opacity"
|
||||||
>
|
[class.opacity-60]="!isElectron"
|
||||||
<div class="flex items-center justify-between gap-4">
|
>
|
||||||
<div>
|
<div class="flex items-center justify-between gap-4">
|
||||||
<p class="text-sm font-medium text-foreground">Launch on system startup</p>
|
<div>
|
||||||
|
<p class="text-sm font-medium text-foreground">Launch on system startup</p>
|
||||||
|
|
||||||
@if (isElectron) {
|
@if (isElectron) {
|
||||||
<p class="text-xs text-muted-foreground">Automatically start MetoYou when you sign in</p>
|
<p class="text-xs text-muted-foreground">Automatically start MetoYou when you sign in</p>
|
||||||
} @else {
|
} @else {
|
||||||
<p class="text-xs text-muted-foreground">This setting is only available in the desktop app.</p>
|
<p class="text-xs text-muted-foreground">This setting is only available in the desktop app.</p>
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label
|
||||||
|
class="relative inline-flex items-center"
|
||||||
|
[class.cursor-pointer]="isElectron && !savingAutoStart()"
|
||||||
|
[class.cursor-not-allowed]="!isElectron || savingAutoStart()"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
[checked]="autoStart()"
|
||||||
|
[disabled]="!isElectron || savingAutoStart()"
|
||||||
|
(change)="onAutoStartChange($event)"
|
||||||
|
id="general-auto-start-toggle"
|
||||||
|
aria-label="Toggle launch on startup"
|
||||||
|
class="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="w-10 h-5 bg-secondary rounded-full peer peer-checked:bg-primary peer-disabled:bg-muted/80 peer-disabled:after:bg-muted-foreground/40 peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all"
|
||||||
|
></div>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label
|
<div
|
||||||
class="relative inline-flex items-center"
|
class="rounded-lg border border-border bg-secondary/20 p-4 transition-opacity"
|
||||||
[class.cursor-pointer]="isElectron && !savingAutoStart()"
|
[class.opacity-60]="!isElectron"
|
||||||
[class.cursor-not-allowed]="!isElectron || savingAutoStart()"
|
>
|
||||||
>
|
<div class="flex items-center justify-between gap-4">
|
||||||
<input
|
<div>
|
||||||
type="checkbox"
|
<p class="text-sm font-medium text-foreground">Minimize to tray on close</p>
|
||||||
[checked]="autoStart()"
|
|
||||||
[disabled]="!isElectron || savingAutoStart()"
|
@if (isElectron) {
|
||||||
(change)="onAutoStartChange($event)"
|
<p class="text-xs text-muted-foreground">Keep MetoYou running in the tray when you click the X button</p>
|
||||||
id="general-auto-start-toggle"
|
} @else {
|
||||||
aria-label="Toggle launch on startup"
|
<p class="text-xs text-muted-foreground">This setting is only available in the desktop app.</p>
|
||||||
class="sr-only peer"
|
}
|
||||||
/>
|
</div>
|
||||||
<div
|
|
||||||
class="w-10 h-5 bg-secondary rounded-full peer peer-checked:bg-primary peer-disabled:bg-muted/80 peer-disabled:after:bg-muted-foreground/40 peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all"
|
<label
|
||||||
></div>
|
class="relative inline-flex items-center"
|
||||||
</label>
|
[class.cursor-pointer]="isElectron && !savingCloseToTray()"
|
||||||
|
[class.cursor-not-allowed]="!isElectron || savingCloseToTray()"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
[checked]="closeToTray()"
|
||||||
|
[disabled]="!isElectron || savingCloseToTray()"
|
||||||
|
(change)="onCloseToTrayChange($event)"
|
||||||
|
id="general-close-to-tray-toggle"
|
||||||
|
aria-label="Toggle minimize to tray on close"
|
||||||
|
class="sr-only peer"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="w-10 h-5 bg-secondary rounded-full peer peer-checked:bg-primary peer-disabled:bg-muted/80 peer-disabled:after:bg-muted-foreground/40 peer-checked:after:translate-x-full after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all"
|
||||||
|
></div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { NgIcon, provideIcons } from '@ng-icons/core';
|
import { NgIcon, provideIcons } from '@ng-icons/core';
|
||||||
import { lucidePower } from '@ng-icons/lucide';
|
import { lucidePower } from '@ng-icons/lucide';
|
||||||
|
|
||||||
|
import type { DesktopSettingsSnapshot } from '../../../../core/platform/electron/electron-api.models';
|
||||||
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
import { ElectronBridgeService } from '../../../../core/platform/electron/electron-bridge.service';
|
||||||
import { PlatformService } from '../../../../core/platform';
|
import { PlatformService } from '../../../../core/platform';
|
||||||
|
|
||||||
@@ -28,7 +29,9 @@ export class GeneralSettingsComponent {
|
|||||||
|
|
||||||
readonly isElectron = this.platform.isElectron;
|
readonly isElectron = this.platform.isElectron;
|
||||||
autoStart = signal(false);
|
autoStart = signal(false);
|
||||||
|
closeToTray = signal(true);
|
||||||
savingAutoStart = signal(false);
|
savingAutoStart = signal(false);
|
||||||
|
savingCloseToTray = signal(false);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
if (this.isElectron) {
|
if (this.isElectron) {
|
||||||
@@ -51,7 +54,7 @@ export class GeneralSettingsComponent {
|
|||||||
try {
|
try {
|
||||||
const snapshot = await api.setDesktopSettings({ autoStart: enabled });
|
const snapshot = await api.setDesktopSettings({ autoStart: enabled });
|
||||||
|
|
||||||
this.autoStart.set(snapshot.autoStart);
|
this.applyDesktopSettings(snapshot);
|
||||||
} catch {
|
} catch {
|
||||||
input.checked = this.autoStart();
|
input.checked = this.autoStart();
|
||||||
} finally {
|
} finally {
|
||||||
@@ -59,6 +62,29 @@ export class GeneralSettingsComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onCloseToTrayChange(event: Event): Promise<void> {
|
||||||
|
const input = event.target as HTMLInputElement;
|
||||||
|
const enabled = !!input.checked;
|
||||||
|
const api = this.electronBridge.getApi();
|
||||||
|
|
||||||
|
if (!this.isElectron || !api) {
|
||||||
|
input.checked = this.closeToTray();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.savingCloseToTray.set(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const snapshot = await api.setDesktopSettings({ closeToTray: enabled });
|
||||||
|
|
||||||
|
this.applyDesktopSettings(snapshot);
|
||||||
|
} catch {
|
||||||
|
input.checked = this.closeToTray();
|
||||||
|
} finally {
|
||||||
|
this.savingCloseToTray.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async loadDesktopSettings(): Promise<void> {
|
private async loadDesktopSettings(): Promise<void> {
|
||||||
const api = this.electronBridge.getApi();
|
const api = this.electronBridge.getApi();
|
||||||
|
|
||||||
@@ -69,7 +95,12 @@ export class GeneralSettingsComponent {
|
|||||||
try {
|
try {
|
||||||
const snapshot = await api.getDesktopSettings();
|
const snapshot = await api.getDesktopSettings();
|
||||||
|
|
||||||
this.autoStart.set(snapshot.autoStart);
|
this.applyDesktopSettings(snapshot);
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private applyDesktopSettings(snapshot: DesktopSettingsSnapshot): void {
|
||||||
|
this.autoStart.set(snapshot.autoStart);
|
||||||
|
this.closeToTray.set(snapshot.closeToTray);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user