add setting to prevent sleep on playback (#1072)

This commit is contained in:
jeffvli 2025-09-06 00:56:06 -07:00
parent 40fb5ba916
commit b00305cc86
7 changed files with 114 additions and 0 deletions

View file

@ -696,6 +696,8 @@
"skipPlaylistPage_description": "when navigating to a playlist, go to the playlist song list page instead of the default page", "skipPlaylistPage_description": "when navigating to a playlist, go to the playlist song list page instead of the default page",
"startMinimized": "start minimized", "startMinimized": "start minimized",
"startMinimized_description": "start the application in system tray", "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": "theme",
"theme_description": "sets the theme to use for the application", "theme_description": "sets the theme to use for the application",
"themeDark": "theme (dark)", "themeDark": "theme (dark)",

View file

@ -9,6 +9,7 @@ import {
nativeImage, nativeImage,
nativeTheme, nativeTheme,
net, net,
powerSaveBlocker,
protocol, protocol,
Rectangle, Rectangle,
screen, screen,
@ -66,6 +67,7 @@ let mainWindow: BrowserWindow | null = null;
let tray: null | Tray = null; let tray: null | Tray = null;
let exitFromTray = false; let exitFromTray = false;
let forceQuit = false; let forceQuit = false;
let powerSaveBlockerId: null | number = null;
if (process.env.NODE_ENV === 'production') { if (process.env.NODE_ENV === 'production') {
import('source-map-support').then((sourceMapSupport) => { 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', () => { app.on('window-all-closed', () => {
globalShortcut.unregisterAll(); globalShortcut.unregisterAll();
// Respect the OSX convention of having the application in memory even // Respect the OSX convention of having the application in memory even

View file

@ -8,7 +8,12 @@ const send = (channel: string, ...args: any[]) => {
ipcRenderer.send(channel, ...args); ipcRenderer.send(channel, ...args);
}; };
const invoke = (channel: string, ...args: any[]) => {
return ipcRenderer.invoke(channel, ...args);
};
export const ipc = { export const ipc = {
invoke,
removeAllListeners, removeAllListeners,
send, send,
}; };

View file

@ -6,6 +6,7 @@ import { AudioPlayer } from '/@/renderer/components';
import { CenterControls } from '/@/renderer/features/player/components/center-controls'; import { CenterControls } from '/@/renderer/features/player/components/center-controls';
import { LeftControls } from '/@/renderer/features/player/components/left-controls'; import { LeftControls } from '/@/renderer/features/player/components/left-controls';
import { RightControls } from '/@/renderer/features/player/components/right-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 { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
import { updateSong } from '/@/renderer/features/player/update-remote-song'; import { updateSong } from '/@/renderer/features/player/update-remote-song';
import { import {
@ -41,6 +42,8 @@ export const Playerbar = () => {
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore(); const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
const setFullScreenPlayerStore = useSetFullScreenPlayerStore(); const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
usePowerSaveBlocker();
const handleToggleFullScreenPlayer = (e?: KeyboardEvent | MouseEvent<HTMLDivElement>) => { const handleToggleFullScreenPlayer = (e?: KeyboardEvent | MouseEvent<HTMLDivElement>) => {
e?.stopPropagation(); e?.stopPropagation();
setFullScreenPlayerStore({ expanded: !isFullScreenPlayerExpanded }); setFullScreenPlayerStore({ expanded: !isFullScreenPlayerExpanded });

View file

@ -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]);
};

View file

@ -203,6 +203,34 @@ export const WindowSettings = () => {
isHidden: !isElectron() || !settings.tray, isHidden: !isElectron() || !settings.tray,
title: t('setting.startMinimized', { postProcess: 'sentenceCase' }), title: t('setting.startMinimized', { postProcess: 'sentenceCase' }),
}, },
{
control: (
<Switch
aria-label="Toggle prevent sleep on playback"
defaultChecked={settings.preventSleepOnPlayback}
disabled={!isElectron()}
onChange={(e) => {
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 <SettingsSection options={windowOptions} />; return <SettingsSection options={windowOptions} />;

View file

@ -320,6 +320,7 @@ export interface SettingsState {
disableAutoUpdate: boolean; disableAutoUpdate: boolean;
exitToTray: boolean; exitToTray: boolean;
minimizeToTray: boolean; minimizeToTray: boolean;
preventSleepOnPlayback: boolean;
startMinimized: boolean; startMinimized: boolean;
tray: boolean; tray: boolean;
windowBarStyle: Platform; windowBarStyle: Platform;
@ -664,6 +665,7 @@ const initialState: SettingsState = {
disableAutoUpdate: false, disableAutoUpdate: false,
exitToTray: false, exitToTray: false,
minimizeToTray: false, minimizeToTray: false,
preventSleepOnPlayback: false,
startMinimized: false, startMinimized: false,
tray: true, tray: true,
windowBarStyle: platformDefaultWindowBarStyle, windowBarStyle: platformDefaultWindowBarStyle,