mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
re-implement mpv with new player
This commit is contained in:
@@ -59,7 +59,6 @@ export const PosterCard = ({
|
||||
<Image className={styles.image} src={data?.imageUrl} />
|
||||
<GridCardControls
|
||||
handleFavorite={controls.handleFavorite}
|
||||
handlePlayQueueAdd={controls.handlePlayQueueAdd}
|
||||
isHovered={isHovered}
|
||||
itemData={data}
|
||||
itemType={controls.itemType}
|
||||
|
||||
@@ -9,16 +9,16 @@ import {
|
||||
PLAYLIST_CONTEXT_MENU_ITEMS,
|
||||
} from '/@/renderer/features/context-menu/context-menu-items';
|
||||
import { useHandleGridContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||
import { usePlayerContext } from '/@/renderer/features/player/context/player-context';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { Play, PlayQueueAddOptions } from '/@/shared/types/types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
export const GridCardControls = ({
|
||||
handleFavorite,
|
||||
handlePlayQueueAdd,
|
||||
isHovered,
|
||||
itemData,
|
||||
itemType,
|
||||
@@ -30,7 +30,6 @@ export const GridCardControls = ({
|
||||
itemType: LibraryItem;
|
||||
serverId: string;
|
||||
}) => void;
|
||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||
isHovered?: boolean;
|
||||
itemData: any;
|
||||
itemType: LibraryItem;
|
||||
@@ -39,17 +38,18 @@ export const GridCardControls = ({
|
||||
const [isFavorite, setIsFavorite] = useState(itemData?.userFavorite);
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
|
||||
const player = usePlayerContext();
|
||||
|
||||
const handlePlay = async (e: MouseEvent<HTMLButtonElement>, playType?: Play) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
handlePlayQueueAdd?.({
|
||||
byItemType: {
|
||||
id: [itemData.id],
|
||||
type: itemType,
|
||||
},
|
||||
playType: playType || playButtonBehavior,
|
||||
});
|
||||
player.addToQueueByFetch(
|
||||
itemData._serverId,
|
||||
[itemData.id],
|
||||
itemType,
|
||||
playType || playButtonBehavior,
|
||||
);
|
||||
};
|
||||
|
||||
const handleFavorites = async (e: MouseEvent<HTMLButtonElement>, serverId: string) => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
|
||||
import { RowClassRules, RowNode } from '@ag-grid-community/core';
|
||||
import { MutableRefObject, useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/listener/use-player-events';
|
||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||
import { useAppFocus } from '/@/renderer/hooks';
|
||||
import { usePlayerSong } from '/@/renderer/store';
|
||||
import { Song } from '/@/shared/types/domain-types';
|
||||
@@ -75,8 +75,8 @@ export const useCurrentSongRowStyles = ({ tableRef }: UseCurrentSongRowStylesPro
|
||||
api.redrawRows({ rowNodes });
|
||||
}
|
||||
},
|
||||
onPlayerStatus: (properties) => {
|
||||
const song = properties.song;
|
||||
onPlayerStatus: () => {
|
||||
const song = currentSong;
|
||||
|
||||
if (tableRef?.current) {
|
||||
const { api, columnApi } = tableRef?.current || {};
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
import type {
|
||||
CellDoubleClickedEvent,
|
||||
RowClassRules,
|
||||
RowDragEvent,
|
||||
RowNode,
|
||||
} from '@ag-grid-community/core';
|
||||
import type { RowClassRules, RowDragEvent, RowNode } from '@ag-grid-community/core';
|
||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
import type { Ref } from 'react';
|
||||
|
||||
import { useMergedRef } from '@mantine/hooks';
|
||||
import '@ag-grid-community/styles/ag-theme-alpine.css';
|
||||
import isElectron from 'is-electron';
|
||||
import { useMergedRef } from '@mantine/hooks';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||
import { ErrorBoundary } from 'react-error-boundary';
|
||||
@@ -19,29 +13,22 @@ import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-tabl
|
||||
import { ErrorFallback } from '/@/renderer/features/action-required/components/error-fallback';
|
||||
import { QUEUE_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
||||
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||
import { useAppFocus } from '/@/renderer/hooks';
|
||||
import {
|
||||
useAppStoreActions,
|
||||
usePlayerQueue,
|
||||
usePlayerSong,
|
||||
usePlayerStatus,
|
||||
usePlayerVolume,
|
||||
} from '/@/renderer/store';
|
||||
import {
|
||||
PersistedTableColumn,
|
||||
usePlaybackType,
|
||||
useSettingsStore,
|
||||
useSettingsStoreActions,
|
||||
useTableSettings,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { searchSongs } from '/@/renderer/utils/search-songs';
|
||||
import { setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||
import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
|
||||
import { PlayerType, TableType } from '/@/shared/types/types';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
import { TableType } from '/@/shared/types/types';
|
||||
|
||||
type QueueProps = {
|
||||
searchTerm?: string;
|
||||
@@ -52,17 +39,12 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
const mergedRef = useMergedRef(ref, tableRef);
|
||||
const queue = usePlayerQueue();
|
||||
// const { reorderQueue, setCurrentTrack } = useQueueControls();
|
||||
const currentSong = usePlayerSong();
|
||||
// const previousSong = usePreviousSong();
|
||||
const status = usePlayerStatus();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const { setAppStore } = useAppStoreActions();
|
||||
const tableConfig = useTableSettings(type);
|
||||
const [gridApi, setGridApi] = useState<AgGridReactType | undefined>();
|
||||
const playbackType = usePlaybackType();
|
||||
// const { play } = usePlayerControls();
|
||||
const volume = usePlayerVolume();
|
||||
const isFocused = useAppFocus();
|
||||
const isFocusedRef = useRef<boolean>(isFocused);
|
||||
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
import type { RefObject } from 'react';
|
||||
|
||||
import isElectron from 'is-electron';
|
||||
import { useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||
|
||||
import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
export interface MpvPlayerEngineHandle extends AudioPlayer {}
|
||||
|
||||
interface MpvPlayerEngineProps {
|
||||
currentSrc: string | undefined;
|
||||
isMuted: boolean;
|
||||
isTransitioning: boolean;
|
||||
nextSrc: string | undefined;
|
||||
onEnded: () => void;
|
||||
onProgress: (e: PlayerOnProgressProps) => void;
|
||||
playerRef: RefObject<MpvPlayerEngineHandle>;
|
||||
playerStatus: PlayerStatus;
|
||||
speed?: number;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
const mpvPlayerListener = isElectron() ? window.api.mpvPlayerListener : null;
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
|
||||
const PROGRESS_UPDATE_INTERVAL = 250;
|
||||
const TRANSITION_PROGRESS_INTERVAL = 10;
|
||||
|
||||
export const MpvPlayerEngine = (props: MpvPlayerEngineProps) => {
|
||||
const {
|
||||
currentSrc,
|
||||
isMuted,
|
||||
isTransitioning,
|
||||
nextSrc,
|
||||
onEnded,
|
||||
onProgress,
|
||||
playerRef,
|
||||
playerStatus,
|
||||
speed,
|
||||
volume,
|
||||
} = props;
|
||||
|
||||
const [internalVolume, setInternalVolume] = useState(volume / 100 || 0);
|
||||
const [duration] = useState(0);
|
||||
const [previousCurrentSrc, setPreviousCurrentSrc] = useState<string | undefined>(currentSrc);
|
||||
|
||||
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// Update volume
|
||||
useEffect(() => {
|
||||
if (!mpvPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
const vol = volume / 100 || 0;
|
||||
setInternalVolume(vol);
|
||||
mpvPlayer.volume(volume);
|
||||
}, [volume]);
|
||||
|
||||
// Update mute status
|
||||
useEffect(() => {
|
||||
if (!mpvPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
mpvPlayer.mute(isMuted);
|
||||
}, [isMuted]);
|
||||
|
||||
// Update speed/playback rate
|
||||
useEffect(() => {
|
||||
if (!mpvPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!speed) {
|
||||
return;
|
||||
}
|
||||
|
||||
mpvPlayer.setProperties({ speed });
|
||||
}, [speed]);
|
||||
|
||||
// Handle current song changes - update queue position 0
|
||||
// When currentSrc changes, we need to update the queue
|
||||
useEffect(() => {
|
||||
if (!mpvPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If currentSrc changed, update the queue
|
||||
if (currentSrc !== previousCurrentSrc) {
|
||||
if (currentSrc) {
|
||||
// Set current song at position 0 and next song at position 1
|
||||
mpvPlayer.setQueue(currentSrc, nextSrc, playerStatus !== PlayerStatus.PLAYING);
|
||||
setPreviousCurrentSrc(currentSrc);
|
||||
} else {
|
||||
// Clear queue if no current song
|
||||
mpvPlayer.setQueue(undefined, undefined, true);
|
||||
setPreviousCurrentSrc(undefined);
|
||||
}
|
||||
} else {
|
||||
// If currentSrc hasn't changed but nextSrc has, update position 1
|
||||
// This happens when the next song changes but current song stays the same
|
||||
if (currentSrc) {
|
||||
mpvPlayer.setQueueNext(nextSrc);
|
||||
}
|
||||
}
|
||||
}, [currentSrc, previousCurrentSrc, nextSrc, playerStatus]);
|
||||
|
||||
// Handle play/pause status
|
||||
useEffect(() => {
|
||||
if (!mpvPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (playerStatus === PlayerStatus.PLAYING) {
|
||||
mpvPlayer.play();
|
||||
} else if (playerStatus === PlayerStatus.PAUSED) {
|
||||
mpvPlayer.pause();
|
||||
}
|
||||
}, [playerStatus]);
|
||||
|
||||
// Set up progress tracking
|
||||
useEffect(() => {
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
}
|
||||
|
||||
const updateProgress = async () => {
|
||||
if (!mpvPlayer) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const time = await mpvPlayer.getCurrentTime();
|
||||
if (time !== undefined) {
|
||||
onProgress({
|
||||
played: time / (duration || time + 10),
|
||||
playedSeconds: time,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Handle error silently
|
||||
}
|
||||
};
|
||||
|
||||
if (currentSrc) {
|
||||
const interval = isTransitioning
|
||||
? TRANSITION_PROGRESS_INTERVAL
|
||||
: PROGRESS_UPDATE_INTERVAL;
|
||||
progressIntervalRef.current = setInterval(updateProgress, interval);
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (progressIntervalRef.current) {
|
||||
clearInterval(progressIntervalRef.current);
|
||||
}
|
||||
};
|
||||
}, [currentSrc, isTransitioning, duration, onProgress]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mpvPlayerListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
const handleOnEnded = () => {
|
||||
onEnded();
|
||||
};
|
||||
|
||||
mpvPlayerListener.rendererAutoNext(handleOnEnded);
|
||||
|
||||
return () => {
|
||||
ipc?.removeAllListeners('renderer-player-auto-next');
|
||||
};
|
||||
}, [nextSrc, onEnded]);
|
||||
|
||||
useImperativeHandle<MpvPlayerEngineHandle, MpvPlayerEngineHandle>(playerRef, () => ({
|
||||
decreaseVolume(by: number) {
|
||||
const newVol = Math.max(0, internalVolume - by / 100);
|
||||
setInternalVolume(newVol);
|
||||
if (mpvPlayer) {
|
||||
mpvPlayer.volume(newVol * 100);
|
||||
}
|
||||
},
|
||||
increaseVolume(by: number) {
|
||||
const newVol = Math.min(1, internalVolume + by / 100);
|
||||
setInternalVolume(newVol);
|
||||
if (mpvPlayer) {
|
||||
mpvPlayer.volume(newVol * 100);
|
||||
}
|
||||
},
|
||||
pause() {
|
||||
if (mpvPlayer) {
|
||||
mpvPlayer.pause();
|
||||
}
|
||||
},
|
||||
play() {
|
||||
if (mpvPlayer) {
|
||||
mpvPlayer.play();
|
||||
}
|
||||
},
|
||||
seekTo(seekTo: number) {
|
||||
if (mpvPlayer) {
|
||||
mpvPlayer.seekTo(seekTo);
|
||||
}
|
||||
},
|
||||
setVolume(vol: number) {
|
||||
const volDecimal = vol / 100 || 0;
|
||||
setInternalVolume(volDecimal);
|
||||
if (mpvPlayer) {
|
||||
mpvPlayer.volume(vol);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
return <div id="mpv-player-engine" style={{ display: 'none' }} />;
|
||||
};
|
||||
|
||||
MpvPlayerEngine.displayName = 'MpvPlayerEngine';
|
||||
@@ -3,17 +3,10 @@ import type { RefObject } from 'react';
|
||||
import { useImperativeHandle, useRef, useState } from 'react';
|
||||
import ReactPlayer from 'react-player';
|
||||
|
||||
import { AudioPlayer } from '/@/renderer/features/player/audio-player/types';
|
||||
import { AudioPlayer, PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
|
||||
import { convertToLogVolume } from '/@/renderer/features/player/audio-player/utils/player-utils';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
export interface OnProgressProps {
|
||||
loaded: number;
|
||||
loadedSeconds: number;
|
||||
played: number;
|
||||
playedSeconds: number;
|
||||
}
|
||||
|
||||
export interface WebPlayerEngineHandle extends AudioPlayer {
|
||||
player1(): {
|
||||
ref: null | ReactPlayer;
|
||||
@@ -30,8 +23,8 @@ interface WebPlayerEngineProps {
|
||||
isTransitioning: boolean;
|
||||
onEndedPlayer1: () => void;
|
||||
onEndedPlayer2: () => void;
|
||||
onProgressPlayer1: (e: OnProgressProps) => void;
|
||||
onProgressPlayer2: (e: OnProgressProps) => void;
|
||||
onProgressPlayer1: (e: PlayerOnProgressProps) => void;
|
||||
onProgressPlayer2: (e: PlayerOnProgressProps) => void;
|
||||
playerNum: number;
|
||||
playerRef: RefObject<WebPlayerEngineHandle>;
|
||||
playerStatus: PlayerStatus;
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import { t } from 'i18next';
|
||||
import isElectron from 'is-electron';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { usePlayerActions } from '/@/renderer/store';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
const mpvPlayerListener = isElectron() ? window.api.mpvPlayerListener : null;
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
|
||||
export const useMainPlayerListener = () => {
|
||||
const {
|
||||
decreaseVolume,
|
||||
increaseVolume,
|
||||
mediaAutoNext,
|
||||
mediaNext,
|
||||
mediaPause,
|
||||
mediaPlay,
|
||||
mediaPrevious,
|
||||
mediaSkipBackward,
|
||||
mediaSkipForward,
|
||||
mediaStop,
|
||||
mediaToggleMute,
|
||||
mediaTogglePlayPause,
|
||||
toggleRepeat,
|
||||
toggleShuffle,
|
||||
} = usePlayerActions();
|
||||
|
||||
const handleMpvError = useCallback(
|
||||
(message: string) => {
|
||||
toast.error({
|
||||
id: 'mpv-error',
|
||||
message,
|
||||
title: t('error.playbackError', { postProcess: 'sentenceCase' }) as string,
|
||||
});
|
||||
mediaPause();
|
||||
mpvPlayer!.pause();
|
||||
},
|
||||
[mediaPause],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!mpvPlayerListener) {
|
||||
return;
|
||||
}
|
||||
|
||||
mpvPlayerListener.rendererPlayPause(() => {
|
||||
mediaTogglePlayPause();
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererNext(() => {
|
||||
mediaNext();
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererPrevious(() => {
|
||||
mediaPrevious();
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererPlayPause(() => {
|
||||
mediaTogglePlayPause();
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererPlay(() => {
|
||||
mediaPlay();
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererPause(() => {
|
||||
mediaPause();
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererStop(() => {
|
||||
mediaStop();
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererSkipForward(() => {
|
||||
mediaSkipForward();
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererSkipBackward(() => {
|
||||
mediaSkipBackward();
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererAutoNext(() => {
|
||||
mediaAutoNext();
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererToggleShuffle(() => {
|
||||
toggleShuffle();
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererToggleRepeat(() => {
|
||||
toggleRepeat();
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererVolumeMute(() => {
|
||||
mediaToggleMute();
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererVolumeUp(() => {
|
||||
increaseVolume(1);
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererVolumeDown(() => {
|
||||
decreaseVolume(1);
|
||||
});
|
||||
|
||||
mpvPlayerListener.rendererError((_event: any, message: string) => {
|
||||
handleMpvError(message);
|
||||
});
|
||||
|
||||
return () => {
|
||||
ipc?.removeAllListeners('renderer-player-play-pause');
|
||||
ipc?.removeAllListeners('renderer-player-next');
|
||||
ipc?.removeAllListeners('renderer-player-previous');
|
||||
ipc?.removeAllListeners('renderer-player-play-pause');
|
||||
ipc?.removeAllListeners('renderer-player-play');
|
||||
ipc?.removeAllListeners('renderer-player-pause');
|
||||
ipc?.removeAllListeners('renderer-player-stop');
|
||||
ipc?.removeAllListeners('renderer-player-skip-forward');
|
||||
ipc?.removeAllListeners('renderer-player-skip-backward');
|
||||
ipc?.removeAllListeners('renderer-player-auto-next');
|
||||
ipc?.removeAllListeners('renderer-player-toggle-shuffle');
|
||||
ipc?.removeAllListeners('renderer-player-toggle-repeat');
|
||||
ipc?.removeAllListeners('renderer-player-volume-mute');
|
||||
ipc?.removeAllListeners('renderer-player-volume-up');
|
||||
ipc?.removeAllListeners('renderer-player-volume-down');
|
||||
ipc?.removeAllListeners('renderer-player-error');
|
||||
};
|
||||
}, [
|
||||
decreaseVolume,
|
||||
handleMpvError,
|
||||
increaseVolume,
|
||||
mediaAutoNext,
|
||||
mediaNext,
|
||||
mediaPause,
|
||||
mediaPlay,
|
||||
mediaPrevious,
|
||||
mediaSkipForward,
|
||||
mediaSkipBackward,
|
||||
mediaStop,
|
||||
mediaToggleMute,
|
||||
mediaTogglePlayPause,
|
||||
toggleRepeat,
|
||||
toggleShuffle,
|
||||
]);
|
||||
};
|
||||
+17
-5
@@ -1,5 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
QueueData,
|
||||
subscribeCurrentTrack,
|
||||
subscribePlayerMute,
|
||||
subscribePlayerProgress,
|
||||
@@ -9,14 +10,14 @@ import {
|
||||
subscribePlayerStatus,
|
||||
subscribePlayerVolume,
|
||||
} from '/@/renderer/store';
|
||||
import { QueueSong } from '/@/shared/types/domain-types';
|
||||
import { QueueData, QueueSong } from '/@/shared/types/domain-types';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
export interface PlayerEvents {
|
||||
interface PlayerEvents {
|
||||
cleanup: () => void;
|
||||
}
|
||||
|
||||
export interface PlayerEventsCallbacks {
|
||||
interface PlayerEventsCallbacks {
|
||||
onCurrentSongChange?: (
|
||||
properties: { index: number; song: QueueSong | undefined },
|
||||
prev: { index: number; song: QueueSong | undefined },
|
||||
@@ -34,7 +35,18 @@ export interface PlayerEventsCallbacks {
|
||||
onPlayerVolume?: (properties: { volume: number }, prev: { volume: number }) => void;
|
||||
}
|
||||
|
||||
export function createPlayerEvents(callbacks: PlayerEventsCallbacks): PlayerEvents {
|
||||
export function usePlayerEvents(callbacks: PlayerEventsCallbacks, deps: React.DependencyList) {
|
||||
useEffect(() => {
|
||||
const engine = createPlayerEvents(callbacks);
|
||||
|
||||
return () => {
|
||||
engine.cleanup();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...deps]);
|
||||
}
|
||||
|
||||
function createPlayerEvents(callbacks: PlayerEventsCallbacks): PlayerEvents {
|
||||
const unsubscribers: (() => void)[] = [];
|
||||
|
||||
// Subscribe to current track changes
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
createPlayerEvents,
|
||||
PlayerEventsCallbacks,
|
||||
} from '/@/renderer/features/player/audio-player/listener/player-events';
|
||||
|
||||
export function usePlayerEvents(callbacks: PlayerEventsCallbacks, deps: React.DependencyList) {
|
||||
useEffect(() => {
|
||||
const engine = createPlayerEvents(callbacks);
|
||||
|
||||
return () => {
|
||||
engine.cleanup();
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [...deps]);
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
import { MpvPlayerEngine, MpvPlayerEngineHandle } from './engine/mpv-player-engine';
|
||||
|
||||
import { useMainPlayerListener } from '/@/renderer/features/player/audio-player/hooks/use-main-player-listener';
|
||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||
import { PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
|
||||
import {
|
||||
usePlayerActions,
|
||||
usePlayerData,
|
||||
usePlayerMuted,
|
||||
usePlayerProperties,
|
||||
usePlayerVolume,
|
||||
} from '/@/renderer/store';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
const PLAY_PAUSE_FADE_DURATION = 300;
|
||||
const PLAY_PAUSE_FADE_INTERVAL = 10;
|
||||
|
||||
export function MpvPlayer() {
|
||||
const playerRef = useRef<MpvPlayerEngineHandle>(null);
|
||||
const { currentSong, nextSong, status } = usePlayerData();
|
||||
const { mediaAutoNext, setTimestamp } = usePlayerActions();
|
||||
const { speed } = usePlayerProperties();
|
||||
const isMuted = usePlayerMuted();
|
||||
const volume = usePlayerVolume();
|
||||
|
||||
const [localPlayerStatus, setLocalPlayerStatus] = useState<PlayerStatus>(status);
|
||||
const [isTransitioning, setIsTransitioning] = useState(false);
|
||||
|
||||
const fadeAndSetStatus = useCallback(
|
||||
async (startVolume: number, endVolume: number, duration: number, status: PlayerStatus) => {
|
||||
if (isTransitioning) {
|
||||
return setLocalPlayerStatus(status);
|
||||
}
|
||||
|
||||
const steps = duration / PLAY_PAUSE_FADE_INTERVAL;
|
||||
const volumeStep = (endVolume - startVolume) / steps;
|
||||
let currentStep = 0;
|
||||
|
||||
const promise = new Promise((resolve) => {
|
||||
const interval = setInterval(() => {
|
||||
currentStep++;
|
||||
const newVolume = startVolume + volumeStep * currentStep;
|
||||
|
||||
playerRef.current?.setVolume(newVolume);
|
||||
|
||||
if (currentStep >= steps) {
|
||||
clearInterval(interval);
|
||||
setIsTransitioning(false);
|
||||
resolve(true);
|
||||
}
|
||||
}, PLAY_PAUSE_FADE_INTERVAL);
|
||||
});
|
||||
|
||||
if (status === PlayerStatus.PAUSED) {
|
||||
await promise;
|
||||
setLocalPlayerStatus(status);
|
||||
} else if (status === PlayerStatus.PLAYING) {
|
||||
setLocalPlayerStatus(status);
|
||||
await promise;
|
||||
}
|
||||
},
|
||||
[isTransitioning],
|
||||
);
|
||||
|
||||
const onProgress = useCallback(
|
||||
(e: PlayerOnProgressProps) => {
|
||||
setTimestamp(Number(e.playedSeconds.toFixed(0)));
|
||||
},
|
||||
[setTimestamp],
|
||||
);
|
||||
|
||||
const handleOnEnded = useCallback(() => {
|
||||
// When mpv auto-advances to the next song (position 1 becomes position 0),
|
||||
// we need to update the player store first, then update the mpv queue with the new next song
|
||||
// This follows the same pattern as the old useCenterControls implementation
|
||||
const playerData = mediaAutoNext();
|
||||
|
||||
// Update the mpv queue with the new next song
|
||||
// The engine will handle the queue update through the useEffect when nextSong changes
|
||||
playerRef.current?.setVolume(volume);
|
||||
setIsTransitioning(false);
|
||||
|
||||
return playerData;
|
||||
}, [mediaAutoNext, volume, setIsTransitioning]);
|
||||
|
||||
usePlayerEvents(
|
||||
{
|
||||
onPlayerSeekToTimestamp: (properties) => {
|
||||
const timestamp = properties.timestamp;
|
||||
playerRef.current?.seekTo(timestamp);
|
||||
},
|
||||
onPlayerStatus: async (properties) => {
|
||||
const status = properties.status;
|
||||
if (status === PlayerStatus.PAUSED) {
|
||||
fadeAndSetStatus(volume, 0, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PAUSED);
|
||||
} else if (status === PlayerStatus.PLAYING) {
|
||||
fadeAndSetStatus(0, volume, PLAY_PAUSE_FADE_DURATION, PlayerStatus.PLAYING);
|
||||
}
|
||||
},
|
||||
onPlayerVolume: (properties) => {
|
||||
const volume = properties.volume;
|
||||
playerRef.current?.setVolume(volume);
|
||||
},
|
||||
},
|
||||
[volume, isTransitioning, fadeAndSetStatus],
|
||||
);
|
||||
|
||||
useMainPlayerListener();
|
||||
|
||||
return (
|
||||
<MpvPlayerEngine
|
||||
currentSrc={currentSong?.streamUrl}
|
||||
isMuted={isMuted}
|
||||
isTransitioning={isTransitioning}
|
||||
nextSrc={nextSong?.streamUrl}
|
||||
onEnded={handleOnEnded}
|
||||
onProgress={onProgress}
|
||||
playerRef={playerRef}
|
||||
playerStatus={localPlayerStatus}
|
||||
speed={speed}
|
||||
volume={volume}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -6,3 +6,8 @@ export interface AudioPlayer {
|
||||
seekTo(seekTo: number): void;
|
||||
setVolume(volume: number): void;
|
||||
}
|
||||
|
||||
export interface PlayerOnProgressProps {
|
||||
played: number;
|
||||
playedSeconds: number;
|
||||
}
|
||||
|
||||
@@ -2,11 +2,14 @@ import type { Dispatch } from 'react';
|
||||
import type ReactPlayer from 'react-player';
|
||||
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { OnProgressProps } from 'react-player/base';
|
||||
|
||||
import { WebPlayerEngine, WebPlayerEngineHandle } from './engine/web-player-engine';
|
||||
|
||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/listener/use-player-events';
|
||||
import {
|
||||
WebPlayerEngine,
|
||||
WebPlayerEngineHandle,
|
||||
} from '/@/renderer/features/player/audio-player/engine/web-player-engine';
|
||||
import { useMainPlayerListener } from '/@/renderer/features/player/audio-player/hooks/use-main-player-listener';
|
||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||
import { PlayerOnProgressProps } from '/@/renderer/features/player/audio-player/types';
|
||||
import {
|
||||
usePlayerActions,
|
||||
usePlayerData,
|
||||
@@ -22,7 +25,7 @@ const PLAY_PAUSE_FADE_INTERVAL = 10;
|
||||
export function WebPlayer() {
|
||||
const playerRef = useRef<WebPlayerEngineHandle>(null);
|
||||
const { num, player1, player2, status } = usePlayerData();
|
||||
const { mediaAutoNext, setProgress } = usePlayerActions();
|
||||
const { mediaAutoNext, setTimestamp } = usePlayerActions();
|
||||
const { crossfadeDuration, speed, transitionType } = usePlayerProperties();
|
||||
const isMuted = usePlayerMuted();
|
||||
const volume = usePlayerVolume();
|
||||
@@ -67,11 +70,11 @@ export function WebPlayer() {
|
||||
);
|
||||
|
||||
const onProgressPlayer1 = useCallback(
|
||||
(e: OnProgressProps) => {
|
||||
(e: PlayerOnProgressProps) => {
|
||||
if (transitionType === 'crossfade' && num === 1) {
|
||||
setProgress(Number(e.playedSeconds.toFixed(0)));
|
||||
setTimestamp(Number(e.playedSeconds.toFixed(0)));
|
||||
} else if (transitionType === 'gapless') {
|
||||
setProgress(Number(e.playedSeconds.toFixed(0)));
|
||||
setTimestamp(Number(e.playedSeconds.toFixed(0)));
|
||||
}
|
||||
|
||||
if (!playerRef.current?.player1()) {
|
||||
@@ -105,15 +108,15 @@ export function WebPlayer() {
|
||||
break;
|
||||
}
|
||||
},
|
||||
[crossfadeDuration, isTransitioning, num, setProgress, transitionType, volume],
|
||||
[crossfadeDuration, isTransitioning, num, setTimestamp, transitionType, volume],
|
||||
);
|
||||
|
||||
const onProgressPlayer2 = useCallback(
|
||||
(e: OnProgressProps) => {
|
||||
(e: PlayerOnProgressProps) => {
|
||||
if (transitionType === PlayerStyle.CROSSFADE && num === 2) {
|
||||
setProgress(Number(e.playedSeconds.toFixed(0)));
|
||||
setTimestamp(Number(e.playedSeconds.toFixed(0)));
|
||||
} else if (transitionType === PlayerStyle.GAPLESS) {
|
||||
setProgress(Number(e.playedSeconds.toFixed(0)));
|
||||
setTimestamp(Number(e.playedSeconds.toFixed(0)));
|
||||
}
|
||||
|
||||
if (!playerRef.current?.player2()) {
|
||||
@@ -147,7 +150,7 @@ export function WebPlayer() {
|
||||
break;
|
||||
}
|
||||
},
|
||||
[crossfadeDuration, isTransitioning, num, setProgress, transitionType, volume],
|
||||
[crossfadeDuration, isTransitioning, num, setTimestamp, transitionType, volume],
|
||||
);
|
||||
|
||||
const handleOnEndedPlayer1 = useCallback(() => {
|
||||
@@ -202,6 +205,8 @@ export function WebPlayer() {
|
||||
[volume, num, isTransitioning],
|
||||
);
|
||||
|
||||
useMainPlayerListener();
|
||||
|
||||
return (
|
||||
<WebPlayerEngine
|
||||
isMuted={isMuted}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import styles from './playerbar-slider.module.css';
|
||||
|
||||
import { MpvPlayer } from '/@/renderer/features/player/audio-player/mpv-player';
|
||||
import { WebPlayer } from '/@/renderer/features/player/audio-player/web-player';
|
||||
import { usePlayerContext } from '/@/renderer/features/player/context/player-context';
|
||||
import {
|
||||
@@ -113,6 +114,7 @@ export const PlayerbarSlider = ({ ...props }: SliderProps) => {
|
||||
</div>
|
||||
</div>
|
||||
{playbackType === PlayerType.WEB && <WebPlayer />}
|
||||
{playbackType === PlayerType.LOCAL && <MpvPlayer />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// import { useTranslation } from 'react-i18next';
|
||||
|
||||
// import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
|
||||
// import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||
// import { updateSong } from '/@/renderer/features/plaayer/update-remote-song';
|
||||
// import {
|
||||
// usePlayerNum,
|
||||
// usePlayerStatus,
|
||||
|
||||
@@ -6,14 +6,13 @@ import {
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { usePlayerStatus, usePlayerStore } from '/@/renderer/store';
|
||||
import { usePlayerStatus } from '/@/renderer/store';
|
||||
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { Slider } from '/@/shared/components/slider/slider';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { CrossfadeStyle, PlayerStyle, PlayerType, PlayerStatus } from '/@/shared/types/types';
|
||||
import { CrossfadeStyle, PlayerStatus, PlayerStyle, PlayerType } from '/@/shared/types/types';
|
||||
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
|
||||
@@ -65,10 +64,6 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
onChange={(e) => {
|
||||
setSettings({ playback: { ...settings, type: e as PlayerType } });
|
||||
ipc?.send('settings-set', { property: 'playbackType', value: e });
|
||||
if (isElectron() && e === PlayerType.LOCAL) {
|
||||
const queueData = usePlayerStore.getState().actions.getPlayerData();
|
||||
setQueue(queueData);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useShallow } from 'zustand/react/shallow';
|
||||
import { createSelectors } from '/@/renderer/lib/zustand';
|
||||
import { useSettingsStore } from '/@/renderer/store/settings.store';
|
||||
import { shuffleInPlace } from '/@/renderer/utils/shuffle';
|
||||
import { QueueSong, Song } from '/@/shared/types/domain-types';
|
||||
import { PlayerData, QueueData, QueueSong, Song } from '/@/shared/types/domain-types';
|
||||
import {
|
||||
Play,
|
||||
PlayerQueueType,
|
||||
@@ -18,34 +18,8 @@ import {
|
||||
PlayerStyle,
|
||||
} from '/@/shared/types/types';
|
||||
|
||||
export interface PlayerData {
|
||||
currentSong: QueueSong | undefined;
|
||||
index: number;
|
||||
muted: boolean;
|
||||
nextSong: QueueSong | undefined;
|
||||
num: 1 | 2;
|
||||
player1: QueueSong | undefined;
|
||||
player2: QueueSong | undefined;
|
||||
previousSong: QueueSong | undefined;
|
||||
queue: QueueData;
|
||||
queueLength: number;
|
||||
repeat: PlayerRepeat;
|
||||
shuffle: PlayerShuffle;
|
||||
speed: number;
|
||||
status: PlayerStatus;
|
||||
transitionType: PlayerStyle;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export interface PlayerState extends Actions, State {}
|
||||
|
||||
export interface QueueData {
|
||||
default: string[];
|
||||
priority: string[];
|
||||
shuffled: string[];
|
||||
songs: QueueSong[];
|
||||
}
|
||||
|
||||
export type QueueGroupingProperty = keyof QueueSong;
|
||||
|
||||
interface Actions {
|
||||
@@ -77,11 +51,11 @@ interface Actions {
|
||||
moveSelectedToNext: (items: QueueSong[]) => void;
|
||||
moveSelectedToTop: (items: QueueSong[]) => void;
|
||||
setCrossfadeDuration: (duration: number) => void;
|
||||
setProgress: (timestamp: number) => void;
|
||||
setQueueType: (queueType: PlayerQueueType) => void;
|
||||
setRepeat: (repeat: PlayerRepeat) => void;
|
||||
setShuffle: (shuffle: PlayerShuffle) => void;
|
||||
setSpeed: (speed: number) => void;
|
||||
setTimestamp: (timestamp: number) => void;
|
||||
setTransitionType: (transitionType: PlayerStyle) => void;
|
||||
setVolume: (volume: number) => void;
|
||||
shuffle: () => void;
|
||||
@@ -131,15 +105,9 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
case Play.LAST: {
|
||||
set((state) => {
|
||||
const currentIndex = state.player.index;
|
||||
const existingIds = new Set(
|
||||
state.queue.songs.map((s) => s._uniqueId),
|
||||
);
|
||||
|
||||
// Add new songs to songs array (avoiding duplicates)
|
||||
// Add new songs to songs object
|
||||
newItems.forEach((item) => {
|
||||
if (!existingIds.has(item._uniqueId)) {
|
||||
state.queue.songs.push(item);
|
||||
}
|
||||
state.queue.songs[item._uniqueId] = item;
|
||||
});
|
||||
|
||||
state.queue.default = [
|
||||
@@ -163,15 +131,9 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
case Play.NEXT: {
|
||||
set((state) => {
|
||||
const currentIndex = state.player.index;
|
||||
const existingIds = new Set(
|
||||
state.queue.songs.map((s) => s._uniqueId),
|
||||
);
|
||||
|
||||
// Add new songs to songs array (avoiding duplicates)
|
||||
// Add new songs to songs object
|
||||
newItems.forEach((item) => {
|
||||
if (!existingIds.has(item._uniqueId)) {
|
||||
state.queue.songs.push(item);
|
||||
}
|
||||
state.queue.songs[item._uniqueId] = item;
|
||||
});
|
||||
|
||||
state.queue.default = [
|
||||
@@ -195,15 +157,9 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
}
|
||||
case Play.NOW: {
|
||||
set((state) => {
|
||||
const existingIds = new Set(
|
||||
state.queue.songs.map((s) => s._uniqueId),
|
||||
);
|
||||
|
||||
// Add new songs to songs array (avoiding duplicates)
|
||||
// Add new songs to songs object
|
||||
newItems.forEach((item) => {
|
||||
if (!existingIds.has(item._uniqueId)) {
|
||||
state.queue.songs.push(item);
|
||||
}
|
||||
state.queue.songs[item._uniqueId] = item;
|
||||
});
|
||||
|
||||
state.queue.default = [];
|
||||
@@ -226,15 +182,9 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
switch (playType) {
|
||||
case Play.LAST: {
|
||||
set((state) => {
|
||||
const existingIds = new Set(
|
||||
state.queue.songs.map((s) => s._uniqueId),
|
||||
);
|
||||
|
||||
// Add new songs to songs array (avoiding duplicates)
|
||||
// Add new songs to songs object
|
||||
newItems.forEach((item) => {
|
||||
if (!existingIds.has(item._uniqueId)) {
|
||||
state.queue.songs.push(item);
|
||||
}
|
||||
state.queue.songs[item._uniqueId] = item;
|
||||
});
|
||||
|
||||
state.queue.priority = [
|
||||
@@ -254,15 +204,10 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
const currentIndex = state.player.index;
|
||||
const isInPriority =
|
||||
currentIndex < state.queue.priority.length;
|
||||
const existingIds = new Set(
|
||||
state.queue.songs.map((s) => s._uniqueId),
|
||||
);
|
||||
|
||||
// Add new songs to songs array (avoiding duplicates)
|
||||
// Add new songs to songs object
|
||||
newItems.forEach((item) => {
|
||||
if (!existingIds.has(item._uniqueId)) {
|
||||
state.queue.songs.push(item);
|
||||
}
|
||||
state.queue.songs[item._uniqueId] = item;
|
||||
});
|
||||
|
||||
if (isInPriority) {
|
||||
@@ -291,15 +236,9 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
}
|
||||
case Play.NOW: {
|
||||
set((state) => {
|
||||
const existingIds = new Set(
|
||||
state.queue.songs.map((s) => s._uniqueId),
|
||||
);
|
||||
|
||||
// Add new songs to songs array (avoiding duplicates)
|
||||
// Add new songs to songs object
|
||||
newItems.forEach((item) => {
|
||||
if (!existingIds.has(item._uniqueId)) {
|
||||
state.queue.songs.push(item);
|
||||
}
|
||||
state.queue.songs[item._uniqueId] = item;
|
||||
});
|
||||
|
||||
state.queue.default = [];
|
||||
@@ -376,13 +315,9 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
const queueType = getQueueType();
|
||||
|
||||
set((state) => {
|
||||
const existingIds = new Set(state.queue.songs.map((s) => s._uniqueId));
|
||||
|
||||
// Add new songs to songs array (avoiding duplicates)
|
||||
// Add new songs to songs object
|
||||
newItems.forEach((item) => {
|
||||
if (!existingIds.has(item._uniqueId)) {
|
||||
state.queue.songs.push(item);
|
||||
}
|
||||
state.queue.songs[item._uniqueId] = item;
|
||||
});
|
||||
|
||||
if (queueType === PlayerQueueType.DEFAULT) {
|
||||
@@ -459,7 +394,7 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
state.player.index = -1;
|
||||
state.queue.default = [];
|
||||
state.queue.priority = [];
|
||||
state.queue.songs = [];
|
||||
state.queue.songs = {};
|
||||
});
|
||||
},
|
||||
clearSelected: (items: QueueSong[]) => {
|
||||
@@ -474,14 +409,18 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
(id) => !uniqueIds.includes(id),
|
||||
);
|
||||
|
||||
// Remove songs from songs array if they're not in default or priority
|
||||
// Remove songs from songs object if they're not in default or priority
|
||||
const remainingIds = new Set([
|
||||
...state.queue.default,
|
||||
...state.queue.priority,
|
||||
]);
|
||||
state.queue.songs = state.queue.songs.filter((song) =>
|
||||
remainingIds.has(song._uniqueId),
|
||||
);
|
||||
const filteredSongs: Record<string, QueueSong> = {};
|
||||
Object.values(state.queue.songs).forEach((song) => {
|
||||
if (remainingIds.has(song._uniqueId)) {
|
||||
filteredSongs[song._uniqueId] = song;
|
||||
}
|
||||
});
|
||||
state.queue.songs = filteredSongs;
|
||||
|
||||
const newQueue = [...state.queue.priority, ...state.queue.default];
|
||||
|
||||
@@ -539,9 +478,7 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
getQueueOrder: () => {
|
||||
const queueType = getQueueType();
|
||||
const state = get();
|
||||
const songsMap = new Map(
|
||||
state.queue.songs.map((song) => [song._uniqueId, song]),
|
||||
);
|
||||
const songsMap = new Map(Object.entries(state.queue.songs));
|
||||
|
||||
if (queueType === PlayerQueueType.PRIORITY) {
|
||||
const defaultIds = state.queue.default;
|
||||
@@ -704,7 +641,7 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
state.player.index = -1;
|
||||
state.queue.default = [];
|
||||
state.queue.priority = [];
|
||||
state.queue.songs = [];
|
||||
state.queue.songs = {};
|
||||
});
|
||||
},
|
||||
mediaToggleMute: () => {
|
||||
@@ -726,12 +663,12 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
const itemUniqueIds = items.map((item) => item._uniqueId);
|
||||
|
||||
set((state) => {
|
||||
const existingIds = new Set(state.queue.songs.map((s) => s._uniqueId));
|
||||
const existingIds = new Set(Object.keys(state.queue.songs));
|
||||
|
||||
// Add new songs to songs array (avoiding duplicates)
|
||||
// Add new songs to songs object (avoiding duplicates)
|
||||
items.forEach((item) => {
|
||||
if (!existingIds.has(item._uniqueId)) {
|
||||
state.queue.songs.push(item);
|
||||
state.queue.songs[item._uniqueId] = item;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -829,13 +766,10 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
moveSelectedToBottom: (items: QueueSong[]) => {
|
||||
set((state) => {
|
||||
const uniqueIds = items.map((item) => item._uniqueId);
|
||||
const existingIds = new Set(state.queue.songs.map((s) => s._uniqueId));
|
||||
|
||||
// Add new songs to songs array (avoiding duplicates)
|
||||
// Add new songs to songs object
|
||||
items.forEach((item) => {
|
||||
if (!existingIds.has(item._uniqueId)) {
|
||||
state.queue.songs.push(item);
|
||||
}
|
||||
state.queue.songs[item._uniqueId] = item;
|
||||
});
|
||||
|
||||
if (state.player.queueType === PlayerQueueType.PRIORITY) {
|
||||
@@ -878,13 +812,10 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
const uniqueId = currentTrack?._uniqueId;
|
||||
|
||||
const uniqueIds = items.map((item) => item._uniqueId);
|
||||
const existingIds = new Set(state.queue.songs.map((s) => s._uniqueId));
|
||||
|
||||
// Add new songs to songs array (avoiding duplicates)
|
||||
// Add new songs to songs object
|
||||
items.forEach((item) => {
|
||||
if (!existingIds.has(item._uniqueId)) {
|
||||
state.queue.songs.push(item);
|
||||
}
|
||||
state.queue.songs[item._uniqueId] = item;
|
||||
});
|
||||
|
||||
if (queueType === PlayerQueueType.DEFAULT) {
|
||||
@@ -966,13 +897,10 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
moveSelectedToTop: (items: QueueSong[]) => {
|
||||
set((state) => {
|
||||
const uniqueIds = items.map((item) => item._uniqueId);
|
||||
const existingIds = new Set(state.queue.songs.map((s) => s._uniqueId));
|
||||
|
||||
// Add new songs to songs array (avoiding duplicates)
|
||||
// Add new songs to songs object
|
||||
items.forEach((item) => {
|
||||
if (!existingIds.has(item._uniqueId)) {
|
||||
state.queue.songs.push(item);
|
||||
}
|
||||
state.queue.songs[item._uniqueId] = item;
|
||||
});
|
||||
|
||||
if (state.player.queueType === PlayerQueueType.PRIORITY) {
|
||||
@@ -1026,7 +954,7 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
default: [],
|
||||
priority: [],
|
||||
shuffled: [],
|
||||
songs: [],
|
||||
songs: {},
|
||||
},
|
||||
setCrossfadeDuration: (duration: number) => {
|
||||
set((state) => {
|
||||
@@ -1034,11 +962,6 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
state.player.crossfadeDuration = normalizedDuration;
|
||||
});
|
||||
},
|
||||
setProgress: (timestamp: number) => {
|
||||
set((state) => {
|
||||
state.player.timestamp = timestamp;
|
||||
});
|
||||
},
|
||||
setQueueType: (queueType: PlayerQueueType) => {
|
||||
set((state) => {
|
||||
// From default -> priority, move all items from default to priority
|
||||
@@ -1075,6 +998,11 @@ export const usePlayerStoreBase = create<PlayerState>()(
|
||||
state.player.speed = normalizedSpeed;
|
||||
});
|
||||
},
|
||||
setTimestamp: (timestamp: number) => {
|
||||
set((state) => {
|
||||
state.player.timestamp = timestamp;
|
||||
});
|
||||
},
|
||||
setTransitionType: (transitionType: PlayerStyle) => {
|
||||
set((state) => {
|
||||
state.player.transitionType = transitionType;
|
||||
@@ -1178,10 +1106,10 @@ export const usePlayerActions = () => {
|
||||
moveSelectedToNext: state.moveSelectedToNext,
|
||||
moveSelectedToTop: state.moveSelectedToTop,
|
||||
setCrossfadeDuration: state.setCrossfadeDuration,
|
||||
setProgress: state.setProgress,
|
||||
setRepeat: state.setRepeat,
|
||||
setShuffle: state.setShuffle,
|
||||
setSpeed: state.setSpeed,
|
||||
setTimestamp: state.setTimestamp,
|
||||
setTransitionType: state.setTransitionType,
|
||||
setVolume: state.setVolume,
|
||||
shuffle: state.shuffle,
|
||||
@@ -1379,13 +1307,12 @@ export const usePlayerData = (): PlayerData => {
|
||||
|
||||
export const updateQueueFavorites = (ids: string[], favorite: boolean) => {
|
||||
usePlayerStoreBase.setState((state) => {
|
||||
// Update songs in the songs array
|
||||
state.queue.songs.forEach((song) => {
|
||||
// Update songs in the songs object
|
||||
Object.values(state.queue.songs).forEach((song) => {
|
||||
if (ids.includes(song.id)) {
|
||||
song.userFavorite = favorite;
|
||||
}
|
||||
});
|
||||
// default and priority are just uniqueId arrays, so they don't need updating
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1434,18 +1361,26 @@ export const usePlayerNum = () => {
|
||||
};
|
||||
|
||||
export const usePlayerQueue = () => {
|
||||
return usePlayerStoreBase((state) => {
|
||||
const queueType = state.player.queueType;
|
||||
return usePlayerStoreBase(
|
||||
useShallow((state) => {
|
||||
const queueType = state.player.queueType;
|
||||
|
||||
switch (queueType) {
|
||||
case PlayerQueueType.DEFAULT:
|
||||
return state.queue.default;
|
||||
case PlayerQueueType.PRIORITY:
|
||||
return state.queue.priority;
|
||||
default:
|
||||
return state.queue.default;
|
||||
}
|
||||
});
|
||||
switch (queueType) {
|
||||
case PlayerQueueType.DEFAULT: {
|
||||
const queue = state.queue.default;
|
||||
return queue.map((id) => state.queue.songs[id]);
|
||||
}
|
||||
case PlayerQueueType.PRIORITY: {
|
||||
const priorityQueue = state.queue.priority;
|
||||
return priorityQueue.map((id) => state.queue.songs[id]);
|
||||
}
|
||||
default: {
|
||||
const defaultQueue = state.queue.default;
|
||||
return defaultQueue.map((id) => state.queue.songs[id]);
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
function getQueueType() {
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
NDUserListSort,
|
||||
} from '/@/shared/api/navidrome/navidrome-types';
|
||||
import { ServerFeatures } from '/@/shared/types/features-types';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
import { PlayerRepeat, PlayerShuffle, PlayerStatus, PlayerStyle } from '/@/shared/types/types';
|
||||
|
||||
export enum LibraryItem {
|
||||
ALBUM = 'album',
|
||||
@@ -58,25 +58,29 @@ export type AnyLibraryItems =
|
||||
| Song[];
|
||||
|
||||
export interface PlayerData {
|
||||
current: {
|
||||
index: number;
|
||||
nextIndex?: number;
|
||||
player: 1 | 2;
|
||||
previousIndex?: number;
|
||||
shuffledIndex: number;
|
||||
song?: QueueSong;
|
||||
status: PlayerStatus;
|
||||
};
|
||||
player1?: QueueSong;
|
||||
player2?: QueueSong;
|
||||
currentSong: QueueSong | undefined;
|
||||
index: number;
|
||||
muted: boolean;
|
||||
nextSong: QueueSong | undefined;
|
||||
num: 1 | 2;
|
||||
player1: QueueSong | undefined;
|
||||
player2: QueueSong | undefined;
|
||||
previousSong: QueueSong | undefined;
|
||||
queue: QueueData;
|
||||
queueLength: number;
|
||||
repeat: PlayerRepeat;
|
||||
shuffle: PlayerShuffle;
|
||||
speed: number;
|
||||
status: PlayerStatus;
|
||||
transitionType: PlayerStyle;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
export interface QueueData {
|
||||
current?: QueueSong;
|
||||
length: number;
|
||||
next?: QueueSong;
|
||||
previous?: QueueSong;
|
||||
default: string[];
|
||||
priority: string[];
|
||||
shuffled: string[];
|
||||
songs: Record<string, QueueSong>;
|
||||
}
|
||||
|
||||
export type QueueSong = Song & {
|
||||
|
||||
Reference in New Issue
Block a user