diff --git a/assets/pause-circle.png b/assets/pause-circle.png new file mode 100644 index 00000000..2636f00b Binary files /dev/null and b/assets/pause-circle.png differ diff --git a/assets/play-circle.png b/assets/play-circle.png new file mode 100644 index 00000000..0d10870e Binary files /dev/null and b/assets/play-circle.png differ diff --git a/assets/skip-next.png b/assets/skip-next.png new file mode 100644 index 00000000..438fb3a3 Binary files /dev/null and b/assets/skip-next.png differ diff --git a/assets/skip-previous.png b/assets/skip-previous.png new file mode 100644 index 00000000..14088508 Binary files /dev/null and b/assets/skip-previous.png differ diff --git a/src/main/main.ts b/src/main/main.ts index e2010894..cfa77ae5 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -9,7 +9,16 @@ * `./src/main.js` using webpack. This gives us some performance wins. */ import path from 'path'; -import { app, BrowserWindow, shell, ipcMain, globalShortcut } from 'electron'; +import { + app, + BrowserWindow, + shell, + ipcMain, + globalShortcut, + Tray, + Menu, + nativeImage, +} from 'electron'; import electronLocalShortcut from 'electron-localshortcut'; import log from 'electron-log'; import { autoUpdater } from 'electron-updater'; @@ -18,7 +27,7 @@ import MpvAPI from 'node-mpv'; import { disableMediaKeys, enableMediaKeys } from './features/core/player/media-keys'; import { store } from './features/core/settings/index'; import MenuBuilder from './menu'; -import { resolveHtmlPath } from './utils'; +import { isLinux, isMacOS, isWindows, resolveHtmlPath } from './utils'; import './features'; declare module 'node-mpv'; @@ -36,6 +45,9 @@ if (store.get('ignore_ssl')) { } let mainWindow: BrowserWindow | null = null; +let tray: Tray | null = null; +let exitFromTray = false; +let forceQuit = false; if (process.env.NODE_ENV === 'production') { const sourceMapSupport = require('source-map-support'); @@ -67,19 +79,105 @@ if (!singleInstance) { app.quit(); } +const RESOURCES_PATH = app.isPackaged + ? path.join(process.resourcesPath, 'assets') + : path.join(__dirname, '../../assets'); + +const getAssetPath = (...paths: string[]): string => { + return path.join(RESOURCES_PATH, ...paths); +}; + +export const getMainWindow = () => { + return mainWindow; +}; + +const createWinThumbarButtons = () => { + if (isWindows()) { + console.log('setting buttons'); + 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 = () => { + if (isMacOS()) { + return; + } + + tray = isLinux() ? new Tray(getAssetPath('icon.png')) : new Tray(getAssetPath('icon.ico')); + 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: () => { + mainWindow?.show(); + createWinThumbarButtons(); + }, + label: 'Open main window', + }, + { + click: () => { + exitFromTray = true; + app.quit(); + }, + label: 'Quit', + }, + ]); + + tray.on('double-click', () => { + mainWindow?.show(); + createWinThumbarButtons(); + }); + + tray.setToolTip('Feishin'); + tray.setContextMenu(contextMenu); +}; + const createWindow = async () => { if (isDevelopment) { await installExtensions(); } - const RESOURCES_PATH = app.isPackaged - ? path.join(process.resourcesPath, 'assets') - : path.join(__dirname, '../../assets'); - - const getAssetPath = (...paths: string[]): string => { - return path.join(RESOURCES_PATH, ...paths); - }; - mainWindow = new BrowserWindow({ frame: false, height: 900, @@ -153,14 +251,41 @@ const createWindow = async () => { mainWindow.minimize(); } else { mainWindow.show(); + createWinThumbarButtons(); } }); mainWindow.on('closed', () => { - // mainWindow?.webContents.send('renderer-player-quit'); mainWindow = null; }); + mainWindow.on('close', (event) => { + if (!exitFromTray && store.get('window_exit_to_tray')) { + if (isMacOS() && !forceQuit) { + exitFromTray = true; + } + event.preventDefault(); + mainWindow?.hide(); + } + }); + + mainWindow.on('minimize', (event: any) => { + if (store.get('window_minimize_to_tray') === true) { + event.preventDefault(); + mainWindow?.hide(); + } + }); + + if (isWindows()) { + app.setAppUserModelId(process.execPath); + } + + if (isMacOS()) { + app.on('before-quit', () => { + forceQuit = true; + }); + } + const menuBuilder = new MenuBuilder(mainWindow); menuBuilder.buildMenu(); @@ -175,16 +300,8 @@ const createWindow = async () => { new AppUpdater(); }; -/** - * Add event listeners... - */ - app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService'); -export const getMainWindow = () => { - return mainWindow; -}; - const MPV_BINARY_PATH = store.get('mpv_path') as string | undefined; const MPV_PARAMETERS = store.get('mpv_parameters') as Array | undefined; @@ -267,11 +384,10 @@ app.on('window-all-closed', () => { // Respect the OSX convention of having the application in memory even // after all windows have been closed - if (process.platform !== 'darwin') { - app.quit(); - } else { - mpv.stop(); + if (isMacOS()) { mainWindow = null; + } else { + app.quit(); } }); @@ -279,6 +395,7 @@ app .whenReady() .then(() => { createWindow(); + 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. diff --git a/src/renderer/features/settings/components/settings-content.tsx b/src/renderer/features/settings/components/settings-content.tsx index f61ac783..1000012a 100644 --- a/src/renderer/features/settings/components/settings-content.tsx +++ b/src/renderer/features/settings/components/settings-content.tsx @@ -1,6 +1,7 @@ import { lazy } from 'react'; import { Tabs } from '/@/renderer/components'; import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store'; +import isElectron from 'is-electron'; import styled from 'styled-components'; const GeneralTab = lazy(() => @@ -44,7 +45,7 @@ export const SettingsContent = () => { General Playback - Window + {isElectron() && Window} @@ -52,9 +53,11 @@ export const SettingsContent = () => { - - - + {isElectron() && ( + + + + )} ); diff --git a/src/renderer/features/settings/components/window/window-settings.tsx b/src/renderer/features/settings/components/window/window-settings.tsx index a5ea7d8e..620eb9e1 100644 --- a/src/renderer/features/settings/components/window/window-settings.tsx +++ b/src/renderer/features/settings/components/window/window-settings.tsx @@ -5,7 +5,7 @@ import { SettingsSection, SettingOption, } from '/@/renderer/features/settings/components/settings-section'; -import { Select } from '/@/renderer/components'; +import { Select, Switch } from '/@/renderer/components'; const WINDOW_BAR_OPTIONS = [ { label: 'Web (hidden)', value: Platform.WEB }, @@ -13,6 +13,8 @@ const WINDOW_BAR_OPTIONS = [ { label: 'macOS', value: Platform.MACOS }, ]; +const localSettings = isElectron() ? window.electron.localSettings : null; + export const WindowSettings = () => { const settings = useWindowSettings(); const { setSettings } = useSettingsStoreActions(); @@ -39,6 +41,50 @@ export const WindowSettings = () => { isHidden: !isElectron(), title: 'Window bar style', }, + { + control: ( + { + if (!e) return; + localSettings?.set('window_minimize_to_tray', e.currentTarget.checked); + setSettings({ + window: { + ...settings, + minimizeToTray: e.currentTarget.checked, + }, + }); + }} + /> + ), + description: 'Minimize the application to the system tray', + isHidden: !isElectron(), + title: 'Minimize to tray', + }, + { + control: ( + { + if (!e) return; + localSettings?.set('window_exit_to_tray', e.currentTarget.checked); + setSettings({ + window: { + ...settings, + exitToTray: e.currentTarget.checked, + }, + }); + }} + /> + ), + description: 'Exit the application to the system tray', + isHidden: !isElectron(), + title: 'Exit to tray', + }, ]; return ; diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index cafd9a02..aa19a448 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -71,6 +71,8 @@ export interface SettingsState { songs: DataTableProps; }; window: { + exitToTray: boolean; + minimizeToTray: boolean; windowBarStyle: Platform; }; } @@ -243,6 +245,8 @@ export const useSettingsStore = create()( }, }, window: { + exitToTray: false, + minimizeToTray: false, windowBarStyle: Platform.WEB, }, })),