diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 20e066e4..15f470e1 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -696,6 +696,8 @@ "skipPlaylistPage_description": "when navigating to a playlist, go to the playlist song list page instead of the default page", "startMinimized": "start minimized", "startMinimized_description": "start the application in system tray", + "preventSleepOnPlayback": "prevent sleep on playback", + "preventSleepOnPlayback_description": "prevent the display from sleeping while music is playing", "theme": "theme", "theme_description": "sets the theme to use for the application", "themeDark": "theme (dark)", diff --git a/src/main/index.ts b/src/main/index.ts index c11247a5..61d124e3 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -9,6 +9,7 @@ import { nativeImage, nativeTheme, net, + powerSaveBlocker, protocol, Rectangle, screen, @@ -66,6 +67,7 @@ let mainWindow: BrowserWindow | null = null; let tray: null | Tray = null; let exitFromTray = false; let forceQuit = false; +let powerSaveBlockerId: null | number = null; if (process.env.NODE_ENV === 'production') { import('source-map-support').then((sourceMapSupport) => { @@ -616,6 +618,28 @@ ipcMain.on( }, ); +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 diff --git a/src/preload/ipc.ts b/src/preload/ipc.ts index 44001069..d4641d1a 100644 --- a/src/preload/ipc.ts +++ b/src/preload/ipc.ts @@ -8,7 +8,12 @@ const send = (channel: string, ...args: any[]) => { ipcRenderer.send(channel, ...args); }; +const invoke = (channel: string, ...args: any[]) => { + return ipcRenderer.invoke(channel, ...args); +}; + export const ipc = { + invoke, removeAllListeners, send, }; diff --git a/src/renderer/features/player/components/playerbar.tsx b/src/renderer/features/player/components/playerbar.tsx index 78074ce8..38a76c57 100644 --- a/src/renderer/features/player/components/playerbar.tsx +++ b/src/renderer/features/player/components/playerbar.tsx @@ -6,6 +6,7 @@ import { AudioPlayer } from '/@/renderer/components'; import { CenterControls } from '/@/renderer/features/player/components/center-controls'; import { LeftControls } from '/@/renderer/features/player/components/left-controls'; import { RightControls } from '/@/renderer/features/player/components/right-controls'; +import { usePowerSaveBlocker } from '/@/renderer/features/player/hooks/use-power-save-blocker'; import { PlayersRef } from '/@/renderer/features/player/ref/players-ref'; import { updateSong } from '/@/renderer/features/player/update-remote-song'; import { @@ -41,6 +42,8 @@ export const Playerbar = () => { const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore(); const setFullScreenPlayerStore = useSetFullScreenPlayerStore(); + usePowerSaveBlocker(); + const handleToggleFullScreenPlayer = (e?: KeyboardEvent | MouseEvent) => { e?.stopPropagation(); setFullScreenPlayerStore({ expanded: !isFullScreenPlayerExpanded }); diff --git a/src/renderer/features/player/hooks/use-power-save-blocker.ts b/src/renderer/features/player/hooks/use-power-save-blocker.ts new file mode 100644 index 00000000..26a71cfa --- /dev/null +++ b/src/renderer/features/player/hooks/use-power-save-blocker.ts @@ -0,0 +1,50 @@ +import isElectron from 'is-electron'; +import { useCallback, useEffect } from 'react'; + +import { useCurrentStatus } from '/@/renderer/store'; +import { useWindowSettings } from '/@/renderer/store'; +import { PlayerStatus } from '/@/shared/types/types'; + +const ipc = isElectron() ? window.api.ipc : null; + +export const usePowerSaveBlocker = () => { + const status = useCurrentStatus(); + const { preventSleepOnPlayback } = useWindowSettings(); + + const startPowerSaveBlocker = useCallback(async () => { + if (!ipc) return; + + try { + await ipc.invoke('power-save-blocker-start'); + } catch (error) { + console.error('Failed to start power save blocker:', error); + } + }, []); + + const stopPowerSaveBlocker = useCallback(async () => { + if (!ipc) return; + + try { + await ipc.invoke('power-save-blocker-stop'); + } catch (error) { + console.error('Failed to stop power save blocker:', error); + } + }, []); + + useEffect(() => { + if (!preventSleepOnPlayback) return; + + if (status === PlayerStatus.PLAYING) { + startPowerSaveBlocker(); + } else { + stopPowerSaveBlocker(); + } + }, [status, preventSleepOnPlayback, startPowerSaveBlocker, stopPowerSaveBlocker]); + + // Clean up on unmount + useEffect(() => { + return () => { + stopPowerSaveBlocker(); + }; + }, [stopPowerSaveBlocker]); +}; diff --git a/src/renderer/features/settings/components/window/window-settings.tsx b/src/renderer/features/settings/components/window/window-settings.tsx index 44fcf862..e3311352 100644 --- a/src/renderer/features/settings/components/window/window-settings.tsx +++ b/src/renderer/features/settings/components/window/window-settings.tsx @@ -203,6 +203,34 @@ export const WindowSettings = () => { isHidden: !isElectron() || !settings.tray, title: t('setting.startMinimized', { postProcess: 'sentenceCase' }), }, + { + control: ( + { + if (!e) return; + localSettings?.set( + 'window_prevent_sleep_on_playback', + e.currentTarget.checked, + ); + setSettings({ + window: { + ...settings, + preventSleepOnPlayback: e.currentTarget.checked, + }, + }); + }} + /> + ), + description: t('setting.preventSleepOnPlayback', { + context: 'description', + postProcess: 'sentenceCase', + }), + isHidden: !isElectron(), + title: t('setting.preventSleepOnPlayback', { postProcess: 'sentenceCase' }), + }, ]; return ; diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 337de100..b07baef0 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -320,6 +320,7 @@ export interface SettingsState { disableAutoUpdate: boolean; exitToTray: boolean; minimizeToTray: boolean; + preventSleepOnPlayback: boolean; startMinimized: boolean; tray: boolean; windowBarStyle: Platform; @@ -664,6 +665,7 @@ const initialState: SettingsState = { disableAutoUpdate: false, exitToTray: false, minimizeToTray: false, + preventSleepOnPlayback: false, startMinimized: false, tray: true, windowBarStyle: platformDefaultWindowBarStyle,