mirror of
https://github.com/jeffvli/feishin.git
synced 2024-11-20 06:27:09 +01:00
Add ability to save/restore queue (#111)
* add ability to save/restore play queue * Add restoreQueue action * Add optional pause param on setQueue --------- Co-authored-by: jeffvli <jeffvictorli@gmail.com>
This commit is contained in:
parent
c1c6ce33e4
commit
106fc90c4a
0
.husky/pre-commit
Normal file → Executable file
0
.husky/pre-commit
Normal file → Executable file
@ -52,7 +52,7 @@ ipcMain.on('player-seek-to', async (_event, time: number) => {
|
||||
});
|
||||
|
||||
// Sets the queue in position 0 and 1 to the given data. Used when manually starting a song or using the next/prev buttons
|
||||
ipcMain.on('player-set-queue', async (_event, data: PlayerData) => {
|
||||
ipcMain.on('player-set-queue', async (_event, data: PlayerData, pause?: boolean) => {
|
||||
if (!data.queue.current && !data.queue.next) {
|
||||
await getMpvInstance()?.clearPlaylist();
|
||||
await getMpvInstance()?.pause();
|
||||
@ -84,6 +84,10 @@ ipcMain.on('player-set-queue', async (_event, data: PlayerData) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pause) {
|
||||
await getMpvInstance()?.pause();
|
||||
}
|
||||
});
|
||||
|
||||
// Replaces the queue in position 1 to the given data
|
||||
|
@ -8,7 +8,9 @@
|
||||
* When running `npm run build` or `npm run build:main`, this file is compiled to
|
||||
* `./src/main.js` using webpack. This gives us some performance wins.
|
||||
*/
|
||||
import path from 'path';
|
||||
import { access, constants, readFile, writeFile } from 'fs';
|
||||
import path, { join } from 'path';
|
||||
import { deflate, inflate } from 'zlib';
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
@ -239,6 +241,36 @@ const createWindow = async () => {
|
||||
disableMediaKeys();
|
||||
});
|
||||
|
||||
ipcMain.on('player-restore-queue', () => {
|
||||
if (store.get('resume')) {
|
||||
const queueLocation = join(app.getPath('userData'), 'queue');
|
||||
|
||||
access(queueLocation, constants.F_OK, (accessError) => {
|
||||
if (accessError) {
|
||||
console.error('unable to access saved queue: ', accessError);
|
||||
return;
|
||||
}
|
||||
|
||||
readFile(queueLocation, (readError, buffer) => {
|
||||
if (readError) {
|
||||
console.error('failed to read saved queue: ', readError);
|
||||
return;
|
||||
}
|
||||
|
||||
inflate(buffer, (decompressError, data) => {
|
||||
if (decompressError) {
|
||||
console.error('failed to decompress queue: ', decompressError);
|
||||
return;
|
||||
}
|
||||
|
||||
const queue = JSON.parse(data.toString());
|
||||
getMainWindow()?.webContents.send('renderer-player-restore-queue', queue);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const globalMediaKeysEnabled = store.get('global_media_hotkeys') as boolean;
|
||||
|
||||
if (globalMediaKeysEnabled) {
|
||||
@ -263,6 +295,8 @@ const createWindow = async () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
|
||||
let saved = false;
|
||||
|
||||
mainWindow.on('close', (event) => {
|
||||
if (!exitFromTray && store.get('window_exit_to_tray')) {
|
||||
if (isMacOS() && !forceQuit) {
|
||||
@ -271,6 +305,43 @@ const createWindow = async () => {
|
||||
event.preventDefault();
|
||||
mainWindow?.hide();
|
||||
}
|
||||
|
||||
if (!saved && store.get('resume')) {
|
||||
event.preventDefault();
|
||||
saved = true;
|
||||
|
||||
getMainWindow()?.webContents.send('renderer-player-save-queue');
|
||||
|
||||
ipcMain.once('player-save-queue', async (_event, data: Record<string, any>) => {
|
||||
const queueLocation = join(app.getPath('userData'), 'queue');
|
||||
const serialized = JSON.stringify(data);
|
||||
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
deflate(serialized, { level: 1 }, (error, deflated) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
} else {
|
||||
writeFile(queueLocation, deflated, (writeError) => {
|
||||
if (writeError) {
|
||||
reject(writeError);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('error saving queue state: ', error);
|
||||
} finally {
|
||||
mainWindow?.close();
|
||||
if (forceQuit) {
|
||||
app.exit();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.on('minimize', (event: any) => {
|
||||
|
@ -42,6 +42,14 @@ const previous = () => {
|
||||
ipcRenderer.send('player-previous');
|
||||
};
|
||||
|
||||
const restoreQueue = () => {
|
||||
ipcRenderer.send('player-restore-queue');
|
||||
};
|
||||
|
||||
const saveQueue = (data: Record<string, any>) => {
|
||||
ipcRenderer.send('player-save-queue', data);
|
||||
};
|
||||
|
||||
const seek = (seconds: number) => {
|
||||
ipcRenderer.send('player-seek', seconds);
|
||||
};
|
||||
@ -50,8 +58,8 @@ const seekTo = (seconds: number) => {
|
||||
ipcRenderer.send('player-seek-to', seconds);
|
||||
};
|
||||
|
||||
const setQueue = (data: PlayerData) => {
|
||||
ipcRenderer.send('player-set-queue', data);
|
||||
const setQueue = (data: PlayerData, pause?: boolean) => {
|
||||
ipcRenderer.send('player-set-queue', data, pause);
|
||||
};
|
||||
|
||||
const setQueueNext = (data: PlayerData) => {
|
||||
@ -134,6 +142,14 @@ const rendererQuit = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-player-quit', cb);
|
||||
};
|
||||
|
||||
const rendererSaveQueue = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-player-save-queue', cb);
|
||||
};
|
||||
|
||||
const rendererRestoreQueue = (cb: (event: IpcRendererEvent) => void) => {
|
||||
ipcRenderer.on('renderer-player-restore-queue', cb);
|
||||
};
|
||||
|
||||
const rendererError = (cb: (event: IpcRendererEvent, data: string) => void) => {
|
||||
ipcRenderer.on('renderer-player-error', cb);
|
||||
};
|
||||
@ -149,6 +165,8 @@ export const mpvPlayer = {
|
||||
previous,
|
||||
quit,
|
||||
restart,
|
||||
restoreQueue,
|
||||
saveQueue,
|
||||
seek,
|
||||
seekTo,
|
||||
setProperties,
|
||||
@ -168,6 +186,8 @@ export const mpvPlayerListener = {
|
||||
rendererPlayPause,
|
||||
rendererPrevious,
|
||||
rendererQuit,
|
||||
rendererRestoreQueue,
|
||||
rendererSaveQueue,
|
||||
rendererSkipBackward,
|
||||
rendererSkipForward,
|
||||
rendererStop,
|
||||
|
@ -17,14 +17,15 @@ import { PlayQueueHandlerContext } from '/@/renderer/features/player';
|
||||
import { AddToPlaylistContextModal } from '/@/renderer/features/playlists';
|
||||
import isElectron from 'is-electron';
|
||||
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
||||
import { usePlayerStore } from '/@/renderer/store';
|
||||
import { PlaybackType } from '/@/renderer/types';
|
||||
import { PlayerState, usePlayerStore, useQueueControls } from '/@/renderer/store';
|
||||
import { PlaybackType, PlayerStatus } from '/@/renderer/types';
|
||||
|
||||
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
|
||||
|
||||
initSimpleImg({ threshold: 0.05 }, true);
|
||||
|
||||
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
||||
const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null;
|
||||
const ipc = isElectron() ? window.electron.ipc : null;
|
||||
|
||||
export const App = () => {
|
||||
@ -33,6 +34,7 @@ export const App = () => {
|
||||
const { type: playbackType } = usePlaybackSettings();
|
||||
const { bindings } = useHotkeySettings();
|
||||
const handlePlayQueueAdd = useHandlePlayQueueAdd();
|
||||
const { restoreQueue } = useQueueControls();
|
||||
|
||||
useEffect(() => {
|
||||
const root = document.documentElement;
|
||||
@ -65,6 +67,36 @@ export const App = () => {
|
||||
}
|
||||
}, [bindings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron()) {
|
||||
mpvPlayer.restoreQueue();
|
||||
|
||||
mpvPlayerListener.rendererSaveQueue(() => {
|
||||
const { current, queue } = usePlayerStore.getState();
|
||||
const stateToSave: Partial<Pick<PlayerState, 'current' | 'queue'>> = {
|
||||
current: {
|
||||
...current,
|
||||
status: PlayerStatus.PAUSED,
|
||||
},
|
||||
queue,
|
||||
};
|
||||
mpvPlayer.saveQueue(stateToSave);
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererRestoreQueue((_event: any, data: Partial<PlayerState>) => {
|
||||
const playerData = restoreQueue(data);
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
mpvPlayer.setQueue(playerData, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
ipc?.removeAllListeners('renderer-player-restore-queue');
|
||||
ipc?.removeAllListeners('renderer-player-save-queue');
|
||||
};
|
||||
}, [playbackType, restoreQueue]);
|
||||
|
||||
return (
|
||||
<MantineProvider
|
||||
withGlobalStyles
|
||||
|
@ -1,3 +1,5 @@
|
||||
// import { write, writeFile } from 'fs';
|
||||
// import { deflate } from 'zlib';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import isElectron from 'is-electron';
|
||||
import { PlaybackType, PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/renderer/types';
|
||||
|
@ -1,3 +1,4 @@
|
||||
import isElectron from 'is-electron';
|
||||
import { Group } from '@mantine/core';
|
||||
import { Select, Tooltip, NumberInput, Switch, Slider } from '/@/renderer/components';
|
||||
import { SettingsSection } from '/@/renderer/features/settings/components/settings-section';
|
||||
@ -8,6 +9,8 @@ import {
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { Play } from '/@/renderer/types';
|
||||
|
||||
const localSettings = isElectron() ? window.electron.localSettings : null;
|
||||
|
||||
const SIDE_QUEUE_OPTIONS = [
|
||||
{ label: 'Fixed', value: 'sideQueue' },
|
||||
{ label: 'Floating', value: 'sideDrawerQueue' },
|
||||
@ -170,6 +173,25 @@ export const ControlSettings = () => {
|
||||
isHidden: false,
|
||||
title: 'Volume wheel step',
|
||||
},
|
||||
{
|
||||
control: (
|
||||
<Switch
|
||||
defaultChecked={settings.resume}
|
||||
onChange={(e) => {
|
||||
localSettings?.set('resume', e.target.checked);
|
||||
setSettings({
|
||||
general: {
|
||||
...settings,
|
||||
resume: e.currentTarget.checked,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
description: 'When exiting, save the current play queue and restore it when reopening',
|
||||
isHidden: !isElectron(),
|
||||
title: 'Save play queue',
|
||||
},
|
||||
];
|
||||
|
||||
return <SettingsSection options={controlOptions} />;
|
||||
|
6
src/renderer/preload.d.ts
vendored
6
src/renderer/preload.d.ts
vendored
@ -1,5 +1,5 @@
|
||||
import { IpcRendererEvent } from 'electron';
|
||||
import { PlayerData } from './store';
|
||||
import { PlayerData, PlayerState } from './store';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@ -17,6 +17,8 @@ declare global {
|
||||
PLAYER_PAUSE(): void;
|
||||
PLAYER_PLAY(): void;
|
||||
PLAYER_PREVIOUS(): void;
|
||||
PLAYER_RESTORE_DATA(): void;
|
||||
PLAYER_SAVE_QUEUE(data: PlayerState): void;
|
||||
PLAYER_SEEK(seconds: number): void;
|
||||
PLAYER_SEEK_TO(seconds: number): void;
|
||||
PLAYER_SET_QUEUE(data: PlayerData): void;
|
||||
@ -30,6 +32,8 @@ declare global {
|
||||
RENDERER_PLAYER_PLAY(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||
RENDERER_PLAYER_PLAY_PAUSE(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||
RENDERER_PLAYER_PREVIOUS(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||
RENDERER_PLAYER_RESTORE_QUEUE(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||
RENDERER_PLAYER_SAVE_QUEUE(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||
RENDERER_PLAYER_STOP(cb: (event: IpcRendererEvent, data: any) => void): void;
|
||||
SETTINGS_GET(data: { property: string }): any;
|
||||
SETTINGS_SET(data: { property: string; value: any }): void;
|
||||
|
@ -74,6 +74,7 @@ export interface PlayerSlice extends PlayerState {
|
||||
previous: () => PlayerData;
|
||||
removeFromQueue: (uniqueIds: string[]) => PlayerData;
|
||||
reorderQueue: (rowUniqueIds: string[], afterUniqueId?: string) => PlayerData;
|
||||
restoreQueue: (data: Partial<PlayerState>) => PlayerData;
|
||||
setCurrentIndex: (index: number) => PlayerData;
|
||||
setCurrentTime: (time: number) => void;
|
||||
setCurrentTrack: (uniqueId: string) => PlayerData;
|
||||
@ -615,6 +616,20 @@ export const usePlayerStore = create<PlayerSlice>()(
|
||||
|
||||
return get().actions.getPlayerData();
|
||||
},
|
||||
restoreQueue: (data) => {
|
||||
set((state) => {
|
||||
state.current = {
|
||||
...state.current,
|
||||
...data.current,
|
||||
};
|
||||
state.queue = {
|
||||
...state.queue,
|
||||
...data.queue,
|
||||
};
|
||||
});
|
||||
|
||||
return get().actions.getPlayerData();
|
||||
},
|
||||
setCurrentIndex: (index) => {
|
||||
if (get().shuffle === PlayerShuffle.TRACK) {
|
||||
const foundSong = get().queue.default.find(
|
||||
@ -874,6 +889,7 @@ export const useQueueControls = () =>
|
||||
moveToTopOfQueue: state.actions.moveToTopOfQueue,
|
||||
removeFromQueue: state.actions.removeFromQueue,
|
||||
reorderQueue: state.actions.reorderQueue,
|
||||
restoreQueue: state.actions.restoreQueue,
|
||||
setCurrentIndex: state.actions.setCurrentIndex,
|
||||
setCurrentTrack: state.actions.setCurrentTrack,
|
||||
setShuffledIndex: state.actions.setShuffledIndex,
|
||||
|
@ -69,6 +69,7 @@ export interface SettingsState {
|
||||
followSystemTheme: boolean;
|
||||
fontContent: string;
|
||||
playButtonBehavior: Play;
|
||||
resume: boolean;
|
||||
showQueueDrawerButton: boolean;
|
||||
sideQueueType: SideQueueType;
|
||||
skipButtons: {
|
||||
@ -128,6 +129,7 @@ const initialState: SettingsState = {
|
||||
followSystemTheme: false,
|
||||
fontContent: 'Poppins',
|
||||
playButtonBehavior: Play.NOW,
|
||||
resume: false,
|
||||
showQueueDrawerButton: false,
|
||||
sideQueueType: 'sideQueue',
|
||||
skipButtons: {
|
||||
|
Loading…
Reference in New Issue
Block a user