src/main/index.ts
Rama main · réplica de lectura del repositorio original.
import { is } from '@electron-toolkit/utils';
import {
app,
BrowserWindow,
BrowserWindowConstructorOptions,
globalShortcut,
ipcMain,
Menu,
nativeImage,
nativeTheme,
net,
powerSaveBlocker,
protocol,
Rectangle,
screen,
shell,
Tray,
} from 'electron';
import electronLocalShortcut from 'electron-localshortcut';
import { access, constants } from 'fs';
import path, { join } from 'path';
import { subscribePlayerStateEvent } from './features/core/player-state-broadcast';
import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys';
import { shutdownServer } from './features/core/remote';
import { store } from './features/core/settings';
import {
AppUpdater,
checkAllChannelsAndGetBest,
configureAndGetUpdater,
type UpdaterInstance,
} from './features/core/updater';
import MenuBuilder, { MenuPlaybackState } from './menu';
import {
createLog,
disableAutoUpdates,
hotkeyToElectronAccelerator,
isLinux,
isMacOS,
isWindows,
} from './utils';
import './features';
import { BindingActions, HotkeyBindings } from '/@/shared/types/hotkeys';
import { PlayerRepeat, PlayerStatus, PlayerType, TitleTheme } from '/@/shared/types/types';
protocol.registerSchemesAsPrivileged([{ privileges: { bypassCSP: true }, scheme: 'samo' }]);
// The main process's ONLY crash diagnostics. Both handlers log and do nothing
// else — deliberately.
//
// These used to be duplicated in features/core/player, where they also called
// `cleanupMpv(true)`. Because both are process-global, a stray rejection in any
// unrelated subsystem killed the audio engine mid-playback. Teardown now lives
// entirely in that module's playback lifecycle (before-quit / SIGINT / SIGTERM,
// with a synchronous `exit` safety net), and nothing here touches mpv.
//
// `console.error` was replaced with `createLog` so these actually reach the
// electron-log file. A crash that only prints to a devtools console nobody has
// open is a crash that gets reported as "it just stopped working".
//
// Note that registering `uncaughtException` at all suppresses Electron's
// default crash-and-die behaviour, so the process keeps running afterwards in
// a state we cannot reason about. That is pre-existing behaviour and is left
// alone here rather than changed blind; it is worth revisiting separately.
process.on('uncaughtException', (error: unknown) => {
createLog({ message: `[SYSTEM] Uncaught exception in main process: ${error}`, type: 'error' });
});
process.on('unhandledRejection', (reason: unknown) => {
createLog({
message: `[SYSTEM] Unhandled rejection in main process: ${reason}`,
type: 'error',
});
});
if (store.get('ignore_ssl')) {
app.commandLine.appendSwitch('ignore-certificate-errors');
}
// From https://github.com/tutao/tutanota/commit/92c6ed27625fcf367f0fbcc755d83d7ff8fde94b
if (isLinux() && !process.argv.some((a) => a.startsWith('--password-store='))) {
const passwordStore = store.get('password_store', 'gnome-libsecret') as string;
app.commandLine.appendSwitch('password-store', passwordStore);
}
let mainWindow: BrowserWindow | null = null;
let tray: null | Tray = null;
let exitFromTray = false;
let forceQuit = false;
let powerSaveBlockerId: null | number = null;
let menuBuilder: MenuBuilder | null = null;
let currentPlaybackStatus: PlayerStatus = PlayerStatus.PAUSED;
let currentPrivateMode = false;
let currentRepeatMode: PlayerRepeat = PlayerRepeat.NONE;
let currentSidebarCollapsed = false;
let currentShuffleEnabled = false;
let playbackMenuAccelerators: MenuPlaybackState['accelerators'] = {};
if (process.env.NODE_ENV === 'production') {
import('source-map-support').then((sourceMapSupport) => {
sourceMapSupport.install();
});
}
const isDevelopment = process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
if (isDevelopment) {
import('electron-debug').then((electronDebug) => {
electronDebug.default();
});
}
const installExtensions = async () => {
import('electron-devtools-installer').then((installer) => {
const forceDownload = !!process.env.UPGRADE_EXTENSIONS;
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'];
installer
.installExtension(
extensions.map((name) => installer[name]),
{ forceDownload },
)
.then((installedExtensions) => {
createLog({
message: `Installed extension: ${installedExtensions}`,
type: 'info',
});
})
.catch(() => {
// Ignore
});
});
};
const userDataPath = app.getPath('userData');
if (isDevelopment) {
const devUserDataPath = `${userDataPath}-dev`;
app.setPath('userData', devUserDataPath);
}
const RESOURCES_PATH = app.isPackaged
? path.join(process.resourcesPath, 'assets')
: path.join(__dirname, '../../assets');
const getAssetPath = (...paths: string[]): string => {
const assetFileName = paths[1];
if (
!app.isPackaged &&
paths[0] === 'icons' &&
assetFileName &&
['icon.ico', 'icon.png'].includes(assetFileName)
) {
return path.join(__dirname, '../../src/renderer/assets', ...paths);
}
return path.join(RESOURCES_PATH, ...paths);
};
export const getMainWindow = () => {
return mainWindow;
};
const rebuildMainMenu = () => {
if (!menuBuilder || !mainWindow) return;
menuBuilder.buildMenu({
accelerators: playbackMenuAccelerators,
playbackStatus: currentPlaybackStatus,
privateMode: currentPrivateMode,
repeatMode: currentRepeatMode,
shuffleEnabled: currentShuffleEnabled,
sidebarCollapsed: currentSidebarCollapsed,
});
if (process.platform !== 'darwin') {
Menu.setApplicationMenu(null);
}
};
export const sendToastToRenderer = ({
message,
type,
}: {
message: string;
type: 'error' | 'info' | 'success' | 'warning';
}) => {
getMainWindow()?.webContents.send('toast-from-main', {
message,
type,
});
};
const createWinThumbarButtons = () => {
if (isWindows()) {
getMainWindow()?.setThumbarButtons([
{
click: () => getMainWindow()?.webContents.send('renderer-player-previous'),
icon: nativeImage.createFromPath(getAssetPath('skip-previous.png')),
tooltip: 'Previous Track',
},
{
click: () => getMainWindow()?.webContents.send('renderer-player-play-pause'),
icon: nativeImage.createFromPath(getAssetPath('play-circle.png')),
tooltip: 'Play/Pause',
},
{
click: () => getMainWindow()?.webContents.send('renderer-player-next'),
icon: nativeImage.createFromPath(getAssetPath('skip-next.png')),
tooltip: 'Next Track',
},
]);
}
};
const createTray = () => {
let trayIcon: Electron.NativeImage | string;
if (isMacOS()) {
const iconPath = getAssetPath('icons/trayTemplate.png');
const icon = nativeImage.createFromPath(iconPath).resize({
height: 26,
width: 26,
});
icon.setTemplateImage(true);
trayIcon = icon;
} else if (isLinux()) {
trayIcon = getAssetPath('icons/icon.png');
} else {
trayIcon = getAssetPath('icons/icon.ico');
}
tray = new Tray(trayIcon);
const contextMenu = Menu.buildFromTemplate([
{
click: () => {
getMainWindow()?.webContents.send('renderer-player-play-pause');
},
label: 'Play/Pause',
},
{
click: () => {
getMainWindow()?.webContents.send('renderer-player-next');
},
label: 'Next Track',
},
{
click: () => {
getMainWindow()?.webContents.send('renderer-player-previous');
},
label: 'Previous Track',
},
{
click: () => {
getMainWindow()?.webContents.send('renderer-player-stop');
},
label: 'Stop',
},
{
type: 'separator',
},
{
click: () => {
if (mainWindow === null) createWindow(false);
else {
mainWindow.show();
createWinThumbarButtons();
}
},
label: 'Open main window',
},
{
click: () => {
exitFromTray = true;
app.quit();
},
label: 'Quit',
},
]);
if (!isMacOS()) {
tray.on('click', () => {
if (store.get('window_minimize_to_tray')) {
if (mainWindow?.isVisible()) {
mainWindow?.hide();
} else {
mainWindow?.show();
createWinThumbarButtons();
}
} else {
mainWindow?.show();
createWinThumbarButtons();
}
});
}
tray.setToolTip('samo');
tray.setContextMenu(contextMenu);
};
async function createWindow(first = true): Promise<void> {
if (isDevelopment) {
await installExtensions().catch(console.log);
}
const nativeFrame = store.get('window_window_bar_style', 'linux') === 'linux';
store.set('window_has_frame', nativeFrame);
const nativeFrameConfig: Record<string, BrowserWindowConstructorOptions> = {
linux: {
autoHideMenuBar: true,
frame: true,
},
macOS: {
autoHideMenuBar: true,
frame: true,
titleBarStyle: 'default',
trafficLightPosition: { x: 10, y: 10 },
},
windows: {
autoHideMenuBar: true,
frame: true,
},
};
// Create the browser window.
mainWindow = new BrowserWindow({
acceptFirstMouse: true,
autoHideMenuBar: true,
// Paints black behind the web contents (resizes repaint the window
// background before the renderer catches up) — no white flash.
backgroundColor: '#000000',
frame: false,
...(isMacOS() && {
frame: true,
titleBarStyle: 'hiddenInset',
trafficLightPosition: { x: 14, y: 8 },
}),
height: 900,
icon: isWindows() ? getAssetPath('icons/icon.ico') : getAssetPath('icons/icon.png'),
minHeight: 120,
minWidth: 480,
show: false,
webPreferences: {
allowRunningInsecureContent: !!store.get('ignore_ssl'),
backgroundThrottling: false,
contextIsolation: true,
devTools: true,
nodeIntegration: true,
preload: join(__dirname, '../preload/index.js'),
sandbox: false,
webSecurity: !store.get('ignore_cors'),
},
width: 1440,
...(nativeFrame && isLinux() && nativeFrameConfig.linux),
...(nativeFrame && isMacOS() && nativeFrameConfig.macOS),
...(nativeFrame && isWindows() && nativeFrameConfig.windows),
});
// From https://github.com/electron/electron/issues/526#issuecomment-1663959513
const bounds = store.get('bounds') as Rectangle | undefined;
if (bounds) {
const screenArea = screen.getDisplayMatching(bounds).workArea;
if (
bounds.x > screenArea.x + screenArea.width ||
bounds.x < screenArea.x ||
bounds.y < screenArea.y ||
bounds.y > screenArea.y + screenArea.height
) {
if (bounds.width < screenArea.width && bounds.height < screenArea.height) {
mainWindow.setBounds({ height: bounds.height, width: bounds.width });
} else {
mainWindow.setBounds({ height: 900, width: 1440 });
}
} else {
mainWindow.setBounds(bounds);
}
}
electronLocalShortcut.register(mainWindow, 'Ctrl+Shift+I', () => {
mainWindow?.webContents.openDevTools();
});
ipcMain.on('window-dev-tools', () => {
mainWindow?.webContents.openDevTools();
});
ipcMain.on('window-maximize', () => {
mainWindow?.maximize();
});
ipcMain.on('window-unmaximize', () => {
mainWindow?.unmaximize();
});
ipcMain.on('window-minimize', () => {
mainWindow?.minimize();
});
ipcMain.on('window-close', () => {
mainWindow?.close();
});
ipcMain.on('set-ignore-mouse-events', (_event, ignore: boolean) => {
mainWindow?.setIgnoreMouseEvents(ignore, { forward: true });
});
ipcMain.on('window-quit', () => {
shutdownServer();
mainWindow?.close();
app.exit();
});
ipcMain.handle('window-clear-cache', async () => {
return mainWindow?.webContents.session.clearCache();
});
ipcMain.handle(
'app-check-for-updates',
async (): Promise<{ updateAvailable: boolean; version?: string }> => {
if (disableAutoUpdates()) {
console.log('Auto updates are disabled');
return { updateAvailable: false };
}
try {
console.log('Checking for updates');
const effectiveChannel = store.get('release_channel') as string;
let result: Awaited<ReturnType<UpdaterInstance['checkForUpdates']>> | null;
let updater: UpdaterInstance;
if (effectiveChannel === 'alpha') {
const best = await checkAllChannelsAndGetBest();
result = best.result;
updater = best.updater;
} else {
updater = configureAndGetUpdater();
result = await updater.checkForUpdates();
}
const updateAvailable = result?.isUpdateAvailable ?? false;
console.log('Update available:', updateAvailable);
if (updateAvailable && store.get('disable_auto_updates') !== true) {
if (isMacOS()) {
getMainWindow()?.webContents.send(
'update-available',
result?.updateInfo?.version,
);
} else {
console.log('Downloading update');
updater.downloadUpdate();
}
}
return {
updateAvailable,
version: result?.updateInfo?.version,
};
} catch {
console.log('Error checking for updates');
return { updateAvailable: false };
}
},
);
ipcMain.on('app-restart', () => {
// Fix for .AppImage
if (process.env.APPIMAGE) {
app.exit();
app.relaunch({
args: process.argv.slice(1).concat(['--appimage-extract-and-run']),
execPath: process.env.APPIMAGE,
});
app.exit(0);
} else {
app.relaunch();
app.exit(0);
}
});
ipcMain.on('global-media-keys-enable', () => {
enableMediaKeys(mainWindow);
});
ipcMain.on('global-media-keys-disable', () => {
disableMediaKeys();
});
ipcMain.on('download-url', (_event, url: string) => {
mainWindow?.webContents.downloadURL(url);
});
const globalMediaKeysEnabled = store.get('global_media_hotkeys', true) as boolean;
if (globalMediaKeysEnabled) {
enableMediaKeys(mainWindow);
}
const startWindowMinimized = store.get('window_start_minimized', false) as boolean;
mainWindow.on('ready-to-show', () => {
// mainWindow.show()
if (!mainWindow) {
throw new Error('"mainWindow" is not defined');
}
if (!first || !startWindowMinimized) {
const maximized = store.get('maximized');
const fullScreen = store.get('fullscreen');
if (maximized) {
mainWindow.maximize();
}
if (fullScreen) {
mainWindow.setFullScreen(true);
}
mainWindow.show();
app.focus({ steal: true });
mainWindow.focus();
mainWindow.webContents.focus();
createWinThumbarButtons();
}
});
mainWindow.on('maximize', () => {
mainWindow?.webContents.send('window-maximize-state', true);
});
mainWindow.on('unmaximize', () => {
mainWindow?.webContents.send('window-maximize-state', false);
});
ipcMain.handle('window-is-maximized', () => {
return mainWindow?.isMaximized() ?? false;
});
mainWindow.on('closed', () => {
ipcMain.removeHandler('window-clear-cache');
ipcMain.removeHandler('app-check-for-updates');
ipcMain.removeHandler('window-is-maximized');
mainWindow = null;
});
mainWindow.on('close', (event) => {
store.set('bounds', mainWindow?.getNormalBounds());
store.set('maximized', mainWindow?.isMaximized());
store.set('fullscreen', mainWindow?.isFullScreen());
if (!exitFromTray && store.get('window_exit_to_tray')) {
event.preventDefault();
mainWindow?.hide();
}
if (forceQuit) {
app.exit();
}
});
(mainWindow as any).on('minimize', (event: any) => {
if (store.get('window_minimize_to_tray') === true) {
event.preventDefault();
mainWindow?.hide();
}
});
if (isWindows()) {
app.setAppUserModelId('org.bouliehaan.samo');
}
if (isMacOS()) {
app.on('before-quit', () => {
forceQuit = true;
});
}
menuBuilder = new MenuBuilder(mainWindow);
rebuildMainMenu();
// samo never captures the screen. Nothing in the renderer calls
// getDisplayMedia any more — this handler exists so that stays true even if
// something later does, or if injected content tries.
//
// It used to feed the mpv visualizer: mpv plays outside the renderer, so the
// only way to get samples for it was system audio. Electron's
// `audio: 'loopback'` is Windows-only, so on macOS that audio could only be
// obtained by attaching it to a live ScreenCaptureKit capture of the whole
// display — which is why samo asked for Screen Recording, and why a
// full-screen capture ran for as long as the visualizer was open. The
// visualizer now reads the Web Audio graph and is simply unavailable under
// mpv. See use-is-visualizer-available.ts.
mainWindow.webContents.session.setDisplayMediaRequestHandler((_request, callback) => {
callback({});
});
if (!disableAutoUpdates() && store.get('disable_auto_updates') !== true) {
new AppUpdater(getMainWindow);
}
const theme = store.get('theme') as TitleTheme | undefined;
nativeTheme.themeSource = theme || 'dark';
mainWindow.webContents.setWindowOpenHandler((details) => {
shell.openExternal(details.url);
return { action: 'deny' };
});
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']);
} else {
mainWindow.loadFile(join(__dirname, '../renderer/index.html'));
}
}
// Disable Chromium's hardware media key handling when:
// - Linux (MPRIS handles it instead)
// - MPV playback (not supported)
// - Windows/non-macOS desktop that hasn't opted into MediaSession
// On macOS + WEB, always enable so Bluetooth headphone controls reach the app
// via MPRemoteCommandCenter regardless of the MediaSession setting toggle.
const enableMediaSession = store.get('mediaSession', false) as boolean;
const playbackType = store.get('playbackType', PlayerType.WEB) as PlayerType;
const shouldDisableMediaFeatures =
isLinux() || playbackType !== PlayerType.WEB || (!isMacOS() && !enableMediaSession);
const chromiumDisabledFeatures: string[] = [];
// Fractional scaling on Wayland: https://github.com/bouliehaan/samo/issues/1271#issuecomment-4063326712
if (isLinux()) {
chromiumDisabledFeatures.push('WaylandFractionalScaleV1');
}
if (shouldDisableMediaFeatures) {
chromiumDisabledFeatures.push('HardwareMediaKeyHandling', 'MediaSessionService');
}
if (chromiumDisabledFeatures.length > 0) {
app.commandLine.appendSwitch('disable-features', chromiumDisabledFeatures.join(','));
}
// https://github.com/electron/electron/issues/46538#issuecomment-2808806722
app.commandLine.appendSwitch('gtk-version', '3');
const textEntryMenuAcceleratorHotkeys = new Set([
'backspace',
'delete',
'enter',
'escape',
'space',
'tab',
]);
const getMenuAccelerator = (data: HotkeyBindings, action: BindingActions) => {
const hotkey = data[action]?.hotkey;
if (!hotkey) return undefined;
// Plain text-entry keys should stay renderer hotkeys only. Registering them
// as native Electron menu accelerators bypasses focused input fields.
if (textEntryMenuAcceleratorHotkeys.has(hotkey.toLowerCase())) {
return undefined;
}
return hotkeyToElectronAccelerator(hotkey);
};
const HOTKEY_ACTIONS: Partial<Record<BindingActions, () => void>> = {
[BindingActions.GLOBAL_SEARCH]: () => {},
[BindingActions.LOCAL_SEARCH]: () => {},
[BindingActions.MUTE]: () => getMainWindow()?.webContents.send('renderer-player-volume-mute'),
[BindingActions.NEXT]: () => getMainWindow()?.webContents.send('renderer-player-next'),
[BindingActions.PAUSE]: () => getMainWindow()?.webContents.send('renderer-player-pause'),
[BindingActions.PLAY]: () => getMainWindow()?.webContents.send('renderer-player-play'),
[BindingActions.PLAY_PAUSE]: () =>
getMainWindow()?.webContents.send('renderer-player-play-pause'),
[BindingActions.PREVIOUS]: () => getMainWindow()?.webContents.send('renderer-player-previous'),
[BindingActions.SHUFFLE]: () =>
getMainWindow()?.webContents.send('renderer-player-toggle-shuffle'),
[BindingActions.SKIP_BACKWARD]: () =>
getMainWindow()?.webContents.send('renderer-player-skip-backward'),
[BindingActions.SKIP_FORWARD]: () =>
getMainWindow()?.webContents.send('renderer-player-skip-forward'),
[BindingActions.STOP]: () => getMainWindow()?.webContents.send('renderer-player-stop'),
[BindingActions.TOGGLE_FULLSCREEN_PLAYER]: () => {},
[BindingActions.TOGGLE_QUEUE]: () => {},
[BindingActions.TOGGLE_REPEAT]: () =>
getMainWindow()?.webContents.send('renderer-player-toggle-repeat'),
[BindingActions.VOLUME_DOWN]: () =>
getMainWindow()?.webContents.send('renderer-player-volume-down'),
[BindingActions.VOLUME_UP]: () =>
getMainWindow()?.webContents.send('renderer-player-volume-up'),
};
ipcMain.on('set-global-shortcuts', (_event, data: HotkeyBindings) => {
// Since we're not tracking the previous shortcuts, we need to unregister all of them
globalShortcut.unregisterAll();
for (const shortcut of Object.keys(data)) {
const isGlobalHotkey = data[shortcut as BindingActions].isGlobal;
const isValidHotkey =
data[shortcut as BindingActions].hotkey &&
data[shortcut as BindingActions].hotkey !== '';
if (isGlobalHotkey && isValidHotkey) {
const accelerator = hotkeyToElectronAccelerator(
data[shortcut as BindingActions].hotkey,
);
globalShortcut.register(accelerator, () => {
HOTKEY_ACTIONS[shortcut as BindingActions]?.();
});
}
}
playbackMenuAccelerators = {
next: getMenuAccelerator(data, BindingActions.NEXT),
playPause:
getMenuAccelerator(data, BindingActions.PLAY_PAUSE) ||
getMenuAccelerator(data, BindingActions.PLAY) ||
getMenuAccelerator(data, BindingActions.PAUSE),
previous: getMenuAccelerator(data, BindingActions.PREVIOUS),
repeat: getMenuAccelerator(data, BindingActions.TOGGLE_REPEAT),
seekBackward: getMenuAccelerator(data, BindingActions.SKIP_BACKWARD),
seekForward: getMenuAccelerator(data, BindingActions.SKIP_FORWARD),
shuffle: getMenuAccelerator(data, BindingActions.SHUFFLE),
stop: getMenuAccelerator(data, BindingActions.STOP),
volumeDown: getMenuAccelerator(data, BindingActions.VOLUME_DOWN),
volumeUp: getMenuAccelerator(data, BindingActions.VOLUME_UP),
};
if (isMacOS()) {
rebuildMainMenu();
}
const globalMediaKeysEnabled = store.get('global_media_hotkeys', true) as boolean;
if (globalMediaKeysEnabled) {
enableMediaKeys(mainWindow);
}
});
ipcMain.on(
'logger',
(
_event,
data: {
message: string;
type: 'debug' | 'error' | 'info' | 'success' | 'verbose' | 'warning';
},
) => {
createLog(data);
},
);
ipcMain.handle('power-save-blocker-start', () => {
if (powerSaveBlockerId !== null) {
return powerSaveBlockerId;
}
powerSaveBlockerId = powerSaveBlocker.start('prevent-display-sleep');
return powerSaveBlockerId;
});
ipcMain.handle('power-save-blocker-stop', () => {
if (powerSaveBlockerId !== null) {
const stopped = powerSaveBlocker.stop(powerSaveBlockerId);
powerSaveBlockerId = null;
return stopped;
}
return false;
});
ipcMain.handle('power-save-blocker-is-started', () => {
return powerSaveBlockerId !== null && powerSaveBlocker.isStarted(powerSaveBlockerId);
});
app.on('window-all-closed', () => {
globalShortcut.unregisterAll();
// Respect the OSX convention of having the application in memory even
// after all windows have been closed
if (isMacOS()) {
mainWindow = null;
} else {
app.quit();
}
});
const FONT_HEADERS = [
'font/collection',
'font/otf',
'font/sfnt',
'font/ttf',
'font/woff',
'font/woff2',
];
const singleInstance = isDevelopment ? true : app.requestSingleInstanceLock();
if (!singleInstance) {
app.quit();
} else {
app.on('second-instance', () => {
if (mainWindow) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
} else if (!mainWindow.isVisible()) {
mainWindow.show();
}
mainWindow.focus();
}
});
app.whenReady()
.then(() => {
protocol.handle('samo', async (request) => {
const filePath = `file:${request.url.slice('samo:'.length)}`;
const response = await net.fetch(filePath);
const contentType = response.headers.get('content-type');
if (!contentType || !FONT_HEADERS.includes(contentType)) {
getMainWindow()?.webContents.send('custom-font-error', filePath);
return new Response(null, {
status: 403,
statusText: 'Forbidden',
});
}
return response;
});
createWindow();
if (store.get('window_enable_tray', true)) {
createTray();
}
app.on('activate', () => {
// On macOS it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (mainWindow === null) createWindow(false);
else if (!mainWindow.isVisible()) {
mainWindow.show();
createWinThumbarButtons();
}
});
})
.catch(console.log);
}
// Register 'open-item' handler globally, ensuring it is only registered once
if (!ipcMain.eventNames().includes('open-item')) {
ipcMain.handle('open-item', async (_event, path: string) => {
return new Promise<void>((resolve, reject) => {
access(path, constants.F_OK, (error) => {
if (error) {
reject(error);
return;
}
shell.showItemInFolder(path);
resolve();
});
});
});
}
// Register 'open-application-directory' handler globally, ensuring it is only registered once
if (!ipcMain.eventNames().includes('open-application-directory')) {
ipcMain.handle('open-application-directory', async () => {
const userDataPath = app.getPath('userData');
shell.openPath(userDataPath);
});
}
subscribePlayerStateEvent('playback', (status) => {
currentPlaybackStatus = status;
if (!isMacOS()) return;
rebuildMainMenu();
});
subscribePlayerStateEvent('repeat', (repeat) => {
currentRepeatMode = repeat;
if (!isMacOS()) return;
rebuildMainMenu();
});
subscribePlayerStateEvent('shuffle', (shuffle) => {
currentShuffleEnabled = shuffle;
if (!isMacOS()) return;
rebuildMainMenu();
});
subscribePlayerStateEvent('privateMode', (privateMode) => {
currentPrivateMode = privateMode;
if (!isMacOS()) return;
rebuildMainMenu();
});
subscribePlayerStateEvent('sidebarCollapsed', (collapsedSidebar) => {
currentSidebarCollapsed = collapsedSidebar;
if (!isMacOS()) return;
rebuildMainMenu();
});