mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 04:20:12 +02:00
Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a137de612a | |||
| ae9a8e6d08 | |||
| 9c3053608d | |||
| d6ea97fa2a | |||
| 578b00fe3d | |||
| f43f696f54 | |||
| 4a81d7b249 | |||
| 88fbc22923 | |||
| daef3b31fe | |||
| 7142017c26 | |||
| f30a466fb2 | |||
| f2e3e7a74e | |||
| 83efd6e8c5 | |||
| 515496ab85 | |||
| 16b99ef597 | |||
| b607c57f59 | |||
| 9f0a8f2bae | |||
| 0e384a6302 | |||
| 0c7cec9f4f | |||
| f30a706eef | |||
| a980a8de0b | |||
| 9abda23a4a | |||
| 4e3a3742a5 |
@@ -3,7 +3,7 @@ import { BrowserWindow, globalShortcut, systemPreferences } from 'electron';
|
||||
import { isMacOS, isWindows } from '../../../utils';
|
||||
import { store } from '../settings';
|
||||
|
||||
import { PlaybackType } from '/@/shared/types/types';
|
||||
import { PlayerType } from '/@/shared/types/types';
|
||||
|
||||
export const enableMediaKeys = (window: BrowserWindow | null) => {
|
||||
if (isMacOS()) {
|
||||
@@ -26,9 +26,9 @@ export const enableMediaKeys = (window: BrowserWindow | null) => {
|
||||
}
|
||||
|
||||
const enableWindowsMediaSession = store.get('mediaSession', false) as boolean;
|
||||
const playbackType = store.get('playbackType', PlaybackType.WEB) as PlaybackType;
|
||||
const playbackType = store.get('playbackType', PlayerType.WEB) as PlayerType;
|
||||
|
||||
if (!enableWindowsMediaSession || !isWindows() || playbackType !== PlaybackType.WEB) {
|
||||
if (!enableWindowsMediaSession || !isWindows() || playbackType !== PlayerType.WEB) {
|
||||
globalShortcut.register('MediaStop', () => {
|
||||
window?.webContents.send('renderer-player-stop');
|
||||
});
|
||||
|
||||
@@ -382,7 +382,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||
getMainWindow()?.webContents.send('request-favorite', {
|
||||
favorite,
|
||||
id,
|
||||
serverId: currentState.song.serverId,
|
||||
serverId: currentState.song._serverId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -443,7 +443,7 @@ const enableServer = (config: RemoteConfig): Promise<void> => {
|
||||
getMainWindow()?.webContents.send('request-rating', {
|
||||
id,
|
||||
rating,
|
||||
serverId: currentState.song.serverId,
|
||||
serverId: currentState.song._serverId,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -578,7 +578,7 @@ ipcMain.on('remote-username', (_event, username: string) => {
|
||||
});
|
||||
|
||||
ipcMain.on('update-favorite', (_event, favorite: boolean, serverId: string, ids: string[]) => {
|
||||
if (currentState.song?.serverId !== serverId) return;
|
||||
if (currentState.song?._serverId !== serverId) return;
|
||||
|
||||
const id = currentState.song.id;
|
||||
|
||||
@@ -592,7 +592,7 @@ ipcMain.on('update-favorite', (_event, favorite: boolean, serverId: string, ids:
|
||||
});
|
||||
|
||||
ipcMain.on('update-rating', (_event, rating: number, serverId: string, ids: string[]) => {
|
||||
if (currentState.song?.serverId !== serverId) return;
|
||||
if (currentState.song?._serverId !== serverId) return;
|
||||
|
||||
const id = currentState.song.id;
|
||||
|
||||
|
||||
+3
-3
@@ -38,7 +38,7 @@ import {
|
||||
} from './utils';
|
||||
import './features';
|
||||
|
||||
import { PlaybackType, TitleTheme } from '/@/shared/types/types';
|
||||
import { PlayerType, TitleTheme } from '/@/shared/types/types';
|
||||
|
||||
export default class AppUpdater {
|
||||
constructor() {
|
||||
@@ -549,9 +549,9 @@ async function createWindow(first = true): Promise<void> {
|
||||
}
|
||||
|
||||
const enableWindowsMediaSession = store.get('mediaSession', false) as boolean;
|
||||
const playbackType = store.get('playbackType', PlaybackType.WEB) as PlaybackType;
|
||||
const playbackType = store.get('playbackType', PlayerType.WEB) as PlayerType;
|
||||
const shouldDisableMediaFeatures =
|
||||
!isWindows() || !enableWindowsMediaSession || playbackType !== PlaybackType.WEB;
|
||||
!isWindows() || !enableWindowsMediaSession || playbackType !== PlayerType.WEB;
|
||||
if (shouldDisableMediaFeatures) {
|
||||
app.commandLine.appendSwitch(
|
||||
'disable-features',
|
||||
|
||||
@@ -96,7 +96,7 @@ export const RemoteContainer = () => {
|
||||
}}
|
||||
variant="transparent"
|
||||
/>
|
||||
{(song?.serverType === 'navidrome' || song?.serverType === 'subsonic') && (
|
||||
{(song?._serverType === 'navidrome' || song?._serverType === 'subsonic') && (
|
||||
<div style={{ margin: 'auto' }}>
|
||||
<Tooltip label="Double click to clear" openDelay={1000}>
|
||||
<Rating
|
||||
|
||||
+11
-120
@@ -2,62 +2,41 @@ import { ClientSideRowModelModule } from '@ag-grid-community/client-side-row-mod
|
||||
import { ModuleRegistry } from '@ag-grid-community/core';
|
||||
import { InfiniteRowModelModule } from '@ag-grid-community/infinite-row-model';
|
||||
import { MantineProvider } from '@mantine/core';
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
import { Notifications } from '@mantine/notifications';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import isElectron from 'is-electron';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import '@mantine/core/styles.css';
|
||||
import '@mantine/notifications/styles.css';
|
||||
import '@mantine/dates/styles.css';
|
||||
|
||||
import '/@/shared/styles/global.css';
|
||||
|
||||
import '@ag-grid-community/styles/ag-grid.css';
|
||||
import 'overlayscrollbars/overlayscrollbars.css';
|
||||
|
||||
import '/styles/overlayscrollbars.css';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc';
|
||||
import { PlayQueueHandlerContext } from '/@/renderer/features/player/context/play-queue-handler-context';
|
||||
import { PlayerProvider } from '/@/renderer/features/player/context/player-context';
|
||||
import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context';
|
||||
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
|
||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||
import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
||||
import { useServerVersion } from '/@/renderer/hooks/use-server-version';
|
||||
import { IsUpdatedDialog } from '/@/renderer/is-updated-dialog';
|
||||
import { AppRouter } from '/@/renderer/router/app-router';
|
||||
import {
|
||||
PlayerState,
|
||||
useCssSettings,
|
||||
useHotkeySettings,
|
||||
usePlaybackSettings,
|
||||
usePlayerStore,
|
||||
useQueueControls,
|
||||
useRemoteSettings,
|
||||
useSettingsStore,
|
||||
} from '/@/renderer/store';
|
||||
import { useCssSettings, useHotkeySettings, useSettingsStore } from '/@/renderer/store';
|
||||
import { useAppTheme } from '/@/renderer/themes/use-app-theme';
|
||||
import { sanitizeCss } from '/@/renderer/utils/sanitize';
|
||||
import { setQueue } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { PlaybackType, PlayerStatus, WebAudio } from '/@/shared/types/types';
|
||||
import '/styles/overlayscrollbars.css';
|
||||
import { WebAudio } from '/@/shared/types/types';
|
||||
|
||||
ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]);
|
||||
|
||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
const remote = isElectron() ? window.api.remote : null;
|
||||
const utils = isElectron() ? window.api.utils : null;
|
||||
|
||||
export const App = () => {
|
||||
const { mode, theme } = useAppTheme();
|
||||
const language = useSettingsStore((store) => store.general.language);
|
||||
|
||||
const { content, enabled } = useCssSettings();
|
||||
const { type: playbackType } = usePlaybackSettings();
|
||||
const { bindings } = useHotkeySettings();
|
||||
const handlePlayQueueAdd = useHandlePlayQueueAdd();
|
||||
const { clearQueue, restoreQueue } = useQueueControls();
|
||||
const remoteSettings = useRemoteSettings();
|
||||
const cssRef = useRef<HTMLStyleElement | null>(null);
|
||||
useDiscordRpc();
|
||||
useServerVersion();
|
||||
@@ -84,104 +63,16 @@ export const App = () => {
|
||||
return () => {};
|
||||
}, [content, enabled]);
|
||||
|
||||
const providerValue = useMemo(() => {
|
||||
return { handlePlayQueueAdd };
|
||||
}, [handlePlayQueueAdd]);
|
||||
|
||||
const webAudioProvider = useMemo(() => {
|
||||
return { setWebAudio, webAudio };
|
||||
}, [webAudio]);
|
||||
|
||||
// Start the mpv instance on startup
|
||||
useEffect(() => {
|
||||
const initializeMpv = async () => {
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
const isRunning: boolean | undefined = await mpvPlayer?.isRunning();
|
||||
|
||||
mpvPlayer?.stop();
|
||||
|
||||
if (!isRunning) {
|
||||
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
|
||||
const properties: Record<string, any> = {
|
||||
speed: usePlayerStore.getState().speed,
|
||||
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
|
||||
};
|
||||
|
||||
await mpvPlayer?.initialize({
|
||||
extraParameters,
|
||||
properties,
|
||||
});
|
||||
|
||||
mpvPlayer?.volume(properties.volume);
|
||||
}
|
||||
}
|
||||
|
||||
utils?.restoreQueue();
|
||||
};
|
||||
|
||||
if (isElectron()) {
|
||||
initializeMpv();
|
||||
}
|
||||
|
||||
return () => {
|
||||
clearQueue();
|
||||
mpvPlayer?.stop();
|
||||
mpvPlayer?.cleanup();
|
||||
};
|
||||
}, [clearQueue, playbackType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron()) {
|
||||
ipc?.send('set-global-shortcuts', bindings);
|
||||
}
|
||||
}, [bindings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (utils) {
|
||||
utils.onSaveQueue(() => {
|
||||
const { current, queue } = usePlayerStore.getState();
|
||||
const stateToSave: Partial<Pick<PlayerState, 'current' | 'queue'>> = {
|
||||
current: {
|
||||
...current,
|
||||
status: PlayerStatus.PAUSED,
|
||||
},
|
||||
queue,
|
||||
};
|
||||
utils.saveQueue(stateToSave);
|
||||
});
|
||||
|
||||
utils.onRestoreQueue((_event: any, data) => {
|
||||
const playerData = restoreQueue(data);
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
setQueue(playerData, true);
|
||||
}
|
||||
updateSong(playerData.current.song);
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
ipc?.removeAllListeners('renderer-restore-queue');
|
||||
ipc?.removeAllListeners('renderer-save-queue');
|
||||
};
|
||||
}, [playbackType, restoreQueue]);
|
||||
|
||||
useEffect(() => {
|
||||
if (remote) {
|
||||
remote
|
||||
?.updateSetting(
|
||||
remoteSettings.enabled,
|
||||
remoteSettings.port,
|
||||
remoteSettings.username,
|
||||
remoteSettings.password,
|
||||
)
|
||||
.catch((error) => {
|
||||
toast.warn({ message: error, title: 'Failed to enable remote' });
|
||||
});
|
||||
}
|
||||
// We only want to fire this once
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (language) {
|
||||
i18n.changeLanguage(language);
|
||||
@@ -200,11 +91,11 @@ export const App = () => {
|
||||
}}
|
||||
zIndex={50000}
|
||||
/>
|
||||
<PlayQueueHandlerContext.Provider value={providerValue}>
|
||||
<WebAudioContext.Provider value={webAudioProvider}>
|
||||
<WebAudioContext.Provider value={webAudioProvider}>
|
||||
<PlayerProvider>
|
||||
<AppRouter />
|
||||
</WebAudioContext.Provider>
|
||||
</PlayQueueHandlerContext.Provider>
|
||||
</PlayerProvider>
|
||||
</WebAudioContext.Provider>
|
||||
<IsUpdatedDialog />
|
||||
</MantineProvider>
|
||||
);
|
||||
|
||||
@@ -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,17 +3,17 @@ 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/hooks/use-player-events';
|
||||
import { useAppFocus } from '/@/renderer/hooks';
|
||||
import { useCurrentSong, usePlayerStore } from '/@/renderer/store';
|
||||
import { usePlayerSong } from '/@/renderer/store';
|
||||
import { Song } from '/@/shared/types/domain-types';
|
||||
import { PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
interface UseCurrentSongRowStylesProps {
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
}
|
||||
|
||||
export const useCurrentSongRowStyles = ({ tableRef }: UseCurrentSongRowStylesProps) => {
|
||||
const currentSong = useCurrentSong();
|
||||
const currentSong = usePlayerSong();
|
||||
const isFocused = useAppFocus();
|
||||
const isFocusedRef = useRef<boolean>(isFocused);
|
||||
|
||||
@@ -50,11 +50,12 @@ export const useCurrentSongRowStyles = ({ tableRef }: UseCurrentSongRowStylesPro
|
||||
};
|
||||
}, [currentSong?.albumId, currentSong?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
// Redraw song rows when current song changes
|
||||
const unsubSongChange = usePlayerStore.subscribe(
|
||||
(state) => state.current.song,
|
||||
(song, previousSong) => {
|
||||
usePlayerEvents(
|
||||
{
|
||||
onCurrentSongChange: (properties, prev) => {
|
||||
const song = properties.song;
|
||||
const previousSong = prev.song;
|
||||
|
||||
if (tableRef?.current) {
|
||||
const { api, columnApi } = tableRef?.current || {};
|
||||
if (api == null || columnApi == null) {
|
||||
@@ -74,14 +75,8 @@ export const useCurrentSongRowStyles = ({ tableRef }: UseCurrentSongRowStylesPro
|
||||
api.redrawRows({ rowNodes });
|
||||
}
|
||||
},
|
||||
{ equalityFn: (a, b) => a?.id === b?.id },
|
||||
);
|
||||
|
||||
// Redraw song rows when the status changes
|
||||
const unsubStatusChange = usePlayerStore.subscribe(
|
||||
(state) => [state.current.status, state.current.song],
|
||||
(current: (PlayerStatus | Song | undefined)[]) => {
|
||||
const song = current[1] as Song;
|
||||
onPlayerStatus: () => {
|
||||
const song = currentSong;
|
||||
|
||||
if (tableRef?.current) {
|
||||
const { api, columnApi } = tableRef?.current || {};
|
||||
@@ -95,16 +90,9 @@ export const useCurrentSongRowStyles = ({ tableRef }: UseCurrentSongRowStylesPro
|
||||
api.redrawRows({ rowNodes });
|
||||
}
|
||||
},
|
||||
{
|
||||
equalityFn: (a, b) => (a[0] as PlayerStatus) === (b[0] as PlayerStatus),
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubSongChange();
|
||||
unsubStatusChange();
|
||||
};
|
||||
}, [tableRef]);
|
||||
},
|
||||
[tableRef],
|
||||
);
|
||||
|
||||
return {
|
||||
rowClassRules,
|
||||
|
||||
@@ -36,7 +36,7 @@ import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-
|
||||
import { useAppFocus, useContainerQuery } from '/@/renderer/hooks';
|
||||
import { useGenreRoute } from '/@/renderer/hooks/use-genre-route';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useCurrentServer, useCurrentSong, useCurrentStatus } from '/@/renderer/store';
|
||||
import { useCurrentServer, usePlayerSong, usePlayerStatus } from '/@/renderer/store';
|
||||
import {
|
||||
PersistedTableColumn,
|
||||
useGeneralSettings,
|
||||
@@ -85,9 +85,9 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
const tableConfig = useTableSettings('albumDetail');
|
||||
const { setTable } = useSettingsStoreActions();
|
||||
const status = useCurrentStatus();
|
||||
const status = usePlayerStatus();
|
||||
const isFocused = useAppFocus();
|
||||
const currentSong = useCurrentSong();
|
||||
const currentSong = usePlayerSong();
|
||||
const { externalLinks, lastFM, musicBrainz } = useGeneralSettings();
|
||||
const genreRoute = useGenreRoute();
|
||||
|
||||
@@ -290,7 +290,7 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
||||
|
||||
if (detailQuery.data.userFavorite) {
|
||||
deleteFavoriteMutation.mutate({
|
||||
apiClientProps: { serverId: detailQuery.data.serverId },
|
||||
apiClientProps: { serverId: detailQuery.data._serverId },
|
||||
query: {
|
||||
id: [detailQuery.data.id],
|
||||
type: LibraryItem.ALBUM,
|
||||
@@ -298,7 +298,7 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
||||
});
|
||||
} else {
|
||||
createFavoriteMutation.mutate({
|
||||
apiClientProps: { serverId: detailQuery.data.serverId },
|
||||
apiClientProps: { serverId: detailQuery.data._serverId },
|
||||
query: {
|
||||
id: [detailQuery.data.id],
|
||||
type: LibraryItem.ALBUM,
|
||||
|
||||
@@ -41,8 +41,8 @@ export const AlbumDetailHeader = forwardRef(
|
||||
const { t } = useTranslation();
|
||||
|
||||
const showRating =
|
||||
detailQuery?.data?.serverType === ServerType.NAVIDROME ||
|
||||
detailQuery?.data?.serverType === ServerType.SUBSONIC;
|
||||
detailQuery?.data?._serverType === ServerType.NAVIDROME ||
|
||||
detailQuery?.data?._serverType === ServerType.SUBSONIC;
|
||||
|
||||
const originalDifferentFromRelease =
|
||||
detailQuery.data?.originalDate &&
|
||||
@@ -137,7 +137,7 @@ export const AlbumDetailHeader = forwardRef(
|
||||
if (!detailQuery?.data) return;
|
||||
|
||||
updateRatingMutation.mutate({
|
||||
apiClientProps: { serverId: detailQuery.data.serverId },
|
||||
apiClientProps: { serverId: detailQuery.data._serverId },
|
||||
query: {
|
||||
item: [detailQuery.data],
|
||||
rating,
|
||||
|
||||
@@ -69,7 +69,7 @@ const DummyAlbumDetailRoute = () => {
|
||||
try {
|
||||
if (wasFavorite) {
|
||||
await deleteFavoriteMutation.mutateAsync({
|
||||
apiClientProps: { serverId: detailQuery.data.serverId },
|
||||
apiClientProps: { serverId: detailQuery.data._serverId },
|
||||
query: {
|
||||
id: [detailQuery.data.id],
|
||||
type: LibraryItem.SONG,
|
||||
@@ -77,7 +77,7 @@ const DummyAlbumDetailRoute = () => {
|
||||
});
|
||||
} else {
|
||||
await createFavoriteMutation.mutateAsync({
|
||||
apiClientProps: { serverId: detailQuery.data.serverId },
|
||||
apiClientProps: { serverId: detailQuery.data._serverId },
|
||||
query: {
|
||||
id: [detailQuery.data.id],
|
||||
type: LibraryItem.SONG,
|
||||
|
||||
@@ -79,7 +79,7 @@ export const AlbumArtistDetailHeader = forwardRef(
|
||||
});
|
||||
};
|
||||
|
||||
const showRating = detailQuery?.data?.serverType === ServerType.NAVIDROME;
|
||||
const showRating = detailQuery?.data?._serverType === ServerType.NAVIDROME;
|
||||
|
||||
return (
|
||||
<LibraryHeader
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -36,7 +36,7 @@ export const useDiscordRpc = () => {
|
||||
|
||||
// Handle change detection
|
||||
const song = current[0];
|
||||
const trackChanged = lastUniqueId !== song.uniqueId;
|
||||
const trackChanged = lastUniqueId !== song._uniqueId;
|
||||
|
||||
/*
|
||||
1. If the song has just started, update status
|
||||
@@ -51,7 +51,7 @@ export const useDiscordRpc = () => {
|
||||
current[2] !== previous[2]
|
||||
) {
|
||||
if (trackChanged) {
|
||||
setlastUniqueId(song.uniqueId);
|
||||
setlastUniqueId(song._uniqueId);
|
||||
}
|
||||
|
||||
const start = Math.round(Date.now() - current[1] * 1000);
|
||||
@@ -119,12 +119,12 @@ export const useDiscordRpc = () => {
|
||||
}
|
||||
|
||||
if (discordSettings.showServerImage && song) {
|
||||
if (song.serverType === ServerType.JELLYFIN && song.imageUrl) {
|
||||
if (song._serverType === ServerType.JELLYFIN && song.imageUrl) {
|
||||
activity.largeImageKey = song.imageUrl;
|
||||
} else if (song.serverType === ServerType.NAVIDROME) {
|
||||
} else if (song._serverType === ServerType.NAVIDROME) {
|
||||
try {
|
||||
const info = await controller.getAlbumInfo({
|
||||
apiClientProps: { serverId: song.serverId },
|
||||
apiClientProps: { serverId: song._serverId },
|
||||
query: { id: song.albumId },
|
||||
});
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ export const lyricsQueries = {
|
||||
return queryOptions({
|
||||
gcTime: Infinity,
|
||||
queryFn: async ({ signal }): Promise<FullLyricsMetadata | null | StructuredLyric[]> => {
|
||||
const server = getServerById(song?.serverId);
|
||||
const server = getServerById(song?._serverId);
|
||||
if (!server) throw new Error('Server not found');
|
||||
if (!song) return null;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { openLyricSearchModal } from '/@/renderer/features/lyrics/components/lyrics-search-form';
|
||||
import {
|
||||
useCurrentSong,
|
||||
usePlayerSong,
|
||||
useLyricsSettings,
|
||||
useSettingsStore,
|
||||
useSettingsStoreActions,
|
||||
@@ -38,7 +38,7 @@ export const LyricsActions = ({
|
||||
setIndex,
|
||||
}: LyricsActionsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const currentSong = useCurrentSong();
|
||||
const currentSong = usePlayerSong();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const { delayMs, sources } = useLyricsSettings();
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
UnsynchronizedLyricsProps,
|
||||
} from '/@/renderer/features/lyrics/unsynchronized-lyrics';
|
||||
import { queryClient } from '/@/renderer/lib/react-query';
|
||||
import { useCurrentSong, useLyricsSettings, usePlayerStore } from '/@/renderer/store';
|
||||
import { usePlayerSong, useLyricsSettings, usePlayerStore } from '/@/renderer/store';
|
||||
import { Center } from '/@/shared/components/center/center';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
@@ -29,7 +29,7 @@ import { Text } from '/@/shared/components/text/text';
|
||||
import { FullLyricsMetadata, LyricSource, LyricsOverride } from '/@/shared/types/domain-types';
|
||||
|
||||
export const Lyrics = () => {
|
||||
const currentSong = useCurrentSong();
|
||||
const currentSong = usePlayerSong();
|
||||
const {
|
||||
enableAutoTranslation,
|
||||
translationApiKey,
|
||||
@@ -45,7 +45,7 @@ export const Lyrics = () => {
|
||||
lyricsQueries.songLyrics(
|
||||
{
|
||||
query: { songId: currentSong?.id || '' },
|
||||
serverId: currentSong?.serverId || '',
|
||||
serverId: currentSong?._serverId || '',
|
||||
},
|
||||
currentSong,
|
||||
),
|
||||
@@ -73,13 +73,13 @@ export const Lyrics = () => {
|
||||
const handleOnResetLyric = useCallback(() => {
|
||||
queryClient.invalidateQueries({
|
||||
exact: true,
|
||||
queryKey: queryKeys.songs.lyrics(currentSong?.serverId, { songId: currentSong?.id }),
|
||||
queryKey: queryKeys.songs.lyrics(currentSong?._serverId, { songId: currentSong?.id }),
|
||||
});
|
||||
}, [currentSong?.id, currentSong?.serverId]);
|
||||
}, [currentSong?.id, currentSong?._serverId]);
|
||||
|
||||
const handleOnRemoveLyric = useCallback(() => {
|
||||
queryClient.setQueryData(
|
||||
queryKeys.songs.lyrics(currentSong?.serverId, { songId: currentSong?.id }),
|
||||
queryKeys.songs.lyrics(currentSong?._serverId, { songId: currentSong?.id }),
|
||||
(prev: FullLyricsMetadata | undefined) => {
|
||||
if (!prev) {
|
||||
return undefined;
|
||||
@@ -91,7 +91,7 @@ export const Lyrics = () => {
|
||||
};
|
||||
},
|
||||
);
|
||||
}, [currentSong?.id, currentSong?.serverId]);
|
||||
}, [currentSong?.id, currentSong?._serverId]);
|
||||
|
||||
const fetchTranslation = useCallback(async () => {
|
||||
if (!lyrics) return;
|
||||
@@ -126,7 +126,7 @@ export const Lyrics = () => {
|
||||
remoteSource: override?.source as LyricSource | undefined,
|
||||
song: currentSong,
|
||||
},
|
||||
serverId: currentSong?.serverId || '',
|
||||
serverId: currentSong?._serverId || '',
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -8,17 +8,16 @@ import { LyricLine } from '/@/renderer/features/lyrics/lyric-line';
|
||||
import { useScrobble } from '/@/renderer/features/player/hooks/use-scrobble';
|
||||
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
||||
import {
|
||||
useCurrentPlayer,
|
||||
useCurrentStatus,
|
||||
useCurrentTime,
|
||||
useLyricsSettings,
|
||||
usePlaybackType,
|
||||
usePlayerActions,
|
||||
usePlayerData,
|
||||
useSeeked,
|
||||
useSetCurrentTime,
|
||||
usePlayerNum,
|
||||
usePlayerStatus,
|
||||
usePlayerTimestamp,
|
||||
} from '/@/renderer/store';
|
||||
import { FullLyricsMetadata, SynchronizedLyricsArray } from '/@/shared/types/domain-types';
|
||||
import { PlaybackType, PlayerStatus } from '/@/shared/types/types';
|
||||
import { PlayerStatus, PlayerType } from '/@/shared/types/types';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
const utils = isElectron() ? window.api.utils : null;
|
||||
@@ -38,33 +37,33 @@ export const SynchronizedLyrics = ({
|
||||
translatedLyrics,
|
||||
}: SynchronizedLyricsProps) => {
|
||||
const playersRef = PlayersRef;
|
||||
const status = useCurrentStatus();
|
||||
const status = usePlayerStatus();
|
||||
const playbackType = usePlaybackType();
|
||||
const playerData = usePlayerData();
|
||||
const now = useCurrentTime();
|
||||
const now = usePlayerTimestamp();
|
||||
const settings = useLyricsSettings();
|
||||
const currentPlayer = useCurrentPlayer();
|
||||
const currentPlayer = usePlayerNum();
|
||||
const currentPlayerRef =
|
||||
currentPlayer === 1 ? playersRef.current?.player1 : playersRef.current?.player2;
|
||||
const setCurrentTime = useSetCurrentTime();
|
||||
|
||||
const { handleScrobbleFromSeek } = useScrobble();
|
||||
|
||||
const handleSeek = useCallback(
|
||||
(time: number) => {
|
||||
if (playbackType === PlaybackType.LOCAL && mpvPlayer) {
|
||||
if (playbackType === PlayerType.LOCAL && mpvPlayer) {
|
||||
mpvPlayer.seekTo(time);
|
||||
setCurrentTime(time, true);
|
||||
// setCurrentTime(time, true);
|
||||
} else {
|
||||
setCurrentTime(time, true);
|
||||
// setCurrentTime(time, true);
|
||||
handleScrobbleFromSeek(time);
|
||||
mpris?.updateSeek(time);
|
||||
currentPlayerRef?.seekTo(time);
|
||||
}
|
||||
},
|
||||
[currentPlayerRef, handleScrobbleFromSeek, playbackType, setCurrentTime],
|
||||
[currentPlayerRef, handleScrobbleFromSeek, playbackType],
|
||||
);
|
||||
|
||||
const seeked = useSeeked();
|
||||
// const seeked = useSeeked();
|
||||
|
||||
// A reference to the timeout handler
|
||||
const lyricTimer = useRef<null | ReturnType<typeof setTimeout>>(null);
|
||||
@@ -97,7 +96,7 @@ export const SynchronizedLyrics = ({
|
||||
};
|
||||
|
||||
const getCurrentTime = useCallback(async () => {
|
||||
if (isElectron() && playbackType !== PlaybackType.WEB) {
|
||||
if (isElectron() && playbackType !== PlayerType.WEB) {
|
||||
if (mpvPlayer) {
|
||||
return mpvPlayer.getCurrentTime();
|
||||
}
|
||||
@@ -109,7 +108,7 @@ export const SynchronizedLyrics = ({
|
||||
}
|
||||
|
||||
const player =
|
||||
playerData.current.player === 1
|
||||
playerData.player.playerNum === 1
|
||||
? playersRef.current.player1
|
||||
: playersRef.current.player2;
|
||||
const underlying = player?.getInternalPlayer();
|
||||
@@ -282,16 +281,16 @@ export const SynchronizedLyrics = ({
|
||||
// we may be playing the same track (repeat one). In this case, we also
|
||||
// need to restart playback
|
||||
const restarted = status === PlayerStatus.PLAYING && now === 0;
|
||||
if (!seeked && !restarted) {
|
||||
return;
|
||||
}
|
||||
// if (!seeked && !restarted) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
if (lyricTimer.current) {
|
||||
clearTimeout(lyricTimer.current);
|
||||
}
|
||||
|
||||
setCurrentLyric(now * 1000 - delayMsRef.current);
|
||||
}, [now, seeked, setCurrentLyric, status]);
|
||||
}, [now, setCurrentLyric, status]);
|
||||
|
||||
useEffect(() => {
|
||||
// Guaranteed cleanup; stop the timer, and just in case also increment
|
||||
|
||||
@@ -7,15 +7,12 @@ import { useTranslation } from 'react-i18next';
|
||||
import { TableConfigDropdown } from '/@/renderer/components/virtual-table';
|
||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
|
||||
import { usePlayerControls, useQueueControls } from '/@/renderer/store';
|
||||
import { usePlayerStore, useSetCurrentTime } from '/@/renderer/store/player.store';
|
||||
import { usePlaybackType } from '/@/renderer/store/settings.store';
|
||||
import { setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Popover } from '/@/shared/components/popover/popover';
|
||||
import { Song } from '/@/shared/types/domain-types';
|
||||
import { PlaybackType, TableType } from '/@/shared/types/types';
|
||||
import { TableType } from '/@/shared/types/types';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
|
||||
@@ -33,30 +30,30 @@ export const PlayQueueListControls = ({
|
||||
type,
|
||||
}: PlayQueueListOptionsProps) => {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
clearQueue,
|
||||
moveToBottomOfQueue,
|
||||
moveToNextOfQueue,
|
||||
moveToTopOfQueue,
|
||||
removeFromQueue,
|
||||
shuffleQueue,
|
||||
} = useQueueControls();
|
||||
// const {
|
||||
// clearQueue,
|
||||
// moveToBottomOfQueue,
|
||||
// moveToNextOfQueue,
|
||||
// moveToTopOfQueue,
|
||||
// removeFromQueue,
|
||||
// shuffleQueue,
|
||||
// } = useQueueControls();
|
||||
|
||||
const { pause } = usePlayerControls();
|
||||
// const { pause } = usePlayerControls();
|
||||
|
||||
const playbackType = usePlaybackType();
|
||||
const setCurrentTime = useSetCurrentTime();
|
||||
// const setCurrentTime = useSetCurrentTime();
|
||||
|
||||
const handleMoveToNext = () => {
|
||||
const selectedRows = tableRef?.current?.grid.api.getSelectedRows();
|
||||
const uniqueIds = selectedRows?.map((row) => row.uniqueId);
|
||||
if (!uniqueIds?.length) return;
|
||||
|
||||
const playerData = moveToNextOfQueue(uniqueIds);
|
||||
// const playerData = moveToNextOfQueue(uniqueIds);
|
||||
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
setQueueNext(playerData);
|
||||
}
|
||||
// if (playbackType === PlaybackType.LOCAL) {
|
||||
// setQueueNext(playerData);
|
||||
// }
|
||||
};
|
||||
|
||||
const handleMoveToBottom = () => {
|
||||
@@ -64,11 +61,11 @@ export const PlayQueueListControls = ({
|
||||
const uniqueIds = selectedRows?.map((row) => row.uniqueId);
|
||||
if (!uniqueIds?.length) return;
|
||||
|
||||
const playerData = moveToBottomOfQueue(uniqueIds);
|
||||
// const playerData = moveToBottomOfQueue(uniqueIds);
|
||||
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
setQueueNext(playerData);
|
||||
}
|
||||
// if (playbackType === PlaybackType.LOCAL) {
|
||||
// setQueueNext(playerData);
|
||||
// }
|
||||
};
|
||||
|
||||
const handleMoveToTop = () => {
|
||||
@@ -76,11 +73,11 @@ export const PlayQueueListControls = ({
|
||||
const uniqueIds = selectedRows?.map((row) => row.uniqueId);
|
||||
if (!uniqueIds?.length) return;
|
||||
|
||||
const playerData = moveToTopOfQueue(uniqueIds);
|
||||
// const playerData = moveToTopOfQueue(uniqueIds);
|
||||
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
setQueueNext(playerData);
|
||||
}
|
||||
// if (playbackType === PlaybackType.LOCAL) {
|
||||
// setQueueNext(playerData);
|
||||
// }
|
||||
};
|
||||
|
||||
const handleRemoveSelected = () => {
|
||||
@@ -88,43 +85,42 @@ export const PlayQueueListControls = ({
|
||||
const uniqueIds = selectedRows?.map((row) => row.uniqueId);
|
||||
if (!uniqueIds?.length) return;
|
||||
|
||||
const currentSong = usePlayerStore.getState().current.song;
|
||||
const playerData = removeFromQueue(uniqueIds);
|
||||
const isCurrentSongRemoved = currentSong && uniqueIds.includes(currentSong.uniqueId);
|
||||
// const currentSong = usePlayerStore.getState().current.song;
|
||||
// const playerData = removeFromQueue(uniqueIds);
|
||||
// const isCurrentSongRemoved = currentSong && uniqueIds.includes(currentSong.uniqueId);
|
||||
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
if (isCurrentSongRemoved) {
|
||||
setQueue(playerData);
|
||||
} else {
|
||||
setQueueNext(playerData);
|
||||
}
|
||||
}
|
||||
// if (playbackType === PlaybackType.LOCAL) {
|
||||
// if (isCurrentSongRemoved) {
|
||||
// setQueue(playerData);
|
||||
// } else {
|
||||
// setQueueNext(playerData);
|
||||
// }
|
||||
// }
|
||||
|
||||
if (isCurrentSongRemoved) {
|
||||
updateSong(playerData.current.song);
|
||||
}
|
||||
// if (isCurrentSongRemoved) {
|
||||
// updateSong(playerData.current.song);
|
||||
// }
|
||||
};
|
||||
|
||||
const handleClearQueue = () => {
|
||||
const playerData = clearQueue();
|
||||
// const playerData = clearQueue();
|
||||
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
setQueue(playerData);
|
||||
mpvPlayer!.pause();
|
||||
}
|
||||
// if (playbackType === PlaybackType.LOCAL) {
|
||||
// setQueue(playerData);
|
||||
// mpvPlayer!.pause();
|
||||
// }
|
||||
|
||||
updateSong(undefined);
|
||||
|
||||
setCurrentTime(0);
|
||||
pause();
|
||||
// setCurrentTime(0);
|
||||
// pause();
|
||||
};
|
||||
|
||||
const handleShuffleQueue = () => {
|
||||
const playerData = shuffleQueue();
|
||||
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
setQueueNext(playerData);
|
||||
}
|
||||
// const playerData = shuffleQueue();
|
||||
// if (playbackType === PlaybackType.LOCAL) {
|
||||
// setQueueNext(playerData);
|
||||
// }
|
||||
};
|
||||
|
||||
const handleSearchTerm = useCallback(
|
||||
|
||||
@@ -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,32 +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,
|
||||
useCurrentSong,
|
||||
useCurrentStatus,
|
||||
useDefaultQueue,
|
||||
usePlayerControls,
|
||||
usePreviousSong,
|
||||
useQueueControls,
|
||||
useVolume,
|
||||
usePlayerQueue,
|
||||
usePlayerSong,
|
||||
usePlayerStatus,
|
||||
} 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 { PlaybackType, TableType } from '/@/shared/types/types';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
import { TableType } from '/@/shared/types/types';
|
||||
|
||||
type QueueProps = {
|
||||
searchTerm?: string;
|
||||
@@ -54,18 +38,13 @@ type QueueProps = {
|
||||
export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<any>) => {
|
||||
const tableRef = useRef<AgGridReactType | null>(null);
|
||||
const mergedRef = useMergedRef(ref, tableRef);
|
||||
const queue = useDefaultQueue();
|
||||
const { reorderQueue, setCurrentTrack } = useQueueControls();
|
||||
const currentSong = useCurrentSong();
|
||||
const previousSong = usePreviousSong();
|
||||
const status = useCurrentStatus();
|
||||
const queue = usePlayerQueue();
|
||||
const currentSong = usePlayerSong();
|
||||
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 = useVolume();
|
||||
const isFocused = useAppFocus();
|
||||
const isFocusedRef = useRef<boolean>(isFocused);
|
||||
|
||||
@@ -94,26 +73,26 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
|
||||
[tableConfig.columns],
|
||||
);
|
||||
|
||||
const handleDoubleClick = (e: CellDoubleClickedEvent) => {
|
||||
const playerData = setCurrentTrack(e.data.uniqueId);
|
||||
updateSong(playerData.current.song);
|
||||
// const handleDoubleClick = (e: CellDoubleClickedEvent) => {
|
||||
// const playerData = setCurrentTrack(e.data.uniqueId);
|
||||
// updateSong(playerData.current.song);
|
||||
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
mpvPlayer!.volume(volume);
|
||||
setQueue(playerData, false);
|
||||
} else {
|
||||
const player =
|
||||
playerData.current.player === 1
|
||||
? PlayersRef.current?.player1
|
||||
: PlayersRef.current?.player2;
|
||||
const underlying = player?.getInternalPlayer();
|
||||
if (underlying) {
|
||||
underlying.currentTime = 0;
|
||||
}
|
||||
}
|
||||
// if (playbackType === PlaybackType.LOCAL) {
|
||||
// mpvPlayer!.volume(volume);
|
||||
// setQueue(playerData, false);
|
||||
// } else {
|
||||
// const player =
|
||||
// playerData.current.player === 1
|
||||
// ? PlayersRef.current?.player1
|
||||
// : PlayersRef.current?.player2;
|
||||
// const underlying = player?.getInternalPlayer();
|
||||
// if (underlying) {
|
||||
// underlying.currentTime = 0;
|
||||
// }
|
||||
// }
|
||||
|
||||
play();
|
||||
};
|
||||
// play();
|
||||
// };
|
||||
|
||||
const handleDragStart = () => {
|
||||
if (type === 'sideDrawerQueue') {
|
||||
@@ -125,14 +104,14 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
|
||||
const handleDragEnd = (e: RowDragEvent<QueueSong>) => {
|
||||
if (!e.nodes.length) return;
|
||||
const selectedUniqueIds = e.nodes
|
||||
.map((node) => node.data?.uniqueId)
|
||||
.map((node) => node.data?._uniqueId)
|
||||
.filter((e) => e !== undefined);
|
||||
|
||||
const playerData = reorderQueue(selectedUniqueIds as string[], e.overNode?.data?.uniqueId);
|
||||
// const playerData = reorderQueue(selectedUniqueIds as string[], e.overNode?.data?.uniqueId);
|
||||
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
setQueueNext(playerData);
|
||||
}
|
||||
// if (playbackType === PlaybackType.LOCAL) {
|
||||
// setQueueNext(playerData);
|
||||
// }
|
||||
|
||||
if (type === 'sideDrawerQueue') {
|
||||
setAppStore({ isReorderingQueue: false });
|
||||
@@ -146,8 +125,8 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
|
||||
const handleGridReady = () => {
|
||||
const { api } = tableRef?.current || {};
|
||||
|
||||
if (currentSong?.uniqueId) {
|
||||
const currentNode = api?.getRowNode(currentSong?.uniqueId);
|
||||
if (currentSong?._uniqueId) {
|
||||
const currentNode = api?.getRowNode(currentSong?._uniqueId);
|
||||
|
||||
if (!currentNode) return;
|
||||
api?.ensureNodeVisible(currentNode, 'middle');
|
||||
@@ -199,10 +178,18 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
|
||||
const rowClassRules = useMemo<RowClassRules | undefined>(() => {
|
||||
return {
|
||||
'current-song': (params) => {
|
||||
return params.data.uniqueId === currentSong?.uniqueId;
|
||||
return params.data.uniqueId === currentSong?._uniqueId;
|
||||
},
|
||||
};
|
||||
}, [currentSong?.uniqueId]);
|
||||
}, [currentSong?._uniqueId]);
|
||||
|
||||
const previousSongRef = useRef<QueueSong | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentSong) {
|
||||
previousSongRef.current = currentSong;
|
||||
}
|
||||
}, [currentSong]);
|
||||
|
||||
// Redraw the current song row when the previous song changes
|
||||
useEffect(() => {
|
||||
@@ -212,11 +199,11 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
|
||||
return;
|
||||
}
|
||||
|
||||
const currentNode = currentSong?.uniqueId
|
||||
? api.getRowNode(currentSong.uniqueId)
|
||||
const currentNode = currentSong?._uniqueId
|
||||
? api.getRowNode(currentSong._uniqueId)
|
||||
: undefined;
|
||||
const previousNode = previousSong?.uniqueId
|
||||
? api.getRowNode(previousSong?.uniqueId)
|
||||
const previousNode = previousSongRef.current?._uniqueId
|
||||
? api.getRowNode(previousSongRef.current?._uniqueId)
|
||||
: undefined;
|
||||
|
||||
const rowNodes = [currentNode, previousNode].filter(
|
||||
@@ -231,7 +218,7 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [currentSong, previousSong, tableConfig.followCurrentSong, status]);
|
||||
}, [currentSong, previousSongRef, tableConfig.followCurrentSong, status]);
|
||||
|
||||
// As a separate rule, update the current row when focus changes. This is
|
||||
// to prevent queue scrolling when the application loses and then gains focus.
|
||||
@@ -243,8 +230,8 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
|
||||
return;
|
||||
}
|
||||
|
||||
const currentNode = currentSong?.uniqueId
|
||||
? api.getRowNode(currentSong.uniqueId)
|
||||
const currentNode = currentSong?._uniqueId
|
||||
? api.getRowNode(currentSong._uniqueId)
|
||||
: undefined;
|
||||
|
||||
if (currentNode) {
|
||||
@@ -266,7 +253,7 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
|
||||
columnDefs={columnDefs}
|
||||
context={{
|
||||
currentSong,
|
||||
handleDoubleClick,
|
||||
// handleDoubleClick,
|
||||
isFocused,
|
||||
isQueue: true,
|
||||
itemType: LibraryItem.SONG,
|
||||
@@ -276,7 +263,7 @@ export const PlayQueue = forwardRef(({ searchTerm, type }: QueueProps, ref: Ref<
|
||||
deselectOnClickOutside={type === 'fullScreen'}
|
||||
getRowId={(data) => data.data.uniqueId}
|
||||
onCellContextMenu={onCellContextMenu}
|
||||
onCellDoubleClicked={handleDoubleClick}
|
||||
// onCellDoubleClicked={handleDoubleClick}
|
||||
onColumnMoved={handleColumnChange}
|
||||
onColumnResized={debouncedColumnChange}
|
||||
onDragStarted={handleDragStart}
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
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 { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings';
|
||||
import { useSettingsStore } from '/@/renderer/store';
|
||||
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 utils = isElectron() ? window.api.utils : 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);
|
||||
|
||||
const mpvExtraParameters = useSettingsStore((store) => store.playback.mpvExtraParameters);
|
||||
const mpvProperties = useSettingsStore((store) => store.playback.mpvProperties);
|
||||
|
||||
// Start the mpv instance on startup
|
||||
useEffect(() => {
|
||||
const initializeMpv = async () => {
|
||||
const isRunning: boolean | undefined = await mpvPlayer?.isRunning();
|
||||
|
||||
mpvPlayer?.stop();
|
||||
|
||||
if (!isRunning) {
|
||||
const properties: Record<string, any> = {
|
||||
// speed: usePlayerStore.getState().speed,
|
||||
...getMpvProperties(mpvProperties),
|
||||
};
|
||||
|
||||
await mpvPlayer?.initialize({
|
||||
extraParameters: mpvExtraParameters,
|
||||
properties,
|
||||
});
|
||||
|
||||
mpvPlayer?.volume(properties.volume);
|
||||
}
|
||||
|
||||
utils?.restoreQueue();
|
||||
};
|
||||
|
||||
initializeMpv();
|
||||
|
||||
return () => {
|
||||
mpvPlayer?.stop();
|
||||
mpvPlayer?.cleanup();
|
||||
};
|
||||
}, [mpvExtraParameters, mpvProperties]);
|
||||
|
||||
// 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';
|
||||
@@ -0,0 +1,166 @@
|
||||
import type { RefObject } from 'react';
|
||||
|
||||
import { useImperativeHandle, useRef, useState } from 'react';
|
||||
import ReactPlayer from 'react-player';
|
||||
|
||||
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 WebPlayerEngineHandle extends AudioPlayer {
|
||||
player1(): {
|
||||
ref: null | ReactPlayer;
|
||||
setVolume: (volume: number) => void;
|
||||
};
|
||||
player2(): {
|
||||
ref: null | ReactPlayer;
|
||||
setVolume: (volume: number) => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface WebPlayerEngineProps {
|
||||
isMuted: boolean;
|
||||
isTransitioning: boolean;
|
||||
onEndedPlayer1: () => void;
|
||||
onEndedPlayer2: () => void;
|
||||
onProgressPlayer1: (e: PlayerOnProgressProps) => void;
|
||||
onProgressPlayer2: (e: PlayerOnProgressProps) => void;
|
||||
playerNum: number;
|
||||
playerRef: RefObject<WebPlayerEngineHandle>;
|
||||
playerStatus: PlayerStatus;
|
||||
speed?: number;
|
||||
src1: string | undefined;
|
||||
src2: string | undefined;
|
||||
volume: number;
|
||||
}
|
||||
|
||||
// Credits: https://gist.github.com/novwhisky/8a1a0168b94f3b6abfaa?permalink_comment_id=1551393#gistcomment-1551393
|
||||
// This is used so that the player will always have an <audio> element. This means that
|
||||
// player1Source and player2Source are connected BEFORE the user presses play for
|
||||
// the first time. This workaround is important for Safari, which seems to require the
|
||||
// source to be connected PRIOR to resuming audio context
|
||||
const EMPTY_SOURCE =
|
||||
'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV';
|
||||
|
||||
export const WebPlayerEngine = (props: WebPlayerEngineProps) => {
|
||||
const {
|
||||
isMuted,
|
||||
isTransitioning,
|
||||
onEndedPlayer1,
|
||||
onEndedPlayer2,
|
||||
onProgressPlayer1,
|
||||
onProgressPlayer2,
|
||||
playerNum,
|
||||
playerRef,
|
||||
playerStatus,
|
||||
speed,
|
||||
src1,
|
||||
src2,
|
||||
volume,
|
||||
} = props;
|
||||
|
||||
const player1Ref = useRef<null | ReactPlayer>(null);
|
||||
const player2Ref = useRef<null | ReactPlayer>(null);
|
||||
|
||||
const [internalVolume1, setInternalVolume1] = useState(volume / 100 || 0);
|
||||
const [internalVolume2, setInternalVolume2] = useState(volume / 100 || 0);
|
||||
|
||||
useImperativeHandle<WebPlayerEngineHandle, WebPlayerEngineHandle>(playerRef, () => ({
|
||||
decreaseVolume(by: number) {
|
||||
setInternalVolume1(Math.max(0, internalVolume1 - by / 100));
|
||||
setInternalVolume2(Math.max(0, internalVolume2 - by / 100));
|
||||
},
|
||||
increaseVolume(by: number) {
|
||||
setInternalVolume1(Math.min(1, internalVolume1 + by / 100));
|
||||
setInternalVolume2(Math.min(1, internalVolume2 + by / 100));
|
||||
},
|
||||
pause() {
|
||||
player1Ref.current?.getInternalPlayer()?.pause();
|
||||
player2Ref.current?.getInternalPlayer()?.pause();
|
||||
},
|
||||
play() {
|
||||
if (playerNum === 1) {
|
||||
player1Ref.current?.getInternalPlayer()?.play();
|
||||
} else {
|
||||
player2Ref.current?.getInternalPlayer()?.play();
|
||||
}
|
||||
},
|
||||
player1() {
|
||||
return {
|
||||
ref: player1Ref?.current,
|
||||
setVolume: (volume: number) => setInternalVolume1(volume / 100 || 0),
|
||||
};
|
||||
},
|
||||
player2() {
|
||||
return {
|
||||
ref: player2Ref?.current,
|
||||
setVolume: (volume: number) => setInternalVolume2(volume / 100 || 0),
|
||||
};
|
||||
},
|
||||
seekTo(seekTo: number) {
|
||||
playerNum === 1
|
||||
? player1Ref.current?.seekTo(seekTo)
|
||||
: player2Ref.current?.seekTo(seekTo);
|
||||
},
|
||||
setVolume(volume: number) {
|
||||
setInternalVolume1(volume / 100 || 0);
|
||||
setInternalVolume2(volume / 100 || 0);
|
||||
},
|
||||
setVolume1(volume: number) {
|
||||
setInternalVolume1(volume / 100 || 0);
|
||||
},
|
||||
setVolume2(volume: number) {
|
||||
setInternalVolume2(volume / 100 || 0);
|
||||
},
|
||||
}));
|
||||
|
||||
const volume1 = convertToLogVolume(internalVolume1);
|
||||
const volume2 = convertToLogVolume(internalVolume2);
|
||||
|
||||
return (
|
||||
<div id="web-player-engine" style={{ display: 'none' }}>
|
||||
{Boolean(src1) && (
|
||||
<ReactPlayer
|
||||
config={{
|
||||
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
|
||||
}}
|
||||
controls={false}
|
||||
height={0}
|
||||
id="web-player-1"
|
||||
muted={isMuted}
|
||||
onEnded={src1 ? () => onEndedPlayer1() : undefined}
|
||||
onProgress={onProgressPlayer1}
|
||||
playbackRate={speed || 1}
|
||||
playing={playerNum === 1 && playerStatus === PlayerStatus.PLAYING}
|
||||
progressInterval={isTransitioning ? 10 : 250}
|
||||
ref={player1Ref}
|
||||
url={src1 || EMPTY_SOURCE}
|
||||
volume={volume1}
|
||||
width={0}
|
||||
/>
|
||||
)}
|
||||
{Boolean(src2) && (
|
||||
<ReactPlayer
|
||||
config={{
|
||||
file: { attributes: { crossOrigin: 'anonymous' }, forceAudio: true },
|
||||
}}
|
||||
controls={false}
|
||||
height={0}
|
||||
id="web-player-2"
|
||||
muted={isMuted}
|
||||
onEnded={src2 ? () => onEndedPlayer2() : undefined}
|
||||
onProgress={onProgressPlayer2}
|
||||
playbackRate={speed || 1}
|
||||
playing={playerNum === 2 && playerStatus === PlayerStatus.PLAYING}
|
||||
progressInterval={isTransitioning ? 10 : 250}
|
||||
ref={player2Ref}
|
||||
url={src2 || EMPTY_SOURCE}
|
||||
volume={volume2}
|
||||
width={0}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
WebPlayerEngine.displayName = 'WebPlayerEngine';
|
||||
@@ -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,
|
||||
]);
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
subscribeCurrentTrack,
|
||||
subscribePlayerMute,
|
||||
subscribePlayerProgress,
|
||||
subscribePlayerQueue,
|
||||
subscribePlayerRepeat,
|
||||
subscribePlayerSeekToTimestamp,
|
||||
subscribePlayerShuffle,
|
||||
subscribePlayerSpeed,
|
||||
subscribePlayerStatus,
|
||||
subscribePlayerVolume,
|
||||
} from '/@/renderer/store';
|
||||
import { QueueData, QueueSong } from '/@/shared/types/domain-types';
|
||||
import { PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
interface PlayerEvents {
|
||||
cleanup: () => void;
|
||||
}
|
||||
|
||||
interface PlayerEventsCallbacks {
|
||||
onCurrentSongChange?: (
|
||||
properties: { index: number; song: QueueSong | undefined },
|
||||
prev: { index: number; song: QueueSong | undefined },
|
||||
) => void;
|
||||
onPlayerMute?: (properties: { muted: boolean }, prev: { muted: boolean }) => void;
|
||||
onPlayerProgress?: (properties: { timestamp: number }, prev: { timestamp: number }) => void;
|
||||
onPlayerQueueChange?: (queue: QueueData, prev: QueueData) => void;
|
||||
onPlayerRepeat?: (properties: { repeat: PlayerRepeat }, prev: { repeat: PlayerRepeat }) => void;
|
||||
onPlayerSeek?: (properties: { seconds: number }, prev: { seconds: number }) => void;
|
||||
onPlayerSeekToTimestamp?: (
|
||||
properties: { timestamp: number },
|
||||
prev: { timestamp: number },
|
||||
) => void;
|
||||
onPlayerShuffle?: (
|
||||
properties: { shuffle: PlayerShuffle },
|
||||
prev: { shuffle: PlayerShuffle },
|
||||
) => void;
|
||||
onPlayerSpeed?: (properties: { speed: number }, prev: { speed: number }) => void;
|
||||
onPlayerStatus?: (properties: { status: PlayerStatus }, prev: { status: PlayerStatus }) => void;
|
||||
onPlayerVolume?: (properties: { volume: number }, prev: { volume: number }) => void;
|
||||
}
|
||||
|
||||
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
|
||||
if (callbacks.onCurrentSongChange) {
|
||||
const unsubscribe = subscribeCurrentTrack(callbacks.onCurrentSongChange);
|
||||
unsubscribers.push(unsubscribe);
|
||||
}
|
||||
|
||||
// Subscribe to player progress
|
||||
if (callbacks.onPlayerProgress) {
|
||||
const unsubscribe = subscribePlayerProgress(callbacks.onPlayerProgress);
|
||||
unsubscribers.push(unsubscribe);
|
||||
}
|
||||
|
||||
// Subscribe to queue changes
|
||||
if (callbacks.onPlayerQueueChange) {
|
||||
const unsubscribe = subscribePlayerQueue(callbacks.onPlayerQueueChange);
|
||||
unsubscribers.push(unsubscribe);
|
||||
}
|
||||
|
||||
// Subscribe to seek events
|
||||
if (callbacks.onPlayerSeekToTimestamp) {
|
||||
const unsubscribe = subscribePlayerSeekToTimestamp(callbacks.onPlayerSeekToTimestamp);
|
||||
unsubscribers.push(unsubscribe);
|
||||
}
|
||||
|
||||
// Subscribe to player status changes
|
||||
if (callbacks.onPlayerStatus) {
|
||||
const unsubscribe = subscribePlayerStatus(callbacks.onPlayerStatus);
|
||||
unsubscribers.push(unsubscribe);
|
||||
}
|
||||
|
||||
// Subscribe to volume changes
|
||||
if (callbacks.onPlayerVolume) {
|
||||
const unsubscribe = subscribePlayerVolume(callbacks.onPlayerVolume);
|
||||
unsubscribers.push(unsubscribe);
|
||||
}
|
||||
|
||||
// Subscribe to mute changes
|
||||
if (callbacks.onPlayerMute) {
|
||||
const unsubscribe = subscribePlayerMute(callbacks.onPlayerMute);
|
||||
unsubscribers.push(unsubscribe);
|
||||
}
|
||||
|
||||
// Subscribe to speed changes
|
||||
if (callbacks.onPlayerSpeed) {
|
||||
const unsubscribe = subscribePlayerSpeed(callbacks.onPlayerSpeed);
|
||||
unsubscribers.push(unsubscribe);
|
||||
}
|
||||
|
||||
// Subscribe to repeat changes
|
||||
if (callbacks.onPlayerRepeat) {
|
||||
const unsubscribe = subscribePlayerRepeat(callbacks.onPlayerRepeat);
|
||||
unsubscribers.push(unsubscribe);
|
||||
}
|
||||
|
||||
// Subscribe to shuffle changes
|
||||
if (callbacks.onPlayerShuffle) {
|
||||
const unsubscribe = subscribePlayerShuffle(callbacks.onPlayerShuffle);
|
||||
unsubscribers.push(unsubscribe);
|
||||
}
|
||||
|
||||
return {
|
||||
cleanup: () => {
|
||||
unsubscribers.forEach((unsubscribe) => unsubscribe());
|
||||
},
|
||||
};
|
||||
}
|
||||
+25
-16
@@ -1,4 +1,4 @@
|
||||
import type { Song } from '/@/shared/types/domain-types';
|
||||
import type { QueueSong, Song } from '/@/shared/types/domain-types';
|
||||
import type { CrossfadeStyle } from '/@/shared/types/types';
|
||||
import type { ReactPlayerProps } from 'react-player';
|
||||
|
||||
@@ -18,12 +18,17 @@ import { api } from '/@/renderer/api';
|
||||
import {
|
||||
crossfadeHandler,
|
||||
gaplessHandler,
|
||||
} from '/@/renderer/components/audio-player/utils/list-handlers';
|
||||
} from '/@/renderer/features/player/audio-player/utils/list-handlers';
|
||||
import { useWebAudio } from '/@/renderer/features/player/hooks/use-webaudio';
|
||||
import { TranscodingConfig, usePlaybackSettings, useSpeed } from '/@/renderer/store';
|
||||
import { useSettingsStore, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import {
|
||||
TranscodingConfig,
|
||||
usePlaybackSettings,
|
||||
usePlayerSpeed,
|
||||
useSettingsStore,
|
||||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { PlaybackStyle, PlayerStatus } from '/@/shared/types/types';
|
||||
import { PlayerStatus, PlayerStyle } from '/@/shared/types/types';
|
||||
|
||||
export type AudioPlayerProgress = {
|
||||
loaded: number;
|
||||
@@ -38,7 +43,7 @@ interface AudioPlayerProps extends ReactPlayerProps {
|
||||
crossfadeStyle: CrossfadeStyle;
|
||||
currentPlayer: 1 | 2;
|
||||
muted: boolean;
|
||||
playbackStyle: PlaybackStyle;
|
||||
playbackStyle: PlayerStyle;
|
||||
player1?: Song;
|
||||
player2?: Song;
|
||||
status: PlayerStatus;
|
||||
@@ -57,26 +62,30 @@ const getDuration = (ref: any) => {
|
||||
const EMPTY_SOURCE =
|
||||
'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU2LjM2LjEwMAAAAAAAAAAAAAAA//OEAAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAAEAAABIADAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV6urq6urq6urq6urq6urq6urq6urq6urq6v////////////////////////////////8AAAAATGF2YzU2LjQxAAAAAAAAAAAAAAAAJAAAAAAAAAAAASDs90hvAAAAAAAAAAAAAAAAAAAA//MUZAAAAAGkAAAAAAAAA0gAAAAATEFN//MUZAMAAAGkAAAAAAAAA0gAAAAARTMu//MUZAYAAAGkAAAAAAAAA0gAAAAAOTku//MUZAkAAAGkAAAAAAAAA0gAAAAANVVV';
|
||||
|
||||
const useSongUrl = (transcode: TranscodingConfig, current: boolean, song?: Song): null | string => {
|
||||
const useSongUrl = (
|
||||
transcode: TranscodingConfig,
|
||||
current: boolean,
|
||||
song?: QueueSong,
|
||||
): null | string => {
|
||||
const prior = useRef(['', '']);
|
||||
|
||||
return useMemo(() => {
|
||||
if (song?.serverId) {
|
||||
if (song?._serverId) {
|
||||
// If we are the current track, we do not want a transcoding
|
||||
// reconfiguration to force a restart.
|
||||
if (current && prior.current[0] === song.uniqueId) {
|
||||
if (current && prior.current[0] === song._uniqueId) {
|
||||
return prior.current[1];
|
||||
}
|
||||
|
||||
if (!transcode.enabled) {
|
||||
// transcoding disabled; save the result
|
||||
prior.current = [song.uniqueId, song.streamUrl];
|
||||
prior.current = [song._uniqueId, song.streamUrl];
|
||||
return song.streamUrl;
|
||||
}
|
||||
|
||||
const result = api.controller.getTranscodingUrl({
|
||||
apiClientProps: {
|
||||
serverId: song.serverId,
|
||||
serverId: song._serverId,
|
||||
},
|
||||
query: {
|
||||
base: song.streamUrl,
|
||||
@@ -85,14 +94,14 @@ const useSongUrl = (transcode: TranscodingConfig, current: boolean, song?: Song)
|
||||
})!;
|
||||
|
||||
// transcoding enabled; save the updated result
|
||||
prior.current = [song.uniqueId, result];
|
||||
prior.current = [song._uniqueId, result];
|
||||
return result;
|
||||
}
|
||||
|
||||
// no track; clear result
|
||||
prior.current = ['', ''];
|
||||
return null;
|
||||
}, [current, song?.uniqueId, song?.serverId, song?.streamUrl, transcode]);
|
||||
}, [current, song?._uniqueId, song?._serverId, song?.streamUrl, transcode]);
|
||||
};
|
||||
|
||||
export interface AudioPlayerRef {
|
||||
@@ -122,7 +131,7 @@ export const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>((props,
|
||||
const shouldUseWebAudio = useSettingsStore((state) => state.playback.webAudio);
|
||||
const preservesPitch = useSettingsStore((state) => state.playback.preservePitch);
|
||||
const { resetSampleRate } = useSettingsStoreActions();
|
||||
const playbackSpeed = useSpeed();
|
||||
const playbackSpeed = usePlayerSpeed();
|
||||
const { transcode } = usePlaybackSettings();
|
||||
|
||||
const stream1 = useSongUrl(transcode, currentPlayer === 1, player1);
|
||||
@@ -448,7 +457,7 @@ export const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>((props,
|
||||
onEnded={stream1 ? handleOnEnded : undefined}
|
||||
onError={handleOnError(player1Ref)}
|
||||
onProgress={
|
||||
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless1 : handleCrossfade1
|
||||
playbackStyle === PlayerStyle.GAPLESS ? handleGapless1 : handleCrossfade1
|
||||
}
|
||||
onReady={handlePlayer1Start}
|
||||
playbackRate={playbackSpeed}
|
||||
@@ -468,7 +477,7 @@ export const AudioPlayer = forwardRef<AudioPlayerRef, AudioPlayerProps>((props,
|
||||
onEnded={stream2 ? handleOnEnded : undefined}
|
||||
onError={handleOnError(player2Ref)}
|
||||
onProgress={
|
||||
playbackStyle === PlaybackStyle.GAPLESS ? handleGapless2 : handleCrossfade2
|
||||
playbackStyle === PlayerStyle.GAPLESS ? handleGapless2 : handleCrossfade2
|
||||
}
|
||||
onReady={handlePlayer2Start}
|
||||
playbackRate={playbackSpeed}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export interface AudioPlayer {
|
||||
decreaseVolume(by: number): void;
|
||||
increaseVolume(by: number): void;
|
||||
pause(): void;
|
||||
play(): void;
|
||||
seekTo(seekTo: number): void;
|
||||
setVolume(volume: number): void;
|
||||
}
|
||||
|
||||
export interface PlayerOnProgressProps {
|
||||
played: number;
|
||||
playedSeconds: number;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export const convertToLogVolume = (linearVolume: number) => {
|
||||
return Math.pow(linearVolume, 2.0);
|
||||
};
|
||||
@@ -0,0 +1,330 @@
|
||||
import type { Dispatch } from 'react';
|
||||
import type ReactPlayer from 'react-player';
|
||||
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
|
||||
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,
|
||||
usePlayerMuted,
|
||||
usePlayerProperties,
|
||||
usePlayerVolume,
|
||||
} from '/@/renderer/store';
|
||||
import { PlayerStatus, PlayerStyle } from '/@/shared/types/types';
|
||||
|
||||
const PLAY_PAUSE_FADE_DURATION = 300;
|
||||
const PLAY_PAUSE_FADE_INTERVAL = 10;
|
||||
|
||||
export function WebPlayer() {
|
||||
const playerRef = useRef<WebPlayerEngineHandle>(null);
|
||||
const { num, player1, player2, status } = usePlayerData();
|
||||
const { mediaAutoNext, setTimestamp } = usePlayerActions();
|
||||
const { crossfadeDuration, speed, transitionType } = usePlayerProperties();
|
||||
const isMuted = usePlayerMuted();
|
||||
const volume = usePlayerVolume();
|
||||
|
||||
const [localPlayerStatus, setLocalPlayerStatus] = useState<PlayerStatus>(status);
|
||||
const [isTransitioning, setIsTransitioning] = useState<boolean | string>(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 onProgressPlayer1 = useCallback(
|
||||
(e: PlayerOnProgressProps) => {
|
||||
if (transitionType === 'crossfade' && num === 1) {
|
||||
setTimestamp(Number(e.playedSeconds.toFixed(0)));
|
||||
} else if (transitionType === 'gapless') {
|
||||
setTimestamp(Number(e.playedSeconds.toFixed(0)));
|
||||
}
|
||||
|
||||
if (!playerRef.current?.player1()) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (transitionType) {
|
||||
case PlayerStyle.CROSSFADE:
|
||||
crossfadeHandler({
|
||||
crossfadeDuration: crossfadeDuration,
|
||||
currentPlayer: playerRef.current.player1(),
|
||||
currentPlayerNum: num,
|
||||
currentTime: e.playedSeconds,
|
||||
duration: getDuration(playerRef.current.player1().ref),
|
||||
isTransitioning,
|
||||
nextPlayer: playerRef.current.player2(),
|
||||
playerNum: 1,
|
||||
setIsTransitioning,
|
||||
volume,
|
||||
});
|
||||
break;
|
||||
case PlayerStyle.GAPLESS:
|
||||
gaplessHandler({
|
||||
currentTime: e.playedSeconds,
|
||||
duration: getDuration(playerRef.current.player1().ref),
|
||||
isFlac: false,
|
||||
isTransitioning,
|
||||
nextPlayer: playerRef.current.player2(),
|
||||
setIsTransitioning,
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
[crossfadeDuration, isTransitioning, num, setTimestamp, transitionType, volume],
|
||||
);
|
||||
|
||||
const onProgressPlayer2 = useCallback(
|
||||
(e: PlayerOnProgressProps) => {
|
||||
if (transitionType === PlayerStyle.CROSSFADE && num === 2) {
|
||||
setTimestamp(Number(e.playedSeconds.toFixed(0)));
|
||||
} else if (transitionType === PlayerStyle.GAPLESS) {
|
||||
setTimestamp(Number(e.playedSeconds.toFixed(0)));
|
||||
}
|
||||
|
||||
if (!playerRef.current?.player2()) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (transitionType) {
|
||||
case PlayerStyle.CROSSFADE:
|
||||
crossfadeHandler({
|
||||
crossfadeDuration: crossfadeDuration,
|
||||
currentPlayer: playerRef.current.player2(),
|
||||
currentPlayerNum: num,
|
||||
currentTime: e.playedSeconds,
|
||||
duration: getDuration(playerRef.current.player2().ref),
|
||||
isTransitioning,
|
||||
nextPlayer: playerRef.current.player1(),
|
||||
playerNum: 2,
|
||||
setIsTransitioning,
|
||||
volume,
|
||||
});
|
||||
break;
|
||||
case PlayerStyle.GAPLESS:
|
||||
gaplessHandler({
|
||||
currentTime: e.playedSeconds,
|
||||
duration: getDuration(playerRef.current.player2().ref),
|
||||
isFlac: false,
|
||||
isTransitioning,
|
||||
nextPlayer: playerRef.current.player1(),
|
||||
setIsTransitioning,
|
||||
});
|
||||
break;
|
||||
}
|
||||
},
|
||||
[crossfadeDuration, isTransitioning, num, setTimestamp, transitionType, volume],
|
||||
);
|
||||
|
||||
const handleOnEndedPlayer1 = useCallback(() => {
|
||||
const promise = new Promise((resolve) => {
|
||||
mediaAutoNext();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
promise.then(() => {
|
||||
playerRef.current?.player1()?.ref?.getInternalPlayer().pause();
|
||||
playerRef.current?.setVolume(volume);
|
||||
setIsTransitioning(false);
|
||||
});
|
||||
}, [mediaAutoNext, volume]);
|
||||
|
||||
const handleOnEndedPlayer2 = useCallback(() => {
|
||||
const promise = new Promise((resolve) => {
|
||||
mediaAutoNext();
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
promise.then(() => {
|
||||
playerRef.current?.player2()?.ref?.getInternalPlayer().pause();
|
||||
playerRef.current?.setVolume(volume);
|
||||
setIsTransitioning(false);
|
||||
});
|
||||
}, [mediaAutoNext, volume]);
|
||||
|
||||
usePlayerEvents(
|
||||
{
|
||||
onPlayerSeekToTimestamp: (properties) => {
|
||||
const timestamp = properties.timestamp;
|
||||
if (num === 1) {
|
||||
playerRef.current?.player1()?.ref?.seekTo(timestamp);
|
||||
} else {
|
||||
playerRef.current?.player2()?.ref?.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, num, isTransitioning],
|
||||
);
|
||||
|
||||
useMainPlayerListener();
|
||||
|
||||
return (
|
||||
<WebPlayerEngine
|
||||
isMuted={isMuted}
|
||||
isTransitioning={Boolean(isTransitioning)}
|
||||
onEndedPlayer1={handleOnEndedPlayer1}
|
||||
onEndedPlayer2={handleOnEndedPlayer2}
|
||||
onProgressPlayer1={onProgressPlayer1}
|
||||
onProgressPlayer2={onProgressPlayer2}
|
||||
playerNum={num}
|
||||
playerRef={playerRef}
|
||||
playerStatus={localPlayerStatus}
|
||||
speed={speed}
|
||||
src1={player1?.streamUrl}
|
||||
src2={player2?.streamUrl}
|
||||
volume={volume}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function crossfadeHandler(args: {
|
||||
crossfadeDuration: number;
|
||||
currentPlayer: {
|
||||
ref: null | ReactPlayer;
|
||||
setVolume: (volume: number) => void;
|
||||
};
|
||||
currentPlayerNum: number;
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
isTransitioning: boolean | string;
|
||||
nextPlayer: {
|
||||
ref: null | ReactPlayer;
|
||||
setVolume: (volume: number) => void;
|
||||
};
|
||||
playerNum: number;
|
||||
setIsTransitioning: Dispatch<boolean | string>;
|
||||
volume: number;
|
||||
}) {
|
||||
const {
|
||||
crossfadeDuration,
|
||||
currentPlayer,
|
||||
currentPlayerNum,
|
||||
currentTime,
|
||||
duration,
|
||||
isTransitioning,
|
||||
nextPlayer,
|
||||
playerNum,
|
||||
setIsTransitioning,
|
||||
volume,
|
||||
} = args;
|
||||
const player = `player${playerNum}`;
|
||||
|
||||
if (!isTransitioning) {
|
||||
if (currentTime > duration - crossfadeDuration) {
|
||||
nextPlayer.setVolume(0);
|
||||
nextPlayer.ref?.getInternalPlayer().play();
|
||||
return setIsTransitioning(player);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isTransitioning !== player && currentPlayerNum !== playerNum) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeLeft = duration - currentTime;
|
||||
|
||||
// Calculate the volume levels based on time remaining
|
||||
const currentPlayerVolume = (timeLeft / crossfadeDuration) * volume;
|
||||
const nextPlayerVolume = ((crossfadeDuration - timeLeft) / crossfadeDuration) * volume;
|
||||
|
||||
// Set volumes for both players
|
||||
currentPlayer.setVolume(currentPlayerVolume);
|
||||
nextPlayer.setVolume(nextPlayerVolume);
|
||||
}
|
||||
|
||||
function gaplessHandler(args: {
|
||||
currentTime: number;
|
||||
duration: number;
|
||||
isFlac: boolean;
|
||||
isTransitioning: boolean | string;
|
||||
nextPlayer: {
|
||||
ref: null | ReactPlayer;
|
||||
setVolume: (volume: number) => void;
|
||||
};
|
||||
setIsTransitioning: Dispatch<boolean | string>;
|
||||
}) {
|
||||
const { currentTime, duration, isFlac, isTransitioning, nextPlayer, setIsTransitioning } = args;
|
||||
|
||||
if (!isTransitioning) {
|
||||
if (currentTime > duration - 2) {
|
||||
return setIsTransitioning(true);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const durationPadding = getDurationPadding(isFlac);
|
||||
|
||||
if (currentTime + durationPadding >= duration) {
|
||||
return nextPlayer.ref
|
||||
?.getInternalPlayer()
|
||||
?.play()
|
||||
.catch(() => {});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function getDuration(ref: null | ReactPlayer | undefined) {
|
||||
return ref?.getInternalPlayer()?.duration || 0;
|
||||
}
|
||||
|
||||
function getDurationPadding(isFlac: boolean) {
|
||||
switch (isFlac) {
|
||||
case false:
|
||||
return 0.116;
|
||||
case true:
|
||||
return 0.065;
|
||||
}
|
||||
}
|
||||
@@ -4,31 +4,6 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
width: 95%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.slider-value-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
justify-content: center;
|
||||
max-width: 50px;
|
||||
|
||||
@media (width < 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.slider-wrapper {
|
||||
display: flex;
|
||||
flex: 6;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.controls-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import { useHotkeys } from '@mantine/hooks';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import formatDuration from 'format-duration';
|
||||
import isElectron from 'is-electron';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import styles from './center-controls.module.css';
|
||||
@@ -10,129 +6,48 @@ import styles from './center-controls.module.css';
|
||||
import { PlayButton, PlayerButton } from '/@/renderer/features/player/components/player-button';
|
||||
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
|
||||
import { openShuffleAllModal } from '/@/renderer/features/player/components/shuffle-all-modal';
|
||||
import { useCenterControls } from '/@/renderer/features/player/hooks/use-center-controls';
|
||||
import { useMediaSession } from '/@/renderer/features/player/hooks/use-media-session';
|
||||
import { usePlayerContext } from '/@/renderer/features/player/context/player-context';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
|
||||
import {
|
||||
useAppStore,
|
||||
useAppStoreActions,
|
||||
useCurrentPlayer,
|
||||
useCurrentSong,
|
||||
useCurrentStatus,
|
||||
useCurrentTime,
|
||||
useHotkeySettings,
|
||||
usePlaybackType,
|
||||
useRepeatStatus,
|
||||
useSetCurrentTime,
|
||||
usePlayerRepeat,
|
||||
usePlayerShuffle,
|
||||
usePlayerSong,
|
||||
usePlayerStatus,
|
||||
useSettingsStore,
|
||||
useShuffleStatus,
|
||||
} from '/@/renderer/store';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
|
||||
import { PlaybackType, PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/shared/types/types';
|
||||
import { PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
interface CenterControlsProps {
|
||||
playersRef: any;
|
||||
}
|
||||
|
||||
export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
export const CenterControls = () => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const [isSeeking, setIsSeeking] = useState(false);
|
||||
const currentSong = useCurrentSong();
|
||||
const currentSong = usePlayerSong();
|
||||
const skip = useSettingsStore((state) => state.general.skipButtons);
|
||||
const buttonSize = useSettingsStore((state) => state.general.buttonSize);
|
||||
const playbackType = usePlaybackType();
|
||||
const player1 = playersRef?.current?.player1;
|
||||
const player2 = playersRef?.current?.player2;
|
||||
const status = useCurrentStatus();
|
||||
const player = useCurrentPlayer();
|
||||
const setCurrentTime = useSetCurrentTime();
|
||||
const repeat = useRepeatStatus();
|
||||
const shuffle = useShuffleStatus();
|
||||
const { bindings } = useHotkeySettings();
|
||||
const { showTimeRemaining } = useAppStore();
|
||||
const { setShowTimeRemaining } = useAppStoreActions();
|
||||
const status = usePlayerStatus();
|
||||
const repeat = usePlayerRepeat();
|
||||
const shuffle = usePlayerShuffle();
|
||||
|
||||
const {
|
||||
handleNextTrack,
|
||||
handlePause,
|
||||
handlePlay,
|
||||
handlePlayPause,
|
||||
handlePrevTrack,
|
||||
handleSeekSlider,
|
||||
handleSkipBackward,
|
||||
handleSkipForward,
|
||||
handleStop,
|
||||
handleToggleRepeat,
|
||||
handleToggleShuffle,
|
||||
} = useCenterControls({ playersRef });
|
||||
mediaNext,
|
||||
mediaPrevious,
|
||||
mediaSkipBackward,
|
||||
mediaSkipForward,
|
||||
mediaStop,
|
||||
mediaTogglePlayPause,
|
||||
toggleRepeat,
|
||||
toggleShuffle,
|
||||
} = usePlayerContext();
|
||||
|
||||
const handlePlayQueueAdd = usePlayQueueAdd();
|
||||
|
||||
const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0;
|
||||
const currentTime = useCurrentTime();
|
||||
const currentPlayerRef = player === 1 ? player1 : player2;
|
||||
const formattedDuration = formatDuration(songDuration * 1000 || 0);
|
||||
const formattedTimeRemaining = formatDuration((currentTime - songDuration) * 1000 || 0);
|
||||
const formattedTime = formatDuration(currentTime * 1000 || 0);
|
||||
|
||||
useEffect(() => {
|
||||
let interval: ReturnType<typeof setInterval>;
|
||||
|
||||
if (status === PlayerStatus.PLAYING && !isSeeking) {
|
||||
if (!isElectron() || playbackType === PlaybackType.WEB) {
|
||||
// Update twice a second for slightly better performance
|
||||
interval = setInterval(() => {
|
||||
if (currentPlayerRef) {
|
||||
setCurrentTime(currentPlayerRef.getCurrentTime());
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, [currentPlayerRef, isSeeking, setCurrentTime, playbackType, status]);
|
||||
|
||||
const [seekValue, setSeekValue] = useState(0);
|
||||
|
||||
useHotkeys([
|
||||
[bindings.playPause.isGlobal ? '' : bindings.playPause.hotkey, handlePlayPause],
|
||||
[bindings.play.isGlobal ? '' : bindings.play.hotkey, handlePlay],
|
||||
[bindings.pause.isGlobal ? '' : bindings.pause.hotkey, handlePause],
|
||||
[bindings.stop.isGlobal ? '' : bindings.stop.hotkey, handleStop],
|
||||
[bindings.next.isGlobal ? '' : bindings.next.hotkey, handleNextTrack],
|
||||
[bindings.previous.isGlobal ? '' : bindings.previous.hotkey, handlePrevTrack],
|
||||
[bindings.toggleRepeat.isGlobal ? '' : bindings.toggleRepeat.hotkey, handleToggleRepeat],
|
||||
[bindings.toggleShuffle.isGlobal ? '' : bindings.toggleShuffle.hotkey, handleToggleShuffle],
|
||||
[
|
||||
bindings.skipBackward.isGlobal ? '' : bindings.skipBackward.hotkey,
|
||||
() => handleSkipBackward(skip?.skipBackwardSeconds || 5),
|
||||
],
|
||||
[
|
||||
bindings.skipForward.isGlobal ? '' : bindings.skipForward.hotkey,
|
||||
() => handleSkipForward(skip?.skipForwardSeconds || 5),
|
||||
],
|
||||
]);
|
||||
|
||||
useMediaSession({
|
||||
handleNextTrack,
|
||||
handlePause,
|
||||
handlePlay,
|
||||
handlePrevTrack,
|
||||
handleSeekSlider,
|
||||
handleSkipBackward,
|
||||
handleSkipForward,
|
||||
handleStop,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.controlsContainer}>
|
||||
<div className={styles.buttonsContainer}>
|
||||
<PlayerButton
|
||||
icon={<Icon fill="default" icon="mediaStop" size={buttonSize - 2} />}
|
||||
onClick={handleStop}
|
||||
onClick={mediaStop}
|
||||
tooltip={{
|
||||
label: t('player.stop', { postProcess: 'sentenceCase' }),
|
||||
openDelay: 0,
|
||||
@@ -148,7 +63,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
/>
|
||||
}
|
||||
isActive={shuffle !== PlayerShuffle.NONE}
|
||||
onClick={handleToggleShuffle}
|
||||
onClick={toggleShuffle}
|
||||
tooltip={{
|
||||
label:
|
||||
shuffle === PlayerShuffle.NONE
|
||||
@@ -163,7 +78,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
/>
|
||||
<PlayerButton
|
||||
icon={<Icon fill="default" icon="mediaPrevious" size={buttonSize} />}
|
||||
onClick={handlePrevTrack}
|
||||
onClick={mediaPrevious}
|
||||
tooltip={{
|
||||
label: t('player.previous', { postProcess: 'sentenceCase' }),
|
||||
openDelay: 0,
|
||||
@@ -175,7 +90,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
icon={
|
||||
<Icon fill="default" icon="mediaStepBackward" size={buttonSize} />
|
||||
}
|
||||
onClick={() => handleSkipBackward(skip?.skipBackwardSeconds)}
|
||||
onClick={mediaSkipBackward}
|
||||
tooltip={{
|
||||
label: t('player.skip', {
|
||||
context: 'back',
|
||||
@@ -190,12 +105,12 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
<PlayButton
|
||||
disabled={currentSong?.id === undefined}
|
||||
isPaused={status === PlayerStatus.PAUSED}
|
||||
onClick={handlePlayPause}
|
||||
onClick={mediaTogglePlayPause}
|
||||
/>
|
||||
{skip?.enabled && (
|
||||
<PlayerButton
|
||||
icon={<Icon fill="default" icon="mediaStepForward" size={buttonSize} />}
|
||||
onClick={() => handleSkipForward(skip?.skipForwardSeconds)}
|
||||
onClick={mediaSkipForward}
|
||||
tooltip={{
|
||||
label: t('player.skip', {
|
||||
context: 'forward',
|
||||
@@ -209,7 +124,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
)}
|
||||
<PlayerButton
|
||||
icon={<Icon fill="default" icon="mediaNext" size={buttonSize} />}
|
||||
onClick={handleNextTrack}
|
||||
onClick={mediaNext}
|
||||
tooltip={{
|
||||
label: t('player.next', { postProcess: 'sentenceCase' }),
|
||||
openDelay: 0,
|
||||
@@ -229,7 +144,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
)
|
||||
}
|
||||
isActive={repeat !== PlayerRepeat.NONE}
|
||||
onClick={handleToggleRepeat}
|
||||
onClick={toggleRepeat}
|
||||
tooltip={{
|
||||
label: `${
|
||||
repeat === PlayerRepeat.NONE
|
||||
@@ -267,57 +182,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.sliderContainer}>
|
||||
<div className={styles.sliderValueWrapper}>
|
||||
<Text
|
||||
className={PlaybackSelectors.elapsedTime}
|
||||
fw={600}
|
||||
isMuted
|
||||
isNoSelect
|
||||
size="xs"
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.sliderWrapper}>
|
||||
<PlayerbarSlider
|
||||
label={(value) => formatDuration(value * 1000)}
|
||||
max={songDuration}
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
setIsSeeking(true);
|
||||
setSeekValue(e);
|
||||
}}
|
||||
onChangeEnd={(e) => {
|
||||
// There is a timing bug in Mantine in which the onChangeEnd
|
||||
// event fires before onChange. Add a small delay to force
|
||||
// onChangeEnd to happen after onCHange
|
||||
setTimeout(() => {
|
||||
handleSeekSlider(e);
|
||||
setIsSeeking(false);
|
||||
}, 50);
|
||||
}}
|
||||
size={6}
|
||||
value={!isSeeking ? currentTime : seekValue}
|
||||
w="100%"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.sliderValueWrapper}>
|
||||
<Text
|
||||
className={PlaybackSelectors.totalDuration}
|
||||
fw={600}
|
||||
isMuted
|
||||
isNoSelect
|
||||
onClick={() => setShowTimeRemaining(!showTimeRemaining)}
|
||||
role="button"
|
||||
size="xs"
|
||||
style={{ cursor: 'pointer', userSelect: 'none' }}
|
||||
>
|
||||
{showTimeRemaining ? formattedTimeRemaining : formattedDuration}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
<PlayerbarSlider />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
} from '/@/renderer/store/full-screen-player.store';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { PlaybackType } from '/@/shared/types/types';
|
||||
import { PlayerType } from '/@/shared/types/types';
|
||||
|
||||
const Visualizer = lazy(() =>
|
||||
import('/@/renderer/features/player/components/visualizer').then((module) => ({
|
||||
@@ -48,7 +48,7 @@ export const FullScreenPlayerQueue = () => {
|
||||
},
|
||||
];
|
||||
|
||||
if (type === PlaybackType.WEB && webAudio) {
|
||||
if (type === PlayerType.WEB && webAudio) {
|
||||
items.push({
|
||||
active: activeTab === 'visualizer',
|
||||
label: t('page.fullscreenPlayer.visualizer', { postProcess: 'titleCase' }),
|
||||
@@ -107,7 +107,7 @@ export const FullScreenPlayerQueue = () => {
|
||||
</div>
|
||||
) : activeTab === 'lyrics' ? (
|
||||
<Lyrics />
|
||||
) : activeTab === 'visualizer' && type === PlaybackType.WEB && webAudio ? (
|
||||
) : activeTab === 'visualizer' && type === PlayerType.WEB && webAudio ? (
|
||||
<Suspense fallback={<></>}>
|
||||
<Visualizer />
|
||||
</Suspense>
|
||||
|
||||
@@ -11,7 +11,7 @@ import { FullScreenPlayerImage } from '/@/renderer/features/player/components/fu
|
||||
import { FullScreenPlayerQueue } from '/@/renderer/features/player/components/full-screen-player-queue';
|
||||
import { useFastAverageColor } from '/@/renderer/hooks';
|
||||
import {
|
||||
useCurrentSong,
|
||||
usePlayerSong,
|
||||
useFullScreenPlayerStore,
|
||||
useFullScreenPlayerStoreActions,
|
||||
useLyricsSettings,
|
||||
@@ -421,7 +421,7 @@ export const FullScreenPlayer = () => {
|
||||
isOpenedRef.current = true;
|
||||
}, [location, setStore]);
|
||||
|
||||
const currentSong = useCurrentSong();
|
||||
const currentSong = usePlayerSong();
|
||||
const { background } = useFastAverageColor({
|
||||
algorithm: 'dominant',
|
||||
src: currentSong?.imageUrl,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { SimilarSongsList } from '/@/renderer/features/similar-songs/components/similar-songs-list';
|
||||
import { useCurrentSong } from '/@/renderer/store';
|
||||
import { usePlayerSong } from '/@/renderer/store';
|
||||
|
||||
export const FullScreenSimilarSongs = () => {
|
||||
const currentSong = useCurrentSong();
|
||||
const currentSong = usePlayerSong();
|
||||
|
||||
return currentSong?.id ? <SimilarSongsList fullScreen song={currentSong} /> : null;
|
||||
};
|
||||
|
||||
@@ -12,9 +12,9 @@ import { useHandleGeneralContextMenu } from '/@/renderer/features/context-menu/h
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import {
|
||||
useAppStoreActions,
|
||||
useCurrentSong,
|
||||
useFullScreenPlayerStore,
|
||||
useHotkeySettings,
|
||||
usePlayerSong,
|
||||
useSetFullScreenPlayerStore,
|
||||
useSidebarStore,
|
||||
} from '/@/renderer/store';
|
||||
@@ -34,7 +34,7 @@ export const LeftControls = () => {
|
||||
const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
|
||||
const { collapsed, image } = useSidebarStore();
|
||||
const hideImage = image && !collapsed;
|
||||
const currentSong = useCurrentSong();
|
||||
const currentSong = usePlayerSong();
|
||||
const title = currentSong?.name;
|
||||
const artists = currentSong?.artists;
|
||||
const { bindings } = useHotkeySettings();
|
||||
|
||||
@@ -49,3 +49,28 @@
|
||||
right: calc(0.1rem * -1);
|
||||
}
|
||||
}
|
||||
|
||||
.slider-container {
|
||||
display: flex;
|
||||
width: 95%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.slider-value-wrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
justify-content: center;
|
||||
max-width: 50px;
|
||||
|
||||
@media (width < 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.slider-wrapper {
|
||||
display: flex;
|
||||
flex: 6;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,128 @@
|
||||
import formatDuration from 'format-duration';
|
||||
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 { useRemote } from '/@/renderer/features/remote/hooks/use-remote';
|
||||
import {
|
||||
useAppStore,
|
||||
useAppStoreActions,
|
||||
usePlaybackType,
|
||||
usePlayerSong,
|
||||
usePlayerTimestamp,
|
||||
} from '/@/renderer/store';
|
||||
import { Slider, SliderProps } from '/@/shared/components/slider/slider';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
|
||||
import { PlayerType } from '/@/shared/types/types';
|
||||
|
||||
export const PlayerbarSlider = ({ ...props }: SliderProps) => {
|
||||
const playbackType = usePlaybackType();
|
||||
const currentSong = usePlayerSong();
|
||||
|
||||
const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0;
|
||||
const [isSeeking, setIsSeeking] = useState(false);
|
||||
const [seekValue, setSeekValue] = useState(0);
|
||||
const currentTime = usePlayerTimestamp();
|
||||
const seekTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const formattedDuration = formatDuration(songDuration * 1000 || 0);
|
||||
const formattedTimeRemaining = formatDuration((currentTime - songDuration) * 1000 || 0);
|
||||
const formattedTime = formatDuration(currentTime * 1000 || 0);
|
||||
|
||||
const { showTimeRemaining } = useAppStore();
|
||||
const { setShowTimeRemaining } = useAppStoreActions();
|
||||
|
||||
const { mediaSeekToTimestamp } = usePlayerContext();
|
||||
|
||||
const handleSeekToTimestamp = (timestamp: number) => {
|
||||
mediaSeekToTimestamp(timestamp);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (seekTimeoutRef.current) {
|
||||
clearTimeout(seekTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useRemote();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.sliderContainer}>
|
||||
<div className={styles.sliderValueWrapper}>
|
||||
<Text
|
||||
className={PlaybackSelectors.elapsedTime}
|
||||
fw={600}
|
||||
isMuted
|
||||
isNoSelect
|
||||
size="xs"
|
||||
style={{ userSelect: 'none' }}
|
||||
>
|
||||
{formattedTime}
|
||||
</Text>
|
||||
</div>
|
||||
<div className={styles.sliderWrapper}>
|
||||
<CustomPlayerbarSlider
|
||||
{...props}
|
||||
label={(value) => formatDuration(value * 1000)}
|
||||
max={songDuration}
|
||||
min={0}
|
||||
onChange={(e) => {
|
||||
// Cancel any pending timeout if user starts seeking again
|
||||
if (seekTimeoutRef.current) {
|
||||
clearTimeout(seekTimeoutRef.current);
|
||||
seekTimeoutRef.current = null;
|
||||
}
|
||||
setIsSeeking(true);
|
||||
setSeekValue(e);
|
||||
}}
|
||||
onChangeEnd={(e) => {
|
||||
setSeekValue(e);
|
||||
handleSeekToTimestamp(e);
|
||||
|
||||
// Delay resetting isSeeking to allow currentTime to catch up
|
||||
// This prevents the slider from flickering back and forth
|
||||
seekTimeoutRef.current = setTimeout(() => {
|
||||
setIsSeeking(false);
|
||||
seekTimeoutRef.current = null;
|
||||
}, 300);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e?.stopPropagation();
|
||||
}}
|
||||
size={6}
|
||||
value={!isSeeking ? currentTime : seekValue}
|
||||
w="100%"
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.sliderValueWrapper}>
|
||||
<Text
|
||||
className={PlaybackSelectors.totalDuration}
|
||||
fw={600}
|
||||
isMuted
|
||||
isNoSelect
|
||||
onClick={() => setShowTimeRemaining(!showTimeRemaining)}
|
||||
role="button"
|
||||
size="xs"
|
||||
style={{ cursor: 'pointer', userSelect: 'none' }}
|
||||
>
|
||||
{showTimeRemaining ? formattedTimeRemaining : formattedDuration}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
{playbackType === PlayerType.WEB && <WebPlayer />}
|
||||
{playbackType === PlayerType.LOCAL && <MpvPlayer />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const CustomPlayerbarSlider = ({ ...props }: SliderProps) => {
|
||||
return (
|
||||
<Slider
|
||||
classNames={{
|
||||
@@ -13,9 +133,7 @@ export const PlayerbarSlider = ({ ...props }: SliderProps) => {
|
||||
track: styles.track,
|
||||
}}
|
||||
{...props}
|
||||
onClick={(e) => {
|
||||
e?.stopPropagation();
|
||||
}}
|
||||
size={6}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,46 +1,18 @@
|
||||
import clsx from 'clsx';
|
||||
import { MouseEvent, useCallback } from 'react';
|
||||
import { MouseEvent } from 'react';
|
||||
|
||||
import styles from './playerbar.module.css';
|
||||
|
||||
import { AudioPlayer } from '/@/renderer/components/audio-player';
|
||||
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 {
|
||||
useCurrentPlayer,
|
||||
useCurrentStatus,
|
||||
useFullScreenPlayerStore,
|
||||
useMuted,
|
||||
usePlayer1Data,
|
||||
usePlayer2Data,
|
||||
usePlayerControls,
|
||||
useSetFullScreenPlayerStore,
|
||||
useVolume,
|
||||
} from '/@/renderer/store';
|
||||
import {
|
||||
useGeneralSettings,
|
||||
usePlaybackType,
|
||||
useSettingsStore,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { useFullScreenPlayerStore, useSetFullScreenPlayerStore } from '/@/renderer/store';
|
||||
import { useGeneralSettings } from '/@/renderer/store/settings.store';
|
||||
import { PlaybackSelectors } from '/@/shared/constants/playback-selectors';
|
||||
import { PlaybackType } from '/@/shared/types/types';
|
||||
|
||||
export const Playerbar = () => {
|
||||
const playersRef = PlayersRef;
|
||||
const settings = useSettingsStore((state) => state.playback);
|
||||
const { playerbarOpenDrawer } = useGeneralSettings();
|
||||
const playbackType = usePlaybackType();
|
||||
const volume = useVolume();
|
||||
const player1 = usePlayer1Data();
|
||||
const player2 = usePlayer2Data();
|
||||
const status = useCurrentStatus();
|
||||
const player = useCurrentPlayer();
|
||||
const muted = useMuted();
|
||||
const { autoNext } = usePlayerControls();
|
||||
const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore();
|
||||
const setFullScreenPlayerStore = useSetFullScreenPlayerStore();
|
||||
|
||||
@@ -51,11 +23,6 @@ export const Playerbar = () => {
|
||||
setFullScreenPlayerStore({ expanded: !isFullScreenPlayerExpanded });
|
||||
};
|
||||
|
||||
const autoNextFn = useCallback(() => {
|
||||
const playerData = autoNext();
|
||||
updateSong(playerData.current.song);
|
||||
}, [autoNext]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.container, PlaybackSelectors.mediaPlayer)}
|
||||
@@ -66,15 +33,15 @@ export const Playerbar = () => {
|
||||
<LeftControls />
|
||||
</div>
|
||||
<div className={styles.centerGridItem}>
|
||||
<CenterControls playersRef={playersRef} />
|
||||
<CenterControls />
|
||||
</div>
|
||||
<div className={styles.rightGridItem}>
|
||||
<RightControls />
|
||||
</div>
|
||||
</div>
|
||||
{playbackType === PlaybackType.WEB && (
|
||||
{/* {playbackType === PlayerType.WEB && (
|
||||
<AudioPlayer
|
||||
autoNext={autoNextFn}
|
||||
// autoNext={autoNextFn}
|
||||
crossfadeDuration={settings.crossfadeDuration}
|
||||
crossfadeStyle={settings.crossfadeStyle}
|
||||
currentPlayer={player}
|
||||
@@ -87,7 +54,7 @@ export const Playerbar = () => {
|
||||
style={settings.style as any}
|
||||
volume={(volume / 100) ** 2}
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import { useHotkeys, useMediaQuery } from '@mantine/hooks';
|
||||
import isElectron from 'is-electron';
|
||||
import { useEffect } from 'react';
|
||||
import { useCallback, useEffect, WheelEvent } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
|
||||
import { useRightControls } from '/@/renderer/features/player/hooks/use-right-controls';
|
||||
import { CustomPlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
|
||||
import { usePlayerContext } from '/@/renderer/features/player/context/player-context';
|
||||
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||
import { useSetRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
||||
import {
|
||||
useAppStoreActions,
|
||||
useCurrentServer,
|
||||
useCurrentSong,
|
||||
useGeneralSettings,
|
||||
useHotkeySettings,
|
||||
useMuted,
|
||||
usePlaybackSettings,
|
||||
usePlaybackType,
|
||||
usePreviousSong,
|
||||
usePlayerData,
|
||||
usePlayerMuted,
|
||||
usePlayerSpeed,
|
||||
usePlayerVolume,
|
||||
useSettingsStore,
|
||||
useSettingsStoreActions,
|
||||
useSidebarStore,
|
||||
useSpeed,
|
||||
useVolume,
|
||||
} from '/@/renderer/store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||
@@ -32,38 +32,53 @@ import { Rating } from '/@/shared/components/rating/rating';
|
||||
import { Slider } from '/@/shared/components/slider/slider';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { LibraryItem, QueueSong, ServerType, Song } from '/@/shared/types/domain-types';
|
||||
import { PlaybackType } from '/@/shared/types/types';
|
||||
import { PlayerType } from '/@/shared/types/types';
|
||||
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
const remote = isElectron() ? window.api.remote : null;
|
||||
|
||||
const calculateVolumeUp = (volume: number, volumeWheelStep: number) => {
|
||||
let volumeToSet;
|
||||
const newVolumeGreaterThanHundred = volume + volumeWheelStep > 100;
|
||||
if (newVolumeGreaterThanHundred) {
|
||||
volumeToSet = 100;
|
||||
} else {
|
||||
volumeToSet = volume + volumeWheelStep;
|
||||
}
|
||||
|
||||
return volumeToSet;
|
||||
};
|
||||
|
||||
const calculateVolumeDown = (volume: number, volumeWheelStep: number) => {
|
||||
let volumeToSet;
|
||||
const newVolumeLessThanZero = volume - volumeWheelStep < 0;
|
||||
if (newVolumeLessThanZero) {
|
||||
volumeToSet = 0;
|
||||
} else {
|
||||
volumeToSet = volume - volumeWheelStep;
|
||||
}
|
||||
|
||||
return volumeToSet;
|
||||
};
|
||||
|
||||
export const RightControls = () => {
|
||||
const { t } = useTranslation();
|
||||
const isMinWidth = useMediaQuery('(max-width: 480px)');
|
||||
const volume = useVolume();
|
||||
const muted = useMuted();
|
||||
const volume = usePlayerVolume();
|
||||
const muted = usePlayerMuted();
|
||||
const server = useCurrentServer();
|
||||
const currentSong = useCurrentSong();
|
||||
const previousSong = usePreviousSong();
|
||||
const { currentSong, previousSong } = usePlayerData();
|
||||
const { setSideBar } = useAppStoreActions();
|
||||
const { rightExpanded: isQueueExpanded } = useSidebarStore();
|
||||
const { bindings } = useHotkeySettings();
|
||||
const {
|
||||
handleMute,
|
||||
handleSpeed,
|
||||
handleVolumeDown,
|
||||
handleVolumeSlider,
|
||||
handleVolumeUp,
|
||||
handleVolumeWheel,
|
||||
} = useRightControls();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const playbackSettings = usePlaybackSettings();
|
||||
const playbackType = usePlaybackType();
|
||||
|
||||
const speed = useSpeed();
|
||||
const { volumeWheelStep } = useGeneralSettings();
|
||||
const speed = usePlayerSpeed();
|
||||
const volumeWidth = useSettingsStore((state) => state.general.volumeWidth);
|
||||
const speedPreservePitch = useSettingsStore((state) => state.playback.preservePitch);
|
||||
|
||||
const { mediaToggleMute, setSpeed, setVolume } = usePlayerContext();
|
||||
const updateRatingMutation = useSetRating({});
|
||||
const addToFavoritesMutation = useCreateFavorite({});
|
||||
const removeFromFavoritesMutation = useDeleteFavorite({});
|
||||
@@ -72,7 +87,7 @@ export const RightControls = () => {
|
||||
if (!song?.id) return;
|
||||
|
||||
addToFavoritesMutation.mutate({
|
||||
apiClientProps: { serverId: song?.serverId || '' },
|
||||
apiClientProps: { serverId: song?._serverId || '' },
|
||||
query: {
|
||||
id: [song.id],
|
||||
type: LibraryItem.SONG,
|
||||
@@ -84,7 +99,7 @@ export const RightControls = () => {
|
||||
if (!currentSong) return;
|
||||
|
||||
updateRatingMutation.mutate({
|
||||
apiClientProps: { serverId: currentSong?.serverId || '' },
|
||||
apiClientProps: { serverId: currentSong?._serverId || '' },
|
||||
query: {
|
||||
item: [currentSong],
|
||||
rating,
|
||||
@@ -96,7 +111,7 @@ export const RightControls = () => {
|
||||
if (!song?.id) return;
|
||||
|
||||
removeFromFavoritesMutation.mutate({
|
||||
apiClientProps: { serverId: song?.serverId || '' },
|
||||
apiClientProps: { serverId: song?._serverId || '' },
|
||||
query: {
|
||||
id: [song.id],
|
||||
type: LibraryItem.SONG,
|
||||
@@ -114,6 +129,46 @@ export const RightControls = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleVolumeDown = useCallback(() => {
|
||||
setVolume(volume - 1);
|
||||
}, [setVolume, volume]);
|
||||
|
||||
const handleVolumeUp = useCallback(() => {
|
||||
setVolume(volume + 1);
|
||||
}, [setVolume, volume]);
|
||||
|
||||
const handleMute = useCallback(() => {
|
||||
mediaToggleMute();
|
||||
}, [mediaToggleMute]);
|
||||
|
||||
const handleSpeed = useCallback(
|
||||
(e: number) => {
|
||||
setSpeed(e);
|
||||
},
|
||||
[setSpeed],
|
||||
);
|
||||
|
||||
const handleVolumeSlider = useCallback(
|
||||
(e: number) => {
|
||||
setVolume(e);
|
||||
},
|
||||
[setVolume],
|
||||
);
|
||||
|
||||
const handleVolumeWheel = useCallback(
|
||||
(e: WheelEvent<HTMLButtonElement | HTMLDivElement>) => {
|
||||
let volumeToSet;
|
||||
if (e.deltaY > 0 || e.deltaX > 0) {
|
||||
volumeToSet = calculateVolumeDown(volume, volumeWheelStep);
|
||||
} else {
|
||||
volumeToSet = calculateVolumeUp(volume, volumeWheelStep);
|
||||
}
|
||||
|
||||
setVolume(volumeToSet);
|
||||
},
|
||||
[setVolume, volume, volumeWheelStep],
|
||||
);
|
||||
|
||||
const handleToggleQueue = () => {
|
||||
setSideBar({ rightExpanded: !isQueueExpanded });
|
||||
};
|
||||
@@ -187,9 +242,9 @@ export const RightControls = () => {
|
||||
query: {
|
||||
item: [
|
||||
{
|
||||
_serverId: currentSong?._serverId || '',
|
||||
id,
|
||||
itemType: LibraryItem.SONG,
|
||||
serverId,
|
||||
} as Song, // This is not a type-safe cast, but it works because those are all the prop
|
||||
],
|
||||
rating,
|
||||
@@ -204,7 +259,12 @@ export const RightControls = () => {
|
||||
}
|
||||
|
||||
return () => {};
|
||||
}, [addToFavoritesMutation, removeFromFavoritesMutation, updateRatingMutation]);
|
||||
}, [
|
||||
addToFavoritesMutation,
|
||||
currentSong?._serverId,
|
||||
removeFromFavoritesMutation,
|
||||
updateRatingMutation,
|
||||
]);
|
||||
|
||||
return (
|
||||
<Flex align="flex-end" direction="column" h="100%" px="1rem" py="0.5rem">
|
||||
@@ -237,7 +297,7 @@ export const RightControls = () => {
|
||||
/>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{playbackType === PlaybackType.WEB && (
|
||||
{playbackType === PlayerType.WEB && (
|
||||
<Option>
|
||||
<Option.Label>
|
||||
{t('setting.preservePitch', {
|
||||
@@ -339,7 +399,7 @@ export const RightControls = () => {
|
||||
variant="subtle"
|
||||
/>
|
||||
{!isMinWidth ? (
|
||||
<PlayerbarSlider
|
||||
<CustomPlayerbarSlider
|
||||
max={100}
|
||||
min={0}
|
||||
onChange={handleVolumeSlider}
|
||||
|
||||
@@ -0,0 +1,467 @@
|
||||
import { QueryClient, useQueryClient } from '@tanstack/react-query';
|
||||
import { createContext, useCallback, useContext, useMemo } from 'react';
|
||||
|
||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||
import { AddToQueueType, usePlayerActions } from '/@/renderer/store';
|
||||
import {
|
||||
LibraryItem,
|
||||
PlaylistSongListResponse,
|
||||
QueueSong,
|
||||
Song,
|
||||
SongListResponse,
|
||||
SongListSort,
|
||||
SortOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { Play, PlayerRepeat, PlayerShuffle } from '/@/shared/types/types';
|
||||
|
||||
interface PlayerContext {
|
||||
addToQueueByData: (data: Song[], type: AddToQueueType) => void;
|
||||
addToQueueByFetch: (
|
||||
serverId: string,
|
||||
id: string[],
|
||||
itemType: LibraryItem,
|
||||
type: AddToQueueType,
|
||||
) => void;
|
||||
clearQueue: () => void;
|
||||
clearSelected: (items: QueueSong[]) => void;
|
||||
decreaseVolume: (amount: number) => void;
|
||||
increaseVolume: (amount: number) => void;
|
||||
mediaNext: () => void;
|
||||
mediaPause: () => void;
|
||||
mediaPlay: (id?: string) => void;
|
||||
mediaPrevious: () => void;
|
||||
mediaSeekToTimestamp: (timestamp: number) => void;
|
||||
mediaSkipBackward: () => void;
|
||||
mediaSkipForward: () => void;
|
||||
mediaStop: () => void;
|
||||
mediaToggleMute: () => void;
|
||||
mediaTogglePlayPause: () => void;
|
||||
moveSelectedTo: (items: QueueSong[], edge: 'bottom' | 'top', uniqueId: string) => void;
|
||||
moveSelectedToBottom: (items: QueueSong[]) => void;
|
||||
moveSelectedToNext: (items: QueueSong[]) => void;
|
||||
moveSelectedToTop: (items: QueueSong[]) => void;
|
||||
setRepeat: (repeat: PlayerRepeat) => void;
|
||||
setShuffle: (shuffle: PlayerShuffle) => void;
|
||||
setSpeed: (speed: number) => void;
|
||||
setVolume: (volume: number) => void;
|
||||
shuffle: () => void;
|
||||
shuffleAll: () => void;
|
||||
shuffleSelected: (items: QueueSong[]) => void;
|
||||
toggleRepeat: () => void;
|
||||
toggleShuffle: () => void;
|
||||
}
|
||||
|
||||
export const PlayerContext = createContext<PlayerContext>({
|
||||
addToQueueByData: () => {},
|
||||
addToQueueByFetch: () => {},
|
||||
clearQueue: () => {},
|
||||
clearSelected: () => {},
|
||||
decreaseVolume: () => {},
|
||||
increaseVolume: () => {},
|
||||
mediaNext: () => {},
|
||||
mediaPause: () => {},
|
||||
mediaPlay: () => {},
|
||||
mediaPrevious: () => {},
|
||||
mediaSeekToTimestamp: () => {},
|
||||
mediaSkipBackward: () => {},
|
||||
mediaSkipForward: () => {},
|
||||
mediaStop: () => {},
|
||||
mediaToggleMute: () => {},
|
||||
mediaTogglePlayPause: () => {},
|
||||
moveSelectedTo: () => {},
|
||||
moveSelectedToBottom: () => {},
|
||||
moveSelectedToNext: () => {},
|
||||
moveSelectedToTop: () => {},
|
||||
setRepeat: () => {},
|
||||
setShuffle: () => {},
|
||||
setSpeed: () => {},
|
||||
setVolume: () => {},
|
||||
shuffle: () => {},
|
||||
shuffleAll: () => {},
|
||||
shuffleSelected: () => {},
|
||||
toggleRepeat: () => {},
|
||||
toggleShuffle: () => {},
|
||||
});
|
||||
|
||||
export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const storeActions = usePlayerActions();
|
||||
|
||||
const addToQueueByData = useCallback(
|
||||
(data: Song[], type: AddToQueueType) => {
|
||||
if (typeof type === 'object' && 'edge' in type && type.edge !== null) {
|
||||
const edge = type.edge === 'top' ? 'top' : 'bottom';
|
||||
storeActions.addToQueueByUniqueId(data, type.uniqueId, edge);
|
||||
} else {
|
||||
storeActions.addToQueueByType(data, type as Play);
|
||||
}
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const addToQueueByFetch = useCallback(
|
||||
async (serverId: string, id: string[], itemType: LibraryItem, type: AddToQueueType) => {
|
||||
const songs = await fetchSongsByItemType(queryClient, serverId, {
|
||||
id,
|
||||
itemType,
|
||||
});
|
||||
|
||||
if (typeof type === 'object' && 'edge' in type && type.edge !== null) {
|
||||
const edge = type.edge === 'top' ? 'top' : 'bottom';
|
||||
storeActions.addToQueueByUniqueId(songs, type.uniqueId, edge);
|
||||
} else {
|
||||
storeActions.addToQueueByType(songs, type as Play);
|
||||
}
|
||||
},
|
||||
[queryClient, storeActions],
|
||||
);
|
||||
|
||||
const clearQueue = useCallback(() => {
|
||||
storeActions.clearQueue();
|
||||
}, [storeActions]);
|
||||
|
||||
const clearSelected = useCallback(
|
||||
(items: QueueSong[]) => {
|
||||
storeActions.clearSelected(items);
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const decreaseVolume = useCallback(
|
||||
(amount: number) => {
|
||||
storeActions.decreaseVolume(amount);
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const increaseVolume = useCallback(
|
||||
(amount: number) => {
|
||||
storeActions.increaseVolume(amount);
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const mediaNext = useCallback(() => {
|
||||
storeActions.mediaNext();
|
||||
}, [storeActions]);
|
||||
|
||||
const mediaPause = useCallback(() => {
|
||||
storeActions.mediaPause();
|
||||
}, [storeActions]);
|
||||
|
||||
const mediaPlay = useCallback(
|
||||
(id?: string) => {
|
||||
storeActions.mediaPlay(id);
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const mediaPrevious = useCallback(() => {
|
||||
storeActions.mediaPrevious();
|
||||
}, [storeActions]);
|
||||
|
||||
const mediaStop = useCallback(() => {
|
||||
storeActions.mediaStop();
|
||||
}, [storeActions]);
|
||||
|
||||
const mediaSeekToTimestamp = useCallback(
|
||||
(timestamp: number) => {
|
||||
storeActions.mediaSeekToTimestamp(timestamp);
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const mediaSkipBackward = useCallback(() => {
|
||||
storeActions.mediaSkipBackward();
|
||||
}, [storeActions]);
|
||||
|
||||
const mediaSkipForward = useCallback(() => {
|
||||
storeActions.mediaSkipForward();
|
||||
}, [storeActions]);
|
||||
|
||||
const setSpeed = useCallback(
|
||||
(speed: number) => {
|
||||
storeActions.setSpeed(speed);
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const mediaToggleMute = useCallback(() => {
|
||||
storeActions.mediaToggleMute();
|
||||
}, [storeActions]);
|
||||
|
||||
const mediaTogglePlayPause = useCallback(() => {
|
||||
storeActions.mediaTogglePlayPause();
|
||||
}, [storeActions]);
|
||||
|
||||
const moveSelectedTo = useCallback(
|
||||
(items: QueueSong[], edge: 'bottom' | 'top', uniqueId: string) => {
|
||||
storeActions.moveSelectedTo(items, uniqueId, edge);
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const moveSelectedToBottom = useCallback(
|
||||
(items: QueueSong[]) => {
|
||||
storeActions.moveSelectedToBottom(items);
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const moveSelectedToNext = useCallback(
|
||||
(items: QueueSong[]) => {
|
||||
storeActions.moveSelectedToNext(items);
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const moveSelectedToTop = useCallback(
|
||||
(items: QueueSong[]) => {
|
||||
storeActions.moveSelectedToTop(items);
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const setVolume = useCallback(
|
||||
(volume: number) => {
|
||||
storeActions.setVolume(volume);
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const setRepeat = useCallback(
|
||||
(repeat: PlayerRepeat) => {
|
||||
storeActions.setRepeat(repeat);
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const setShuffle = useCallback(
|
||||
(shuffle: PlayerShuffle) => {
|
||||
storeActions.setShuffle(shuffle);
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const shuffle = useCallback(() => {
|
||||
storeActions.shuffle();
|
||||
}, [storeActions]);
|
||||
|
||||
const shuffleAll = useCallback(() => {
|
||||
storeActions.shuffleAll();
|
||||
}, [storeActions]);
|
||||
|
||||
const shuffleSelected = useCallback(
|
||||
(items: QueueSong[]) => {
|
||||
storeActions.shuffleSelected(items);
|
||||
},
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const toggleRepeat = useCallback(() => {
|
||||
storeActions.toggleRepeat();
|
||||
}, [storeActions]);
|
||||
|
||||
const toggleShuffle = useCallback(() => {
|
||||
storeActions.toggleShuffle();
|
||||
}, [storeActions]);
|
||||
|
||||
const contextValue: PlayerContext = useMemo(
|
||||
() => ({
|
||||
addToQueueByData,
|
||||
addToQueueByFetch,
|
||||
clearQueue,
|
||||
clearSelected,
|
||||
decreaseVolume,
|
||||
increaseVolume,
|
||||
mediaNext,
|
||||
mediaPause,
|
||||
mediaPlay,
|
||||
mediaPrevious,
|
||||
mediaSeekToTimestamp,
|
||||
mediaSkipBackward,
|
||||
mediaSkipForward,
|
||||
mediaStop,
|
||||
mediaToggleMute,
|
||||
mediaTogglePlayPause,
|
||||
moveSelectedTo,
|
||||
moveSelectedToBottom,
|
||||
moveSelectedToNext,
|
||||
moveSelectedToTop,
|
||||
setRepeat,
|
||||
setShuffle,
|
||||
setSpeed,
|
||||
setVolume,
|
||||
shuffle,
|
||||
shuffleAll,
|
||||
shuffleSelected,
|
||||
toggleRepeat,
|
||||
toggleShuffle,
|
||||
}),
|
||||
[
|
||||
addToQueueByData,
|
||||
addToQueueByFetch,
|
||||
clearQueue,
|
||||
clearSelected,
|
||||
decreaseVolume,
|
||||
setSpeed,
|
||||
increaseVolume,
|
||||
mediaNext,
|
||||
mediaPause,
|
||||
mediaPlay,
|
||||
mediaPrevious,
|
||||
mediaSeekToTimestamp,
|
||||
mediaSkipBackward,
|
||||
mediaSkipForward,
|
||||
mediaStop,
|
||||
mediaToggleMute,
|
||||
mediaTogglePlayPause,
|
||||
moveSelectedTo,
|
||||
moveSelectedToBottom,
|
||||
moveSelectedToNext,
|
||||
moveSelectedToTop,
|
||||
setRepeat,
|
||||
setShuffle,
|
||||
setVolume,
|
||||
shuffle,
|
||||
shuffleAll,
|
||||
shuffleSelected,
|
||||
toggleRepeat,
|
||||
toggleShuffle,
|
||||
],
|
||||
);
|
||||
|
||||
return <PlayerContext.Provider value={contextValue}>{children}</PlayerContext.Provider>;
|
||||
};
|
||||
|
||||
export const usePlayerContext = () => {
|
||||
return useContext(PlayerContext);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the songs from the server
|
||||
* @param queryClient - The query client to use to fetch the data
|
||||
* @param serverId - The library id to use to fetch the data
|
||||
* @param type - The type of the item to add to the queue
|
||||
* @param args - The arguments to use to fetch the data
|
||||
* @returns The songs to add to the queue
|
||||
*/
|
||||
export async function fetchSongsByItemType(
|
||||
queryClient: QueryClient,
|
||||
serverId: string,
|
||||
args: {
|
||||
id: string[];
|
||||
itemType: LibraryItem;
|
||||
params?: Record<string, any>;
|
||||
},
|
||||
) {
|
||||
const songs: Song[] = [];
|
||||
|
||||
switch (args.itemType) {
|
||||
case LibraryItem.ALBUM: {
|
||||
const promises: Promise<SongListResponse>[] = [];
|
||||
|
||||
for (const id of args.id) {
|
||||
promises.push(
|
||||
queryClient.fetchQuery(
|
||||
songsQueries.list({
|
||||
query: {
|
||||
albumIds: [id],
|
||||
sortBy: SongListSort.ID,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
...args.params,
|
||||
},
|
||||
serverId: serverId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
songs.push(...results.flatMap((r) => r.items));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
case LibraryItem.ARTIST: {
|
||||
const promises: Promise<SongListResponse>[] = [];
|
||||
|
||||
for (const id of args.id) {
|
||||
promises.push(
|
||||
queryClient.fetchQuery(
|
||||
songsQueries.list({
|
||||
query: {
|
||||
albumArtistIds: [id],
|
||||
limit: -1,
|
||||
sortBy: SongListSort.ID,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
...args.params,
|
||||
},
|
||||
serverId: serverId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
songs.push(...results.flatMap((r) => r.items));
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case LibraryItem.GENRE: {
|
||||
const promises: Promise<SongListResponse>[] = [];
|
||||
|
||||
for (const id of args.id) {
|
||||
promises.push(
|
||||
queryClient.fetchQuery(
|
||||
songsQueries.list({
|
||||
query: {
|
||||
genreIds: [id],
|
||||
limit: -1,
|
||||
sortBy: SongListSort.ID,
|
||||
sortOrder: SortOrder.ASC,
|
||||
startIndex: 0,
|
||||
...args.params,
|
||||
},
|
||||
serverId: serverId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
songs.push(...results.flatMap((r) => r.items));
|
||||
break;
|
||||
}
|
||||
|
||||
case LibraryItem.PLAYLIST: {
|
||||
const promises: Promise<PlaylistSongListResponse>[] = [];
|
||||
|
||||
for (const id of args.id) {
|
||||
promises.push(
|
||||
queryClient.fetchQuery(
|
||||
playlistsQueries.songList({
|
||||
query: {
|
||||
id: id,
|
||||
...args.params,
|
||||
},
|
||||
serverId: serverId,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises);
|
||||
|
||||
songs.push(...results.flatMap((r) => r.items));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return songs;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,226 +1,226 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import isElectron from 'is-electron';
|
||||
import { nanoid } from 'nanoid/non-secure';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
// import { useQueryClient } from '@tanstack/react-query';
|
||||
// import isElectron from 'is-electron';
|
||||
// import { nanoid } from 'nanoid/non-secure';
|
||||
// import { useCallback, useRef } from 'react';
|
||||
// import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
||||
import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||
import {
|
||||
getAlbumArtistSongsById,
|
||||
getAlbumSongsById,
|
||||
getArtistSongsById,
|
||||
getGenreSongsById,
|
||||
getPlaylistSongsById,
|
||||
getSongById,
|
||||
getSongsByQuery,
|
||||
} from '/@/renderer/features/player/utils';
|
||||
import { useCurrentServer, usePlayerControls, usePlayerStore } from '/@/renderer/store';
|
||||
import { useGeneralSettings, usePlaybackType } from '/@/renderer/store/settings.store';
|
||||
import { setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import {
|
||||
instanceOfCancellationError,
|
||||
LibraryItem,
|
||||
QueueSong,
|
||||
Song,
|
||||
SongListResponse,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { Play, PlaybackType, PlayQueueAddOptions } from '/@/shared/types/types';
|
||||
// import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
// import { PlayersRef } from '/@/renderer/features/player/ref/players-ref';
|
||||
// import { updateSong } from '/@/renderer/features/player/update-remote-song';
|
||||
// import {
|
||||
// getAlbumArtistSongsById,
|
||||
// getAlbumSongsById,
|
||||
// getArtistSongsById,
|
||||
// getGenreSongsById,
|
||||
// getPlaylistSongsById,
|
||||
// getSongById,
|
||||
// getSongsByQuery,
|
||||
// } from '/@/renderer/features/player/utils';
|
||||
// import { useCurrentServer, usePlayerStore } from '/@/renderer/store';
|
||||
// import { useGeneralSettings, usePlaybackType } from '/@/renderer/store/settings.store';
|
||||
// import { setQueue, setQueueNext } from '/@/renderer/utils/set-transcoded-queue-data';
|
||||
// import { toast } from '/@/shared/components/toast/toast';
|
||||
// import {
|
||||
// instanceOfCancellationError,
|
||||
// LibraryItem,
|
||||
// QueueSong,
|
||||
// Song,
|
||||
// SongListResponse,
|
||||
// } from '/@/shared/types/domain-types';
|
||||
// import { Play, PlaybackType, PlayQueueAddOptions } from '/@/shared/types/types';
|
||||
|
||||
const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
|
||||
let queryKey;
|
||||
// const getRootQueryKey = (itemType: LibraryItem, serverId: string) => {
|
||||
// let queryKey;
|
||||
|
||||
switch (itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
queryKey = queryKeys.songs.list(serverId);
|
||||
break;
|
||||
case LibraryItem.ALBUM_ARTIST:
|
||||
queryKey = queryKeys.songs.list(serverId);
|
||||
break;
|
||||
case LibraryItem.GENRE:
|
||||
queryKey = queryKeys.songs.list(serverId);
|
||||
break;
|
||||
case LibraryItem.PLAYLIST:
|
||||
queryKey = queryKeys.playlists.songList(serverId);
|
||||
break;
|
||||
case LibraryItem.SONG:
|
||||
queryKey = queryKeys.songs.list(serverId);
|
||||
break;
|
||||
default:
|
||||
queryKey = queryKeys.songs.list(serverId);
|
||||
break;
|
||||
}
|
||||
// switch (itemType) {
|
||||
// case LibraryItem.ALBUM:
|
||||
// queryKey = queryKeys.songs.list(serverId);
|
||||
// break;
|
||||
// case LibraryItem.ALBUM_ARTIST:
|
||||
// queryKey = queryKeys.songs.list(serverId);
|
||||
// break;
|
||||
// case LibraryItem.GENRE:
|
||||
// queryKey = queryKeys.songs.list(serverId);
|
||||
// break;
|
||||
// case LibraryItem.PLAYLIST:
|
||||
// queryKey = queryKeys.playlists.songList(serverId);
|
||||
// break;
|
||||
// case LibraryItem.SONG:
|
||||
// queryKey = queryKeys.songs.list(serverId);
|
||||
// break;
|
||||
// default:
|
||||
// queryKey = queryKeys.songs.list(serverId);
|
||||
// break;
|
||||
// }
|
||||
|
||||
return queryKey;
|
||||
};
|
||||
// return queryKey;
|
||||
// };
|
||||
|
||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
// const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
|
||||
const addToQueue = usePlayerStore.getState().actions.addToQueue;
|
||||
// const addToQueue = usePlayerStore.getState().actions.addToQueue;
|
||||
|
||||
export const useHandlePlayQueueAdd = () => {
|
||||
const { t } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const playbackType = usePlaybackType();
|
||||
const server = useCurrentServer();
|
||||
const { play } = usePlayerControls();
|
||||
const timeoutIds = useRef<null | Record<string, ReturnType<typeof setTimeout>>>({});
|
||||
// export const useHandlePlayQueueAdd = () => {
|
||||
// const { t } = useTranslation();
|
||||
// const queryClient = useQueryClient();
|
||||
// const playbackType = usePlaybackType();
|
||||
// const server = useCurrentServer();
|
||||
// const { play } = usePlayerControls();
|
||||
// const timeoutIds = useRef<null | Record<string, ReturnType<typeof setTimeout>>>({});
|
||||
|
||||
const { doubleClickQueueAll } = useGeneralSettings();
|
||||
// const { doubleClickQueueAll } = useGeneralSettings();
|
||||
|
||||
const handlePlayQueueAdd = useCallback(
|
||||
async (options: PlayQueueAddOptions) => {
|
||||
if (!server) return toast.error({ message: 'No server selected', type: 'error' });
|
||||
const { byData, byItemType, initialIndex, initialSongId, playType, query } = options;
|
||||
let songs: null | QueueSong[] = null;
|
||||
// Allow this to be undefined for "play shuffled". If undefined, default to 0,
|
||||
// otherwise, choose the selected item in the queue
|
||||
let initialSongIndex: number | undefined;
|
||||
let toastId: null | string = null;
|
||||
// const handlePlayQueueAdd = useCallback(
|
||||
// async (options: PlayQueueAddOptions) => {
|
||||
// if (!server) return toast.error({ message: 'No server selected', type: 'error' });
|
||||
// const { byData, byItemType, initialIndex, initialSongId, playType, query } = options;
|
||||
// let songs: null | QueueSong[] = null;
|
||||
// // Allow this to be undefined for "play shuffled". If undefined, default to 0,
|
||||
// // otherwise, choose the selected item in the queue
|
||||
// let initialSongIndex: number | undefined;
|
||||
// let toastId: null | string = null;
|
||||
|
||||
if (byItemType) {
|
||||
let songList: SongListResponse | undefined;
|
||||
const { id, type: itemType } = byItemType;
|
||||
// if (byItemType) {
|
||||
// let songList: SongListResponse | undefined;
|
||||
// const { id, type: itemType } = byItemType;
|
||||
|
||||
const fetchId = nanoid();
|
||||
timeoutIds.current = {
|
||||
...timeoutIds.current,
|
||||
[fetchId]: setTimeout(() => {
|
||||
toastId = toast.info({
|
||||
autoClose: false,
|
||||
message: t('player.playbackFetchCancel', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
onClose: () => {
|
||||
queryClient.cancelQueries({
|
||||
exact: false,
|
||||
queryKey: getRootQueryKey(itemType, server?.id),
|
||||
});
|
||||
},
|
||||
title: t('player.playbackFetchInProgress', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
});
|
||||
}, 2000),
|
||||
};
|
||||
// const fetchId = nanoid();
|
||||
// timeoutIds.current = {
|
||||
// ...timeoutIds.current,
|
||||
// [fetchId]: setTimeout(() => {
|
||||
// toastId = toast.info({
|
||||
// autoClose: false,
|
||||
// message: t('player.playbackFetchCancel', {
|
||||
// postProcess: 'sentenceCase',
|
||||
// }),
|
||||
// onClose: () => {
|
||||
// queryClient.cancelQueries({
|
||||
// exact: false,
|
||||
// queryKey: getRootQueryKey(itemType, server?.id),
|
||||
// });
|
||||
// },
|
||||
// title: t('player.playbackFetchInProgress', {
|
||||
// postProcess: 'sentenceCase',
|
||||
// }),
|
||||
// });
|
||||
// }, 2000),
|
||||
// };
|
||||
|
||||
try {
|
||||
if (itemType === LibraryItem.PLAYLIST) {
|
||||
songList = await getPlaylistSongsById({
|
||||
id: id?.[0],
|
||||
query,
|
||||
queryClient,
|
||||
server,
|
||||
});
|
||||
} else if (itemType === LibraryItem.ALBUM) {
|
||||
songList = await getAlbumSongsById({ id, query, queryClient, server });
|
||||
} else if (itemType === LibraryItem.ALBUM_ARTIST) {
|
||||
songList = await getAlbumArtistSongsById({
|
||||
id,
|
||||
query,
|
||||
queryClient,
|
||||
server,
|
||||
});
|
||||
} else if (itemType === LibraryItem.ARTIST) {
|
||||
songList = await getArtistSongsById({
|
||||
id,
|
||||
query,
|
||||
queryClient,
|
||||
server,
|
||||
});
|
||||
} else if (itemType === LibraryItem.GENRE) {
|
||||
songList = await getGenreSongsById({ id, query, queryClient, server });
|
||||
} else if (itemType === LibraryItem.SONG) {
|
||||
if (id?.length === 1) {
|
||||
songList = await getSongById({ id: id?.[0], queryClient, server });
|
||||
} else if (!doubleClickQueueAll && initialSongId) {
|
||||
songList = await getSongById({
|
||||
id: initialSongId,
|
||||
queryClient,
|
||||
server,
|
||||
});
|
||||
} else {
|
||||
songList = await getSongsByQuery({ query, queryClient, server });
|
||||
}
|
||||
}
|
||||
// try {
|
||||
// if (itemType === LibraryItem.PLAYLIST) {
|
||||
// songList = await getPlaylistSongsById({
|
||||
// id: id?.[0],
|
||||
// query,
|
||||
// queryClient,
|
||||
// server,
|
||||
// });
|
||||
// } else if (itemType === LibraryItem.ALBUM) {
|
||||
// songList = await getAlbumSongsById({ id, query, queryClient, server });
|
||||
// } else if (itemType === LibraryItem.ALBUM_ARTIST) {
|
||||
// songList = await getAlbumArtistSongsById({
|
||||
// id,
|
||||
// query,
|
||||
// queryClient,
|
||||
// server,
|
||||
// });
|
||||
// } else if (itemType === LibraryItem.ARTIST) {
|
||||
// songList = await getArtistSongsById({
|
||||
// id,
|
||||
// query,
|
||||
// queryClient,
|
||||
// server,
|
||||
// });
|
||||
// } else if (itemType === LibraryItem.GENRE) {
|
||||
// songList = await getGenreSongsById({ id, query, queryClient, server });
|
||||
// } else if (itemType === LibraryItem.SONG) {
|
||||
// if (id?.length === 1) {
|
||||
// songList = await getSongById({ id: id?.[0], queryClient, server });
|
||||
// } else if (!doubleClickQueueAll && initialSongId) {
|
||||
// songList = await getSongById({
|
||||
// id: initialSongId,
|
||||
// queryClient,
|
||||
// server,
|
||||
// });
|
||||
// } else {
|
||||
// songList = await getSongsByQuery({ query, queryClient, server });
|
||||
// }
|
||||
// }
|
||||
|
||||
clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
|
||||
delete timeoutIds.current[fetchId];
|
||||
if (toastId) {
|
||||
toast.hide(toastId);
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (instanceOfCancellationError(err)) {
|
||||
return null;
|
||||
}
|
||||
// clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
|
||||
// delete timeoutIds.current[fetchId];
|
||||
// if (toastId) {
|
||||
// toast.hide(toastId);
|
||||
// }
|
||||
// } catch (err: any) {
|
||||
// if (instanceOfCancellationError(err)) {
|
||||
// return null;
|
||||
// }
|
||||
|
||||
clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
|
||||
delete timeoutIds.current[fetchId];
|
||||
toast.hide(fetchId);
|
||||
// clearTimeout(timeoutIds.current[fetchId] as ReturnType<typeof setTimeout>);
|
||||
// delete timeoutIds.current[fetchId];
|
||||
// toast.hide(fetchId);
|
||||
|
||||
return toast.error({
|
||||
message: err.message,
|
||||
title: t('error.genericError', { postProcess: 'sentenceCase' }) as string,
|
||||
});
|
||||
}
|
||||
// return toast.error({
|
||||
// message: err.message,
|
||||
// title: t('error.genericError', { postProcess: 'sentenceCase' }) as string,
|
||||
// });
|
||||
// }
|
||||
|
||||
songs =
|
||||
songList?.items?.map((song: Song) => ({ ...song, uniqueId: nanoid() })) || null;
|
||||
} else if (byData) {
|
||||
songs = byData.map((song) => ({ ...song, uniqueId: nanoid() })) || null;
|
||||
}
|
||||
// songs =
|
||||
// songList?.items?.map((song: Song) => ({ ...song, uniqueId: nanoid() })) || null;
|
||||
// } else if (byData) {
|
||||
// songs = byData.map((song) => ({ ...song, uniqueId: nanoid() })) || null;
|
||||
// }
|
||||
|
||||
if (!songs || songs?.length === 0)
|
||||
return toast.warn({
|
||||
message: t('common.noResultsFromQuery', { postProcess: 'sentenceCase' }),
|
||||
title: t('player.playbackFetchNoResults', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
// if (!songs || songs?.length === 0)
|
||||
// return toast.warn({
|
||||
// message: t('common.noResultsFromQuery', { postProcess: 'sentenceCase' }),
|
||||
// title: t('player.playbackFetchNoResults', { postProcess: 'sentenceCase' }),
|
||||
// });
|
||||
|
||||
if (initialIndex) {
|
||||
initialSongIndex = initialIndex;
|
||||
} else if (initialSongId) {
|
||||
initialSongIndex = songs.findIndex((song) => song.id === initialSongId);
|
||||
}
|
||||
// if (initialIndex) {
|
||||
// initialSongIndex = initialIndex;
|
||||
// } else if (initialSongId) {
|
||||
// initialSongIndex = songs.findIndex((song) => song.id === initialSongId);
|
||||
// }
|
||||
|
||||
const hadSong = usePlayerStore.getState().queue.default.length > 0;
|
||||
const playerData = addToQueue({ initialIndex: initialSongIndex, playType, songs });
|
||||
// const hadSong = usePlayerStore.getState().queue.default.length > 0;
|
||||
// const playerData = addToQueue({ initialIndex: initialSongIndex, playType, songs });
|
||||
|
||||
updateSong(playerData.current.song);
|
||||
// updateSong(playerData.current.song);
|
||||
|
||||
const replacesQueue = playType === Play.NOW || playType === Play.SHUFFLE;
|
||||
// const replacesQueue = playType === Play.NOW || playType === Play.SHUFFLE;
|
||||
|
||||
if (playbackType === PlaybackType.LOCAL) {
|
||||
mpvPlayer!.volume(usePlayerStore.getState().volume);
|
||||
// if (playbackType === PlaybackType.LOCAL) {
|
||||
// mpvPlayer!.volume(usePlayerStore.getState().volume);
|
||||
|
||||
if (replacesQueue || !hadSong) {
|
||||
mpvPlayer!.pause();
|
||||
setQueue(playerData, false);
|
||||
} else {
|
||||
setQueueNext(playerData);
|
||||
}
|
||||
} else {
|
||||
const player =
|
||||
playerData.current.player === 1
|
||||
? PlayersRef.current?.player1
|
||||
: PlayersRef.current?.player2;
|
||||
const underlying = player?.getInternalPlayer();
|
||||
if (underlying && replacesQueue) {
|
||||
underlying.currentTime = 0;
|
||||
}
|
||||
}
|
||||
// if (replacesQueue || !hadSong) {
|
||||
// mpvPlayer!.pause();
|
||||
// setQueue(playerData, false);
|
||||
// } else {
|
||||
// setQueueNext(playerData);
|
||||
// }
|
||||
// } else {
|
||||
// const player =
|
||||
// playerData.current.player === 1
|
||||
// ? PlayersRef.current?.player1
|
||||
// : PlayersRef.current?.player2;
|
||||
// const underlying = player?.getInternalPlayer();
|
||||
// if (underlying && replacesQueue) {
|
||||
// underlying.currentTime = 0;
|
||||
// }
|
||||
// }
|
||||
|
||||
// We should only play if the queue was empty, or we are doing play NOW
|
||||
// (override the queue).
|
||||
if (replacesQueue || !hadSong) {
|
||||
play();
|
||||
}
|
||||
// // We should only play if the queue was empty, or we are doing play NOW
|
||||
// // (override the queue).
|
||||
// if (replacesQueue || !hadSong) {
|
||||
// play();
|
||||
// }
|
||||
|
||||
return null;
|
||||
},
|
||||
[doubleClickQueueAll, play, playbackType, queryClient, server, t],
|
||||
);
|
||||
// return null;
|
||||
// },
|
||||
// [doubleClickQueueAll, play, playbackType, queryClient, server, t],
|
||||
// );
|
||||
|
||||
return handlePlayQueueAdd;
|
||||
};
|
||||
// return handlePlayQueueAdd;
|
||||
// };
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
useCurrentSong,
|
||||
useCurrentStatus,
|
||||
usePlayerSong,
|
||||
usePlayerStatus,
|
||||
usePlaybackSettings,
|
||||
useSettingsStore,
|
||||
} from '/@/renderer/store';
|
||||
@@ -28,8 +28,8 @@ export const useMediaSession = ({
|
||||
handleStop: () => void;
|
||||
}) => {
|
||||
const { mediaSession: mediaSessionEnabled } = usePlaybackSettings();
|
||||
const playerStatus = useCurrentStatus();
|
||||
const currentSong = useCurrentSong();
|
||||
const playerStatus = usePlayerStatus();
|
||||
const currentSong = usePlayerSong();
|
||||
const mediaSession = navigator.mediaSession;
|
||||
const skip = useSettingsStore((state) => state.general.skipButtons);
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import isElectron from 'is-electron';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { useCurrentStatus } from '/@/renderer/store';
|
||||
import { usePlayerStatus } 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 status = usePlayerStatus();
|
||||
const { preventSleepOnPlayback } = useWindowSettings();
|
||||
|
||||
const startPowerSaveBlocker = useCallback(async () => {
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import isElectron from 'is-electron';
|
||||
import { useCallback, useEffect, WheelEvent } from 'react';
|
||||
|
||||
import {
|
||||
useMuted,
|
||||
usePlayerControls,
|
||||
useSetCurrentSpeed,
|
||||
useSpeed,
|
||||
useVolume,
|
||||
} from '/@/renderer/store';
|
||||
import { usePlayerMuted, usePlayerSpeed, usePlayerVolume } from '/@/renderer/store';
|
||||
import { useGeneralSettings } from '/@/renderer/store/settings.store';
|
||||
|
||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
@@ -40,12 +34,12 @@ const calculateVolumeDown = (volume: number, volumeWheelStep: number) => {
|
||||
};
|
||||
|
||||
export const useRightControls = () => {
|
||||
const { setMuted, setVolume } = usePlayerControls();
|
||||
const volume = useVolume();
|
||||
const muted = useMuted();
|
||||
// const { setMuted, setVolume } = usePlayerControls();
|
||||
const volume = usePlayerVolume();
|
||||
const muted = usePlayerMuted();
|
||||
const { volumeWheelStep } = useGeneralSettings();
|
||||
const speed = useSpeed();
|
||||
const setCurrentSpeed = useSetCurrentSpeed();
|
||||
const speed = usePlayerSpeed();
|
||||
// const setCurrentSpeed = useSetCurrentSpeed();
|
||||
|
||||
// Ensure that the mpv player volume is set on startup
|
||||
useEffect(() => {
|
||||
@@ -63,40 +57,37 @@ export const useRightControls = () => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const handleSpeed = useCallback(
|
||||
(e: number) => {
|
||||
setCurrentSpeed(e);
|
||||
if (mpvPlayer) {
|
||||
mpvPlayer?.setProperties({ speed: e });
|
||||
}
|
||||
},
|
||||
[setCurrentSpeed],
|
||||
);
|
||||
const handleSpeed = useCallback((e: number) => {
|
||||
// setCurrentSpeed(e);
|
||||
if (mpvPlayer) {
|
||||
mpvPlayer?.setProperties({ speed: e });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleVolumeSlider = (e: number) => {
|
||||
mpvPlayer?.volume(e);
|
||||
remote?.updateVolume(e);
|
||||
setVolume(e);
|
||||
// setVolume(e);
|
||||
};
|
||||
|
||||
const handleVolumeSliderState = (e: number) => {
|
||||
remote?.updateVolume(e);
|
||||
setVolume(e);
|
||||
// setVolume(e);
|
||||
};
|
||||
|
||||
const handleVolumeDown = useCallback(() => {
|
||||
const volumeToSet = calculateVolumeDown(volume, volumeWheelStep);
|
||||
mpvPlayer?.volume(volumeToSet);
|
||||
remote?.updateVolume(volumeToSet);
|
||||
setVolume(volumeToSet);
|
||||
}, [setVolume, volume, volumeWheelStep]);
|
||||
// setVolume(volumeToSet);
|
||||
}, [volume, volumeWheelStep]);
|
||||
|
||||
const handleVolumeUp = useCallback(() => {
|
||||
const volumeToSet = calculateVolumeUp(volume, volumeWheelStep);
|
||||
mpvPlayer?.volume(volumeToSet);
|
||||
remote?.updateVolume(volumeToSet);
|
||||
setVolume(volumeToSet);
|
||||
}, [setVolume, volume, volumeWheelStep]);
|
||||
// setVolume(volumeToSet);
|
||||
}, [volume, volumeWheelStep]);
|
||||
|
||||
const handleVolumeWheel = useCallback(
|
||||
(e: WheelEvent<HTMLButtonElement | HTMLDivElement>) => {
|
||||
@@ -109,15 +100,15 @@ export const useRightControls = () => {
|
||||
|
||||
mpvPlayer?.volume(volumeToSet);
|
||||
remote?.updateVolume(volumeToSet);
|
||||
setVolume(volumeToSet);
|
||||
// setVolume(volumeToSet);
|
||||
},
|
||||
[setVolume, volume, volumeWheelStep],
|
||||
[volume, volumeWheelStep],
|
||||
);
|
||||
|
||||
const handleMute = useCallback(() => {
|
||||
setMuted(!muted);
|
||||
// setMuted(!muted);
|
||||
mpvPlayer?.mute(!muted);
|
||||
}, [muted, setMuted]);
|
||||
}, [muted]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron()) {
|
||||
|
||||
@@ -103,7 +103,7 @@ export const useScrobble = () => {
|
||||
// Only trigger if the song changed, or the player changed. This should be the case
|
||||
// anyways, but who knows
|
||||
if (
|
||||
currentSong.uniqueId !== previous[0]?.uniqueId ||
|
||||
currentSong._uniqueId !== previous[0]?._uniqueId ||
|
||||
current[2] !== previous[2]
|
||||
) {
|
||||
const artists =
|
||||
@@ -141,15 +141,15 @@ export const useScrobble = () => {
|
||||
|
||||
if (
|
||||
(!isCurrentSongScrobbled && shouldSubmitScrobble) ||
|
||||
previousSong?.serverType === ServerType.JELLYFIN
|
||||
previousSong?._serverType === ServerType.JELLYFIN
|
||||
) {
|
||||
const position =
|
||||
previousSong?.serverType === ServerType.JELLYFIN
|
||||
previousSong?._serverType === ServerType.JELLYFIN
|
||||
? previousSongTimeSec * 1e7
|
||||
: undefined;
|
||||
|
||||
sendScrobble.mutate({
|
||||
apiClientProps: { serverId: previousSong?.serverId || '' },
|
||||
apiClientProps: { serverId: previousSong?._serverId || '' },
|
||||
query: {
|
||||
id: previousSong.id,
|
||||
position,
|
||||
@@ -173,7 +173,7 @@ export const useScrobble = () => {
|
||||
// Send start scrobble when song changes and the new song is playing
|
||||
if (currentStatus === PlayerStatus.PLAYING && currentSong?.id) {
|
||||
sendScrobble.mutate({
|
||||
apiClientProps: { serverId: currentSong?.serverId || '' },
|
||||
apiClientProps: { serverId: currentSong?._serverId || '' },
|
||||
query: {
|
||||
event: 'start',
|
||||
id: currentSong.id,
|
||||
@@ -182,7 +182,7 @@ export const useScrobble = () => {
|
||||
},
|
||||
});
|
||||
|
||||
if (currentSong?.serverType === ServerType.JELLYFIN) {
|
||||
if (currentSong?._serverType === ServerType.JELLYFIN) {
|
||||
// It is possible that another function sets an interval.
|
||||
// We only want one running, so clear the existing interval
|
||||
if (progressIntervalId.current) {
|
||||
@@ -379,7 +379,7 @@ export const useScrobble = () => {
|
||||
// multiple times in a row and playback goes normally (no next/previous)
|
||||
equalityFn: (a, b) =>
|
||||
// compute whether the song changed
|
||||
a[0]?.uniqueId === b[0]?.uniqueId &&
|
||||
a[0]?._uniqueId === b[0]?._uniqueId &&
|
||||
// compute whether the same player: relevant for repeat one and repeat all (one track)
|
||||
a[2] === b[2],
|
||||
},
|
||||
|
||||
@@ -3,12 +3,11 @@ import { AxiosError } from 'axios';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { MutationOptions } from '/@/renderer/lib/react-query';
|
||||
import { useIncrementQueuePlayCount } from '/@/renderer/store';
|
||||
import { usePlayEvent } from '/@/renderer/store/event.store';
|
||||
import { ScrobbleArgs, ScrobbleResponse } from '/@/shared/types/domain-types';
|
||||
|
||||
export const useSendScrobble = (options?: MutationOptions) => {
|
||||
const incrementPlayCount = useIncrementQueuePlayCount();
|
||||
// const incrementPlayCount = useIncrementQueuePlayCount();
|
||||
const sendPlayEvent = usePlayEvent();
|
||||
|
||||
return useMutation<ScrobbleResponse, AxiosError, ScrobbleArgs, null>({
|
||||
@@ -21,7 +20,7 @@ export const useSendScrobble = (options?: MutationOptions) => {
|
||||
onSuccess: (_data, variables) => {
|
||||
// Manually increment the play count for the song in the queue if scrobble was submitted
|
||||
if (variables.query.submission) {
|
||||
incrementPlayCount([variables.query.id]);
|
||||
// incrementPlayCount([variables.query.id]);
|
||||
sendPlayEvent(variables.query.id);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -29,8 +29,8 @@ import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-a
|
||||
import { useAppFocus } from '/@/renderer/hooks';
|
||||
import {
|
||||
useCurrentServer,
|
||||
useCurrentSong,
|
||||
useCurrentStatus,
|
||||
usePlayerSong,
|
||||
usePlayerStatus,
|
||||
usePlaylistDetailStore,
|
||||
usePlaylistDetailTablePagination,
|
||||
useSetPlaylistDetailTable,
|
||||
@@ -58,9 +58,9 @@ interface PlaylistDetailContentProps {
|
||||
export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetailContentProps) => {
|
||||
const { playlistId } = useParams() as { playlistId: string };
|
||||
const queryClient = useQueryClient();
|
||||
const status = useCurrentStatus();
|
||||
const status = usePlayerStatus();
|
||||
const isFocused = useAppFocus();
|
||||
const currentSong = useCurrentSong();
|
||||
const currentSong = usePlayerSong();
|
||||
const server = useCurrentServer();
|
||||
const page = usePlaylistDetailStore();
|
||||
const filters: PlaylistSongListQueryClientSide = useMemo(() => {
|
||||
|
||||
+1
-1
@@ -377,7 +377,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
||||
if (!detailQuery.data) return;
|
||||
deletePlaylistMutation?.mutate(
|
||||
{
|
||||
apiClientProps: { serverId: detailQuery.data.serverId },
|
||||
apiClientProps: { serverId: detailQuery.data._serverId },
|
||||
query: { id: detailQuery.data.id },
|
||||
},
|
||||
{
|
||||
|
||||
@@ -56,7 +56,7 @@ const PlaylistDetailSongListRoute = () => {
|
||||
|
||||
createPlaylistMutation.mutate(
|
||||
{
|
||||
apiClientProps: { serverId: detailQuery?.data?.serverId },
|
||||
apiClientProps: { serverId: detailQuery?.data?._serverId },
|
||||
body: {
|
||||
_custom: {
|
||||
navidrome: {
|
||||
@@ -83,7 +83,7 @@ const PlaylistDetailSongListRoute = () => {
|
||||
},
|
||||
);
|
||||
deletePlaylistMutation.mutate({
|
||||
apiClientProps: { serverId: detailQuery?.data?.serverId },
|
||||
apiClientProps: { serverId: detailQuery?.data?._serverId },
|
||||
query: { id: playlistId },
|
||||
});
|
||||
},
|
||||
@@ -124,7 +124,7 @@ const PlaylistDetailSongListRoute = () => {
|
||||
}),
|
||||
)
|
||||
}
|
||||
serverId={detailQuery?.data?.serverId || ''}
|
||||
serverId={detailQuery?.data?._serverId || ''}
|
||||
/>
|
||||
),
|
||||
title: t('common.saveAs', { postProcess: 'sentenceCase' }),
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
import isElectron from 'is-electron';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { usePlayerEvents } from '/@/renderer/features/player/audio-player/hooks/use-player-events';
|
||||
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
|
||||
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
|
||||
import { useSetRating } from '/@/renderer/features/shared/mutations/set-rating-mutation';
|
||||
import { usePlayerActions, useRemoteSettings } from '/@/renderer/store';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||
import { PlayerShuffle } from '/@/shared/types/types';
|
||||
|
||||
const remote = isElectron() ? window.api.remote : null;
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
|
||||
export const useRemote = () => {
|
||||
const { mediaSkipForward, setTimestamp, setVolume } = usePlayerActions();
|
||||
|
||||
const remoteSettings = useRemoteSettings();
|
||||
const updateRatingMutation = useSetRating({});
|
||||
const addToFavoritesMutation = useCreateFavorite({});
|
||||
const removeFromFavoritesMutation = useDeleteFavorite({});
|
||||
|
||||
// Initialize the remote
|
||||
useEffect(() => {
|
||||
if (!remote) {
|
||||
return;
|
||||
}
|
||||
|
||||
remote
|
||||
?.updateSetting(
|
||||
remoteSettings.enabled,
|
||||
remoteSettings.port,
|
||||
remoteSettings.username,
|
||||
remoteSettings.password,
|
||||
)
|
||||
.catch((error) => {
|
||||
toast.warn({ message: error, title: 'Failed to enable remote' });
|
||||
});
|
||||
// We only want to fire this once
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!remote) {
|
||||
return;
|
||||
}
|
||||
|
||||
remote.requestPosition((_e: unknown, data: { position: number }) => {
|
||||
const newTime = data.position;
|
||||
setTimestamp(newTime);
|
||||
});
|
||||
|
||||
remote.requestSeek((_e: unknown, data: { offset: number }) => {
|
||||
mediaSkipForward(data.offset);
|
||||
});
|
||||
|
||||
remote.requestRating(
|
||||
(_e: unknown, data: { id: string; rating: number; serverId: string }) => {
|
||||
updateRatingMutation.mutate({
|
||||
apiClientProps: { serverId: data.serverId },
|
||||
query: {
|
||||
item: [
|
||||
{
|
||||
_serverId: data.serverId,
|
||||
id: data.id,
|
||||
itemType: LibraryItem.SONG,
|
||||
} as Song,
|
||||
],
|
||||
rating: data.rating,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
remote.requestVolume((_e: unknown, data: { volume: number }) => {
|
||||
setVolume(data.volume);
|
||||
});
|
||||
|
||||
remote.requestFavorite(
|
||||
(_e: unknown, data: { favorite: boolean; id: string; serverId: string }) => {
|
||||
const mutator = data.favorite
|
||||
? addToFavoritesMutation
|
||||
: removeFromFavoritesMutation;
|
||||
mutator.mutate({
|
||||
apiClientProps: { serverId: data.serverId },
|
||||
query: {
|
||||
id: [data.id],
|
||||
type: LibraryItem.SONG,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
ipc?.removeAllListeners('request-position');
|
||||
ipc?.removeAllListeners('request-seek');
|
||||
ipc?.removeAllListeners('request-volume');
|
||||
ipc?.removeAllListeners('request-favorite');
|
||||
ipc?.removeAllListeners('request-rating');
|
||||
};
|
||||
}, [
|
||||
addToFavoritesMutation,
|
||||
mediaSkipForward,
|
||||
removeFromFavoritesMutation,
|
||||
setTimestamp,
|
||||
setVolume,
|
||||
updateRatingMutation,
|
||||
]);
|
||||
|
||||
usePlayerEvents(
|
||||
{
|
||||
onPlayerProgress: (properties) => {
|
||||
if (!remote) {
|
||||
return;
|
||||
}
|
||||
|
||||
remote.updatePosition(properties.timestamp);
|
||||
},
|
||||
onPlayerRepeat: (properties) => {
|
||||
if (!remote) {
|
||||
return;
|
||||
}
|
||||
|
||||
remote.updateRepeat(properties.repeat);
|
||||
},
|
||||
onPlayerShuffle: (properties) => {
|
||||
if (!remote) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isShuffleEnabled = properties.shuffle !== PlayerShuffle.NONE;
|
||||
remote.updateShuffle(isShuffleEnabled);
|
||||
},
|
||||
onPlayerStatus: (properties) => {
|
||||
if (!remote) {
|
||||
return;
|
||||
}
|
||||
|
||||
remote.updatePlayback(properties.status);
|
||||
},
|
||||
onPlayerVolume: (properties) => {
|
||||
if (!remote) {
|
||||
return;
|
||||
}
|
||||
|
||||
remote.updateVolume(properties.volume);
|
||||
},
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { useHotkeySettings, usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { PlaybackType } from '/@/shared/types/types';
|
||||
import { PlayerType } from '/@/shared/types/types';
|
||||
|
||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||
const isWindows = isElectron() ? window.api.utils.isWindows() : false;
|
||||
@@ -25,9 +25,7 @@ export const WindowHotkeySettings = () => {
|
||||
defaultChecked={settings.globalMediaHotkeys}
|
||||
disabled={
|
||||
!isElectron() ||
|
||||
(enableWindowsMediaSession &&
|
||||
isWindows &&
|
||||
playbackType === PlaybackType.WEB)
|
||||
(enableWindowsMediaSession && isWindows && playbackType === PlayerType.WEB)
|
||||
}
|
||||
onChange={(e) => {
|
||||
setSettings({
|
||||
|
||||
@@ -6,14 +6,13 @@ import {
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { useCurrentStatus, usePlayerStore } from '/@/renderer/store';
|
||||
import { usePlayerActions, usePlayerProperties, 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, PlaybackStyle, PlaybackType, PlayerStatus } from '/@/shared/types/types';
|
||||
import { CrossfadeStyle, PlayerStatus, PlayerStyle, PlayerType } from '/@/shared/types/types';
|
||||
|
||||
const ipc = isElectron() ? window.api.ipc : null;
|
||||
|
||||
@@ -26,7 +25,10 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
const { t } = useTranslation();
|
||||
const settings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const status = useCurrentStatus();
|
||||
const status = usePlayerStatus();
|
||||
|
||||
const { crossfadeDuration, transitionType } = usePlayerProperties();
|
||||
const { setCrossfadeDuration, setTransitionType } = usePlayerActions();
|
||||
|
||||
const [audioDevices, setAudioDevices] = useState<{ label: string; value: string }[]>([]);
|
||||
|
||||
@@ -43,7 +45,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
);
|
||||
};
|
||||
|
||||
if (settings.type === PlaybackType.WEB) {
|
||||
if (settings.type === PlayerType.WEB) {
|
||||
getAudioDevices();
|
||||
}
|
||||
}, [settings.type, t]);
|
||||
@@ -56,19 +58,15 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
{
|
||||
disabled: !isElectron(),
|
||||
label: 'MPV',
|
||||
value: PlaybackType.LOCAL,
|
||||
value: PlayerType.LOCAL,
|
||||
},
|
||||
{ label: 'Web', value: PlaybackType.WEB },
|
||||
{ label: 'Web', value: PlayerType.WEB },
|
||||
]}
|
||||
defaultValue={settings.type}
|
||||
disabled={status === PlayerStatus.PLAYING}
|
||||
onChange={(e) => {
|
||||
setSettings({ playback: { ...settings, type: e as PlaybackType } });
|
||||
setSettings({ playback: { ...settings, type: e as PlayerType } });
|
||||
ipc?.send('settings-set', { property: 'playbackType', value: e });
|
||||
if (isElectron() && e === PlaybackType.LOCAL) {
|
||||
const queueData = usePlayerStore.getState().actions.getPlayerData();
|
||||
setQueue(queueData);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
@@ -89,7 +87,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
clearable
|
||||
data={audioDevices}
|
||||
defaultValue={settings.audioDeviceId}
|
||||
disabled={settings.type !== PlaybackType.WEB}
|
||||
disabled={settings.type !== PlayerType.WEB}
|
||||
onChange={(e) => setSettings({ playback: { ...settings, audioDeviceId: e } })}
|
||||
/>
|
||||
),
|
||||
@@ -97,7 +95,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: !isElectron() || settings.type !== PlaybackType.WEB,
|
||||
isHidden: !isElectron() || settings.type !== PlayerType.WEB,
|
||||
title: t('setting.audioDevice', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
@@ -109,28 +107,26 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
context: 'optionNormal',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: PlaybackStyle.GAPLESS,
|
||||
value: PlayerStyle.GAPLESS,
|
||||
},
|
||||
{
|
||||
label: t('setting.playbackStyle', {
|
||||
context: 'optionCrossFade',
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
value: PlaybackStyle.CROSSFADE,
|
||||
value: PlayerStyle.CROSSFADE,
|
||||
},
|
||||
]}
|
||||
defaultValue={settings.style}
|
||||
disabled={settings.type !== PlaybackType.WEB || status === PlayerStatus.PLAYING}
|
||||
onChange={(e) =>
|
||||
setSettings({ playback: { ...settings, style: e as PlaybackStyle } })
|
||||
}
|
||||
defaultValue={transitionType}
|
||||
disabled={settings.type !== PlayerType.WEB || status === PlayerStatus.PLAYING}
|
||||
onChange={(e) => setTransitionType(e as PlayerStyle)}
|
||||
/>
|
||||
),
|
||||
description: t('setting.playbackStyle', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.WEB,
|
||||
isHidden: settings.type !== PlayerType.WEB,
|
||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||
title: t('setting.playbackStyle', {
|
||||
context: 'description',
|
||||
@@ -152,7 +148,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.WEB,
|
||||
isHidden: settings.type !== PlayerType.WEB,
|
||||
note: t('common.restartRequired', { postProcess: 'sentenceCase' }),
|
||||
title: t('setting.webAudio', {
|
||||
postProcess: 'sentenceCase',
|
||||
@@ -173,7 +169,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.WEB,
|
||||
isHidden: settings.type !== PlayerType.WEB,
|
||||
title: t('setting.preservePitch', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
@@ -181,17 +177,15 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
{
|
||||
control: (
|
||||
<Slider
|
||||
defaultValue={settings.crossfadeDuration}
|
||||
defaultValue={crossfadeDuration}
|
||||
disabled={
|
||||
settings.type !== PlaybackType.WEB ||
|
||||
settings.style !== PlaybackStyle.CROSSFADE ||
|
||||
settings.type !== PlayerType.WEB ||
|
||||
settings.style !== PlayerStyle.CROSSFADE ||
|
||||
status === PlayerStatus.PLAYING
|
||||
}
|
||||
max={15}
|
||||
min={0}
|
||||
onChangeEnd={(e) =>
|
||||
setSettings({ playback: { ...settings, crossfadeDuration: e } })
|
||||
}
|
||||
min={3}
|
||||
onChangeEnd={(e) => setCrossfadeDuration(e)}
|
||||
w={100}
|
||||
/>
|
||||
),
|
||||
@@ -199,7 +193,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.WEB,
|
||||
isHidden: settings.type !== PlayerType.WEB,
|
||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||
title: t('setting.crossfadeDuration', {
|
||||
postProcess: 'sentenceCase',
|
||||
@@ -224,8 +218,8 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
]}
|
||||
defaultValue={settings.crossfadeStyle}
|
||||
disabled={
|
||||
settings.type !== PlaybackType.WEB ||
|
||||
settings.style !== PlaybackStyle.CROSSFADE ||
|
||||
settings.type !== PlayerType.WEB ||
|
||||
settings.style !== PlayerStyle.CROSSFADE ||
|
||||
status === PlayerStatus.PLAYING
|
||||
}
|
||||
onChange={(e) => {
|
||||
@@ -241,7 +235,7 @@ export const AudioSettings = ({ hasFancyAudio }: { hasFancyAudio: boolean }) =>
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.WEB,
|
||||
isHidden: settings.type !== PlayerType.WEB,
|
||||
note: status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined,
|
||||
title: t('setting.crossfadeStyle', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { usePlaybackSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { PlaybackType } from '/@/shared/types/types';
|
||||
import { PlayerType } from '/@/shared/types/types';
|
||||
|
||||
const isWindows = isElectron() ? window.api.utils.isWindows() : null;
|
||||
const isDesktop = isElectron();
|
||||
@@ -30,7 +30,7 @@ export const MediaSessionSettings = () => {
|
||||
<Switch
|
||||
aria-label="Toggle media Session"
|
||||
defaultChecked={mediaSession}
|
||||
disabled={!isWindows || !isDesktop || playbackType !== PlaybackType.WEB}
|
||||
disabled={!isWindows || !isDesktop || playbackType !== PlayerType.WEB}
|
||||
onChange={handleMediaSessionChange}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -6,11 +6,9 @@ import {
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { usePlayerControls, usePlayerStore, useQueueControls } from '/@/renderer/store';
|
||||
import {
|
||||
SettingsState,
|
||||
usePlaybackSettings,
|
||||
useSettingsStore,
|
||||
useSettingsStoreActions,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
@@ -22,7 +20,7 @@ import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { Textarea } from '/@/shared/components/textarea/textarea';
|
||||
import { PlaybackType } from '/@/shared/types/types';
|
||||
import { PlayerType } from '/@/shared/types/types';
|
||||
|
||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||
const mpvPlayer = isElectron() ? window.api.mpvPlayer : null;
|
||||
@@ -74,8 +72,8 @@ export const MpvSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const settings = usePlaybackSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const { pause } = usePlayerControls();
|
||||
const { clearQueue } = useQueueControls();
|
||||
// const { pause } = usePlayerControls();
|
||||
// const { clearQueue } = useQueueControls();
|
||||
|
||||
const [mpvPath, setMpvPath] = useState(
|
||||
(localSettings?.get('mpv_path') as string | undefined) || '',
|
||||
@@ -129,21 +127,21 @@ export const MpvSettings = () => {
|
||||
mpvPlayer?.setProperties(mpvSetting);
|
||||
};
|
||||
|
||||
const handleReloadMpv = () => {
|
||||
pause();
|
||||
clearQueue();
|
||||
// const handleReloadMpv = () => {
|
||||
// pause();
|
||||
// clearQueue();
|
||||
|
||||
const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
|
||||
const properties: Record<string, any> = {
|
||||
speed: usePlayerStore.getState().speed,
|
||||
...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
|
||||
};
|
||||
mpvPlayer?.restart({
|
||||
binaryPath: mpvPath || undefined,
|
||||
extraParameters,
|
||||
properties,
|
||||
});
|
||||
};
|
||||
// const extraParameters = useSettingsStore.getState().playback.mpvExtraParameters;
|
||||
// const properties: Record<string, any> = {
|
||||
// speed: usePlayerStore.getState().speed,
|
||||
// ...getMpvProperties(useSettingsStore.getState().playback.mpvProperties),
|
||||
// };
|
||||
// mpvPlayer?.restart({
|
||||
// binaryPath: mpvPath || undefined,
|
||||
// extraParameters,
|
||||
// properties,
|
||||
// });
|
||||
// };
|
||||
|
||||
const handleSetExtraParameters = (data: string[]) => {
|
||||
setSettings({
|
||||
@@ -160,7 +158,7 @@ export const MpvSettings = () => {
|
||||
<Group gap="sm">
|
||||
<ActionIcon
|
||||
icon="refresh"
|
||||
onClick={handleReloadMpv}
|
||||
// onClick={handleReloadMpv}
|
||||
tooltip={{
|
||||
label: t('common.reload', { postProcess: 'titleCase' }),
|
||||
openDelay: 0,
|
||||
@@ -194,7 +192,7 @@ export const MpvSettings = () => {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
isHidden: settings.type !== PlayerType.LOCAL,
|
||||
note: 'Restart required',
|
||||
title: t('setting.mpvExecutablePath', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
@@ -235,7 +233,7 @@ export const MpvSettings = () => {
|
||||
</Text>
|
||||
</Stack>
|
||||
),
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
isHidden: settings.type !== PlayerType.LOCAL,
|
||||
note: t('common.restartRequired', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
@@ -268,7 +266,7 @@ export const MpvSettings = () => {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
isHidden: settings.type !== PlayerType.LOCAL,
|
||||
title: t('setting.gaplessAudio', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
{
|
||||
@@ -311,7 +309,7 @@ export const MpvSettings = () => {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
isHidden: settings.type !== PlaybackType.LOCAL,
|
||||
isHidden: settings.type !== PlayerType.LOCAL,
|
||||
title: t('setting.audioExclusiveMode', { postProcess: 'sentenceCase' }),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ScrobbleSettings } from '/@/renderer/features/settings/components/playb
|
||||
import { TranscodeSettings } from '/@/renderer/features/settings/components/playback/transcode-settings';
|
||||
import { useSettingsStore } from '/@/renderer/store';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { PlaybackType } from '/@/shared/types/types';
|
||||
import { PlayerType } from '/@/shared/types/types';
|
||||
|
||||
const MpvSettings = lazy(() =>
|
||||
import('/@/renderer/features/settings/components/playback/mpv-settings').then((module) => {
|
||||
@@ -22,7 +22,7 @@ export const PlaybackTab = () => {
|
||||
|
||||
const hasFancyAudio = useMemo(() => {
|
||||
return (
|
||||
(isElectron() && audioType === PlaybackType.LOCAL) ||
|
||||
(isElectron() && audioType === PlayerType.LOCAL) ||
|
||||
(useWebAudio && 'AudioContext' in window)
|
||||
);
|
||||
}, [audioType, useWebAudio]);
|
||||
|
||||
@@ -5,7 +5,7 @@ import isElectron from 'is-electron';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store';
|
||||
import { useSetAlbumListItemDataById } from '/@/renderer/store';
|
||||
import { useFavoriteEvent } from '/@/renderer/store/event.store';
|
||||
import {
|
||||
AlbumArtistDetailResponse,
|
||||
@@ -21,7 +21,7 @@ export const useCreateFavorite = (args: MutationHookArgs) => {
|
||||
const { options } = args || {};
|
||||
const queryClient = useQueryClient();
|
||||
const setAlbumListData = useSetAlbumListItemDataById();
|
||||
const setQueueFavorite = useSetQueueFavorite();
|
||||
// const setQueueFavorite = useSetQueueFavorite();
|
||||
const setFavoriteEvent = useFavoriteEvent();
|
||||
|
||||
return useMutation<FavoriteResponse, AxiosError, FavoriteArgs, null>({
|
||||
@@ -46,7 +46,7 @@ export const useCreateFavorite = (args: MutationHookArgs) => {
|
||||
|
||||
if (variables.query.type === LibraryItem.SONG) {
|
||||
remote?.updateFavorite(true, serverId, variables.query.id);
|
||||
setQueueFavorite(variables.query.id, true);
|
||||
// setQueueFavorite(variables.query.id, true);
|
||||
setFavoriteEvent(variables.query.id, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import isElectron from 'is-electron';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { useSetAlbumListItemDataById, useSetQueueFavorite } from '/@/renderer/store';
|
||||
import { useSetAlbumListItemDataById } from '/@/renderer/store';
|
||||
import { useFavoriteEvent } from '/@/renderer/store/event.store';
|
||||
import {
|
||||
AlbumArtistDetailResponse,
|
||||
@@ -21,7 +21,7 @@ export const useDeleteFavorite = (args: MutationHookArgs) => {
|
||||
const { options } = args || {};
|
||||
const queryClient = useQueryClient();
|
||||
const setAlbumListData = useSetAlbumListItemDataById();
|
||||
const setQueueFavorite = useSetQueueFavorite();
|
||||
// const setQueueFavorite = useSetQueueFavorite();
|
||||
const setFavoriteEvent = useFavoriteEvent();
|
||||
|
||||
return useMutation<FavoriteResponse, AxiosError, FavoriteArgs, null>({
|
||||
@@ -44,7 +44,7 @@ export const useDeleteFavorite = (args: MutationHookArgs) => {
|
||||
|
||||
if (variables.query.type === LibraryItem.SONG) {
|
||||
remote?.updateFavorite(false, serverId, variables.query.id);
|
||||
setQueueFavorite(variables.query.id, false);
|
||||
// setQueueFavorite(variables.query.id, false);
|
||||
setFavoriteEvent(variables.query.id, false);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import isElectron from 'is-electron';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { MutationHookArgs } from '/@/renderer/lib/react-query';
|
||||
import { useSetAlbumListItemDataById, useSetQueueRating } from '/@/renderer/store';
|
||||
import { useSetAlbumListItemDataById } from '/@/renderer/store';
|
||||
import { useRatingEvent } from '/@/renderer/store/event.store';
|
||||
import {
|
||||
Album,
|
||||
@@ -24,7 +24,7 @@ export const useSetRating = (args: MutationHookArgs) => {
|
||||
const { options } = args || {};
|
||||
const queryClient = useQueryClient();
|
||||
const setAlbumListData = useSetAlbumListItemDataById();
|
||||
const setQueueRating = useSetQueueRating();
|
||||
// const setQueueRating = useSetQueueRating();
|
||||
const setRatingEvent = useRatingEvent();
|
||||
|
||||
return useMutation<
|
||||
@@ -46,7 +46,7 @@ export const useSetRating = (args: MutationHookArgs) => {
|
||||
setAlbumListData(item.id, { userRating: item.userRating });
|
||||
break;
|
||||
case LibraryItem.SONG:
|
||||
setQueueRating([item.id], item.userRating);
|
||||
// setQueueRating([item.id], item.userRating);
|
||||
setRatingEvent([item.id], item.userRating);
|
||||
break;
|
||||
}
|
||||
@@ -67,14 +67,14 @@ export const useSetRating = (args: MutationHookArgs) => {
|
||||
}
|
||||
|
||||
if (songIds.length > 0) {
|
||||
setQueueRating(songIds, variables.query.rating);
|
||||
// setQueueRating(songIds, variables.query.rating);
|
||||
setRatingEvent(songIds, variables.query.rating);
|
||||
}
|
||||
|
||||
if (remote) {
|
||||
remote.updateRating(
|
||||
variables.query.rating,
|
||||
variables.query.item[0].serverId,
|
||||
variables.query.item[0]._serverId,
|
||||
songIds,
|
||||
);
|
||||
}
|
||||
@@ -88,9 +88,9 @@ export const useSetRating = (args: MutationHookArgs) => {
|
||||
variables.query.item[0].itemType === LibraryItem.ALBUM;
|
||||
|
||||
if (isAlbumDetailPage) {
|
||||
const { id: albumId, serverId } = variables.query.item[0] as Album;
|
||||
const { id: albumId, _serverId } = variables.query.item[0] as Album;
|
||||
|
||||
const queryKey = queryKeys.albums.detail(serverId || '', { id: albumId });
|
||||
const queryKey = queryKeys.albums.detail(_serverId || '', { id: albumId });
|
||||
const previous = queryClient.getQueryData<AlbumDetailResponse>(queryKey);
|
||||
if (previous) {
|
||||
queryClient.setQueryData<AlbumDetailResponse>(queryKey, {
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
} from '/@/renderer/features/sidebar/components/sidebar-playlist-list';
|
||||
import {
|
||||
useAppStoreActions,
|
||||
useCurrentSong,
|
||||
usePlayerSong,
|
||||
useFullScreenPlayerStore,
|
||||
useSetFullScreenPlayerStore,
|
||||
useSidebarStore,
|
||||
@@ -42,7 +42,7 @@ export const Sidebar = () => {
|
||||
const sidebar = useSidebarStore();
|
||||
const { setSideBar } = useAppStoreActions();
|
||||
const { sidebarPlaylistList } = useGeneralSettings();
|
||||
const currentSong = useCurrentSong();
|
||||
const currentSong = usePlayerSong();
|
||||
|
||||
const translatedSidebarItemMap = useMemo(
|
||||
() => ({
|
||||
|
||||
@@ -9,7 +9,6 @@ import { getColumnDefs, VirtualTable } from '/@/renderer/components/virtual-tabl
|
||||
import { ErrorFallback } from '/@/renderer/features/action-required/components/error-fallback';
|
||||
import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
|
||||
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
|
||||
import { useHandlePlayQueueAdd } from '/@/renderer/features/player/hooks/use-handle-playqueue-add';
|
||||
import { songsQueries } from '/@/renderer/features/songs/api/songs-api';
|
||||
import { usePlayButtonBehavior, useTableSettings } from '/@/renderer/store';
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
@@ -24,8 +23,6 @@ export type SimilarSongsListProps = {
|
||||
export const SimilarSongsList = ({ count, fullScreen, song }: SimilarSongsListProps) => {
|
||||
const tableRef = useRef<AgGridReact<Song> | null>(null);
|
||||
const tableConfig = useTableSettings(fullScreen ? 'fullScreen' : 'songs');
|
||||
const handlePlayQueueAdd = useHandlePlayQueueAdd();
|
||||
const playButtonBehavior = usePlayButtonBehavior();
|
||||
|
||||
const songQuery = useQuery(
|
||||
songsQueries.similar({
|
||||
@@ -37,7 +34,7 @@ export const SimilarSongsList = ({ count, fullScreen, song }: SimilarSongsListPr
|
||||
count,
|
||||
songId: song.id,
|
||||
},
|
||||
serverId: song?.serverId,
|
||||
serverId: song?._serverId,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -51,11 +48,11 @@ export const SimilarSongsList = ({ count, fullScreen, song }: SimilarSongsListPr
|
||||
const handleRowDoubleClick = (e: RowDoubleClickedEvent<Song>) => {
|
||||
if (!e.data || !songQuery.data) return;
|
||||
|
||||
handlePlayQueueAdd?.({
|
||||
byData: songQuery.data,
|
||||
initialSongId: e.data.id,
|
||||
playType: playButtonBehavior,
|
||||
});
|
||||
// handlePlayQueueAdd?.({
|
||||
// byData: songQuery.data,
|
||||
// initialSongId: e.data.id,
|
||||
// playType: playButtonBehavior,
|
||||
// });
|
||||
};
|
||||
|
||||
return songQuery.isLoading ? (
|
||||
|
||||
@@ -12,8 +12,8 @@ import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/conte
|
||||
import { useAppFocus } from '/@/renderer/hooks';
|
||||
import {
|
||||
useCurrentServer,
|
||||
useCurrentSong,
|
||||
useCurrentStatus,
|
||||
usePlayerSong,
|
||||
usePlayerStatus,
|
||||
usePlayButtonBehavior,
|
||||
} from '/@/renderer/store';
|
||||
import { LibraryItem, QueueSong, SongListQuery } from '/@/shared/types/domain-types';
|
||||
@@ -27,8 +27,8 @@ export const SongListTableView = ({ itemCount, tableRef }: SongListTableViewProp
|
||||
const server = useCurrentServer();
|
||||
const { customFilters, handlePlay, id, pageKey } = useListContext();
|
||||
const isFocused = useAppFocus();
|
||||
const currentSong = useCurrentSong();
|
||||
const status = useCurrentStatus();
|
||||
const currentSong = usePlayerSong();
|
||||
const status = usePlayerStatus();
|
||||
|
||||
const { rowClassRules } = useCurrentSongRowStyles({ tableRef });
|
||||
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { HotkeyItem, useHotkeys } from '@mantine/hooks';
|
||||
import clsx from 'clsx';
|
||||
import isElectron from 'is-electron';
|
||||
import { lazy, useMemo } from 'react';
|
||||
import { lazy } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import styles from './default-layout.module.css';
|
||||
|
||||
import { ContextMenuProvider } from '/@/renderer/features/context-menu/context-menu-provider';
|
||||
import { CommandPalette } from '/@/renderer/features/search/components/command-palette';
|
||||
import { MainContent } from '/@/renderer/layouts/default-layout/main-content';
|
||||
import { PlayerBar } from '/@/renderer/layouts/default-layout/player-bar';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import {
|
||||
useAppStore,
|
||||
useCommandPalette,
|
||||
useCurrentStatus,
|
||||
useQueueStatus,
|
||||
} from '/@/renderer/store';
|
||||
import { useCommandPalette } from '/@/renderer/store';
|
||||
import {
|
||||
useGeneralSettings,
|
||||
useHotkeySettings,
|
||||
@@ -24,13 +18,13 @@ import {
|
||||
useSettingsStoreActions,
|
||||
useWindowSettings,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { Platform, PlaybackType, PlayerStatus } from '/@/shared/types/types';
|
||||
import { Platform, PlayerType } from '/@/shared/types/types';
|
||||
|
||||
if (!isElectron()) {
|
||||
useSettingsStore.getState().actions.setSettings({
|
||||
playback: {
|
||||
...useSettingsStore.getState().playback,
|
||||
type: PlaybackType.WEB,
|
||||
type: PlayerType.WEB,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -53,9 +47,6 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
|
||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||
const settings = useGeneralSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const playerStatus = useCurrentStatus();
|
||||
const { currentSong, index, length } = useQueueStatus();
|
||||
const { privateMode } = useAppStore();
|
||||
|
||||
const updateZoom = (increase: number) => {
|
||||
const newVal = settings.zoomFactor + increase;
|
||||
@@ -83,21 +74,8 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
|
||||
...(isElectron() ? zoomHotkeys : []),
|
||||
]);
|
||||
|
||||
const title = useMemo(() => {
|
||||
const statusString = playerStatus === PlayerStatus.PAUSED ? '(Paused) ' : '';
|
||||
const queueString = length ? `(${index + 1} / ${length}) ` : '';
|
||||
const privateModeString = privateMode ? '(Private mode)' : '';
|
||||
const title = `${
|
||||
length
|
||||
? `${statusString}${queueString}${currentSong?.name}${currentSong?.artistName ? ` — ${currentSong?.artistName} — Feishin` : ''}`
|
||||
: 'Feishin'
|
||||
}${privateMode ? ` ${privateModeString}` : ''}`;
|
||||
document.title = title;
|
||||
return title;
|
||||
}, [currentSong?.artistName, currentSong?.name, index, length, playerStatus, privateMode]);
|
||||
|
||||
return (
|
||||
<ContextMenuProvider>
|
||||
<>
|
||||
<div
|
||||
className={clsx(styles.layout, {
|
||||
[styles.macos]: windowBarStyle === Platform.MACOS,
|
||||
@@ -105,11 +83,11 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
|
||||
})}
|
||||
id="default-layout"
|
||||
>
|
||||
{windowBarStyle !== Platform.WEB && <WindowBar title={title} />}
|
||||
<WindowBar />
|
||||
<MainContent shell={shell} />
|
||||
<PlayerBar />
|
||||
</div>
|
||||
<CommandPalette modalProps={{ handlers, opened }} />
|
||||
</ContextMenuProvider>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import isElectron from 'is-electron';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { RiCheckboxBlankLine, RiCloseLine, RiSubtractLine } from 'react-icons/ri';
|
||||
|
||||
import appIcon from '../../../assets/icons/32x32.png';
|
||||
@@ -12,9 +12,9 @@ import macMinHover from './assets/min-mac-hover.png';
|
||||
import macMin from './assets/min-mac.png';
|
||||
import styles from './window-bar.module.css';
|
||||
|
||||
import { useWindowSettings } from '/@/renderer/store';
|
||||
import { useAppStore, usePlayerData, usePlayerStatus, useWindowSettings } from '/@/renderer/store';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { Platform } from '/@/shared/types/types';
|
||||
import { Platform, PlayerStatus } from '/@/shared/types/types';
|
||||
|
||||
const localSettings = isElectron() ? window.api.localSettings : null;
|
||||
|
||||
@@ -121,14 +121,13 @@ const MacOsControls = ({ controls, title }: WindowBarControlsProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
interface WindowBarProps {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export const WindowBar = ({ title }: WindowBarProps) => {
|
||||
export const WindowBar = () => {
|
||||
const { windowBarStyle } = useWindowSettings();
|
||||
const playerStatus = usePlayerStatus();
|
||||
const { privateMode } = useAppStore();
|
||||
const handleMinimize = () => minimize();
|
||||
|
||||
const { currentSong, index, queueLength } = usePlayerData();
|
||||
const [max, setMax] = useState(localSettings?.env.START_MAXIMIZED || false);
|
||||
|
||||
const handleMaximize = useCallback(() => {
|
||||
@@ -142,6 +141,23 @@ export const WindowBar = ({ title }: WindowBarProps) => {
|
||||
|
||||
const handleClose = useCallback(() => close(), []);
|
||||
|
||||
const title = useMemo(() => {
|
||||
const statusString = playerStatus === PlayerStatus.PAUSED ? '(Paused) ' : '';
|
||||
const queueString = queueLength ? `(${index + 1} / ${queueLength}) ` : '';
|
||||
const privateModeString = privateMode ? '(Private mode)' : '';
|
||||
const title = `${
|
||||
queueLength
|
||||
? `${statusString}${queueString}${currentSong?.name}${currentSong?.artistName ? ` — ${currentSong?.artistName} — Feishin` : ''}`
|
||||
: 'Feishin'
|
||||
}${privateMode ? ` ${privateModeString}` : ''}`;
|
||||
document.title = title;
|
||||
return title;
|
||||
}, [currentSong?.artistName, currentSong?.name, index, playerStatus, privateMode, queueLength]);
|
||||
|
||||
if (windowBarStyle === Platform.WEB) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{windowBarStyle === Platform.WINDOWS && (
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { StoreApi, UseBoundStore } from 'zustand';
|
||||
|
||||
type WithSelectors<S> = S extends { getState: () => infer T }
|
||||
? S & { use: { [K in keyof T]: () => T[K] } }
|
||||
: never;
|
||||
|
||||
export const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(_store: S) => {
|
||||
const store = _store as WithSelectors<typeof _store>;
|
||||
store.use = {};
|
||||
for (const k of Object.keys(store.getState())) {
|
||||
(store.use as any)[k] = () => store((s) => s[k as keyof typeof s]);
|
||||
}
|
||||
|
||||
return store;
|
||||
};
|
||||
@@ -4,7 +4,7 @@ import { Navigate, Outlet } from 'react-router-dom';
|
||||
|
||||
import { useServerAuthenticated } from '/@/renderer/hooks/use-server-authenticated';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useCurrentServer, useSetPlayerFallback } from '/@/renderer/store';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { Center } from '/@/shared/components/center/center';
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
@@ -16,7 +16,7 @@ const mpvPlayerListener = isElectron() ? window.api.mpvPlayerListener : null;
|
||||
|
||||
export const AppOutlet = () => {
|
||||
const currentServer = useCurrentServer();
|
||||
const setFallback = useSetPlayerFallback();
|
||||
// const setFallback = useSetPlayerFallback();
|
||||
const authState = useServerAuthenticated();
|
||||
|
||||
const isActionsRequired = useMemo(() => {
|
||||
@@ -28,20 +28,20 @@ export const AppOutlet = () => {
|
||||
return isActionRequired;
|
||||
}, [currentServer]);
|
||||
|
||||
useEffect(() => {
|
||||
utils?.mainMessageListener((_event, data) => {
|
||||
toast.show(data);
|
||||
});
|
||||
// useEffect(() => {
|
||||
// utils?.mainMessageListener((_event, data) => {
|
||||
// toast.show(data);
|
||||
// });
|
||||
|
||||
mpvPlayerListener?.rendererPlayerFallback((_event, data) => {
|
||||
setFallback(data);
|
||||
});
|
||||
// mpvPlayerListener?.rendererPlayerFallback((_event, data) => {
|
||||
// setFallback(data);
|
||||
// });
|
||||
|
||||
return () => {
|
||||
ipc?.removeAllListeners('toast-from-main');
|
||||
ipc?.removeAllListeners('renderer-player-fallback');
|
||||
};
|
||||
}, [setFallback]);
|
||||
// return () => {
|
||||
// ipc?.removeAllListeners('toast-from-main');
|
||||
// ipc?.removeAllListeners('renderer-player-fallback');
|
||||
// };
|
||||
// }, [setFallback]);
|
||||
|
||||
if (authState === AuthState.LOADING) {
|
||||
return (
|
||||
|
||||
+1351
-1097
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,6 @@ import { createWithEqualityFn } from 'zustand/traditional';
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { ContextMenuItemType } from '/@/renderer/features/context-menu/events';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { usePlayerStore } from '/@/renderer/store/player.store';
|
||||
import { mergeOverridingColumns } from '/@/renderer/store/utils';
|
||||
import { FontValueSchema } from '/@/renderer/types/fonts';
|
||||
import { randomString } from '/@/renderer/utils';
|
||||
@@ -21,8 +20,8 @@ import {
|
||||
FontType,
|
||||
Platform,
|
||||
Play,
|
||||
PlaybackStyle,
|
||||
PlaybackType,
|
||||
PlayerStyle,
|
||||
PlayerType,
|
||||
TableColumn,
|
||||
TableType,
|
||||
} from '/@/shared/types/types';
|
||||
@@ -258,9 +257,9 @@ const PlaybackSettingsSchema = z.object({
|
||||
muted: z.boolean(),
|
||||
preservePitch: z.boolean(),
|
||||
scrobble: ScrobbleSettingsSchema,
|
||||
style: z.nativeEnum(PlaybackStyle),
|
||||
style: z.nativeEnum(PlayerStyle),
|
||||
transcode: TranscodingConfigSchema,
|
||||
type: z.nativeEnum(PlaybackType),
|
||||
type: z.nativeEnum(PlayerType),
|
||||
webAudio: z.boolean(),
|
||||
});
|
||||
|
||||
@@ -652,11 +651,11 @@ const initialState: SettingsState = {
|
||||
scrobbleAtDuration: 240,
|
||||
scrobbleAtPercentage: 75,
|
||||
},
|
||||
style: PlaybackStyle.GAPLESS,
|
||||
style: PlayerStyle.GAPLESS,
|
||||
transcode: {
|
||||
enabled: false,
|
||||
},
|
||||
type: PlaybackType.WEB,
|
||||
type: PlayerType.WEB,
|
||||
webAudio: true,
|
||||
},
|
||||
remote: {
|
||||
@@ -843,7 +842,7 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
|
||||
...initialState,
|
||||
playback: {
|
||||
...initialState.playback,
|
||||
type: PlaybackType.WEB,
|
||||
type: PlayerType.WEB,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@@ -967,12 +966,6 @@ export const useGeneralSettings = () => useSettingsStore((state) => state.genera
|
||||
|
||||
export const usePlaybackType = () =>
|
||||
useSettingsStore((state) => {
|
||||
const isFallback = usePlayerStore.getState().fallback;
|
||||
|
||||
if (isFallback) {
|
||||
return PlaybackType.WEB;
|
||||
}
|
||||
|
||||
return state.playback.type;
|
||||
});
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const modifyUrl = (song: QueueSong): string => {
|
||||
if (transcode.enabled) {
|
||||
const streamUrl = api.controller.getTranscodingUrl({
|
||||
apiClientProps: {
|
||||
serverId: song.serverId,
|
||||
serverId: song._serverId,
|
||||
},
|
||||
query: {
|
||||
base: song.streamUrl,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
export function shuffle<T>(array: T[]): T[] {
|
||||
// Create a copy of the array to avoid mutating the original
|
||||
const shuffled = [...array];
|
||||
|
||||
// Loop through the array from the last element to the first
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
// Generate a random index from 0 to i
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
// Swap elements at positions i and j
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
|
||||
}
|
||||
|
||||
return shuffled;
|
||||
}
|
||||
|
||||
export function shuffleInPlace<T>(array: T[]): T[] {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { jfType } from '/@/shared/api/jellyfin/jellyfin-types';
|
||||
@@ -228,6 +227,8 @@ const normalizeSong = (
|
||||
}
|
||||
|
||||
return {
|
||||
_serverId: server?.id || '',
|
||||
_serverType: ServerType.JELLYFIN,
|
||||
album: item.Album,
|
||||
albumArtists: item.AlbumArtists?.map((entry) => ({
|
||||
id: entry.Id,
|
||||
@@ -290,13 +291,10 @@ const normalizeSong = (
|
||||
: null,
|
||||
releaseYear: item.ProductionYear ? String(item.ProductionYear) : null,
|
||||
sampleRate,
|
||||
serverId: server?.id || '',
|
||||
serverType: ServerType.JELLYFIN,
|
||||
size,
|
||||
streamUrl,
|
||||
tags: getTags(item),
|
||||
trackNumber: item.IndexNumber,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item.DateCreated,
|
||||
userFavorite: (item.UserData && item.UserData.IsFavorite) || false,
|
||||
userRating: null,
|
||||
@@ -309,6 +307,8 @@ const normalizeAlbum = (
|
||||
imageSize?: number,
|
||||
): Album => {
|
||||
return {
|
||||
_serverId: server?.id || '',
|
||||
_serverType: ServerType.JELLYFIN,
|
||||
albumArtist: item.AlbumArtist,
|
||||
albumArtists:
|
||||
item.AlbumArtists.map((entry) => ({
|
||||
@@ -351,13 +351,10 @@ const normalizeAlbum = (
|
||||
releaseDate: item.PremiereDate?.split('T')[0] || null,
|
||||
releaseTypes: [],
|
||||
releaseYear: item.ProductionYear || null,
|
||||
serverId: server?.id || '',
|
||||
serverType: ServerType.JELLYFIN,
|
||||
size: null,
|
||||
songCount: item?.ChildCount || null,
|
||||
songs: item.Songs?.map((song) => normalizeSong(song, server, '', imageSize)),
|
||||
tags: getTags(item),
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item?.DateLastMediaAdded || item.DateCreated,
|
||||
userFavorite: item.UserData?.IsFavorite || false,
|
||||
userRating: null,
|
||||
@@ -386,6 +383,8 @@ const normalizeAlbumArtist = (
|
||||
) || [];
|
||||
|
||||
return {
|
||||
_serverId: server?.id || '',
|
||||
_serverType: ServerType.JELLYFIN,
|
||||
albumCount: item.AlbumCount ?? null,
|
||||
backgroundImageUrl: null,
|
||||
biography: item.Overview || null,
|
||||
@@ -407,8 +406,6 @@ const normalizeAlbumArtist = (
|
||||
mbz: item.ProviderIds?.MusicBrainzArtist || null,
|
||||
name: item.Name,
|
||||
playCount: item.UserData?.PlayCount || 0,
|
||||
serverId: server?.id || '',
|
||||
serverType: ServerType.JELLYFIN,
|
||||
similarArtists,
|
||||
songCount: item.SongCount ?? null,
|
||||
userFavorite: item.UserData?.IsFavorite || false,
|
||||
@@ -430,6 +427,8 @@ const normalizePlaylist = (
|
||||
const imagePlaceholderUrl = null;
|
||||
|
||||
return {
|
||||
_serverId: server?.id || '',
|
||||
_serverType: ServerType.JELLYFIN,
|
||||
description: item.Overview || null,
|
||||
duration: item.RunTimeTicks / 10000,
|
||||
genres: item.GenreItems?.map((entry) => ({
|
||||
@@ -447,8 +446,6 @@ const normalizePlaylist = (
|
||||
ownerId: null,
|
||||
public: null,
|
||||
rules: null,
|
||||
serverId: server?.id || '',
|
||||
serverType: ServerType.JELLYFIN,
|
||||
size: null,
|
||||
songCount: item?.ChildCount || null,
|
||||
sync: null,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import z from 'zod';
|
||||
|
||||
import { ndType } from '/@/shared/api/navidrome/navidrome-types';
|
||||
@@ -152,6 +151,8 @@ const normalizeSong = (
|
||||
album: item.album,
|
||||
albumId: item.albumId,
|
||||
...getArtists(item),
|
||||
_serverId: server?.id || 'unknown',
|
||||
_serverType: ServerType.NAVIDROME,
|
||||
artistName: item.artist,
|
||||
bitDepth: item.bitDepth || null,
|
||||
bitRate: item.bitRate,
|
||||
@@ -204,13 +205,10 @@ const normalizeSong = (
|
||||
).toISOString(),
|
||||
releaseYear: String(item.year),
|
||||
sampleRate: item.sampleRate || null,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.NAVIDROME,
|
||||
size: item.size,
|
||||
streamUrl: `${server?.url}/rest/stream.view?id=${id}&v=1.13.0&c=Feishin&${server?.credential}`,
|
||||
tags: item.tags || null,
|
||||
trackNumber: item.trackNumber,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item.updatedAt,
|
||||
userFavorite: item.starred || false,
|
||||
userRating: item.rating || null,
|
||||
@@ -280,6 +278,8 @@ const normalizeAlbum = (
|
||||
return {
|
||||
...parseAlbumTags(item),
|
||||
...getArtists(item),
|
||||
_serverId: server?.id || 'unknown',
|
||||
_serverType: ServerType.NAVIDROME,
|
||||
albumArtist: item.albumArtist,
|
||||
backdropImageUrl: imageBackdropUrl,
|
||||
comment: item.comment || null,
|
||||
@@ -301,9 +301,9 @@ const normalizeAlbum = (
|
||||
imagePlaceholderUrl,
|
||||
imageUrl,
|
||||
isCompilation: item.compilation,
|
||||
|
||||
itemType: LibraryItem.ALBUM,
|
||||
lastPlayedAt: normalizePlayDate(item),
|
||||
|
||||
mbzId: item.mbzAlbumId || null,
|
||||
name: item.name,
|
||||
originalDate: item.originalDate
|
||||
@@ -317,12 +317,9 @@ const normalizeAlbum = (
|
||||
: new Date(Date.UTC(item.minYear, 0, 1))
|
||||
).toISOString(),
|
||||
releaseYear: item.minYear,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.NAVIDROME,
|
||||
size: item.size,
|
||||
songCount: item.songCount,
|
||||
songs: item.songs ? item.songs.map((song) => normalizeSong(song, server)) : undefined,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item.updatedAt,
|
||||
userFavorite: item.starred,
|
||||
userRating: item.rating || null,
|
||||
@@ -365,6 +362,8 @@ const normalizeAlbumArtist = (
|
||||
}
|
||||
|
||||
return {
|
||||
_serverId: server?.id || 'unknown',
|
||||
_serverType: ServerType.NAVIDROME,
|
||||
albumCount,
|
||||
backgroundImageUrl: null,
|
||||
biography: item.biography || null,
|
||||
@@ -382,8 +381,6 @@ const normalizeAlbumArtist = (
|
||||
mbz: item.mbzArtistId || null,
|
||||
name: item.name,
|
||||
playCount: item.playCount || 0,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.NAVIDROME,
|
||||
similarArtists:
|
||||
item.similarArtists?.map((artist) => ({
|
||||
id: artist.id,
|
||||
@@ -412,6 +409,8 @@ const normalizePlaylist = (
|
||||
const imagePlaceholderUrl = null;
|
||||
|
||||
return {
|
||||
_serverId: server?.id || 'unknown',
|
||||
_serverType: ServerType.NAVIDROME,
|
||||
description: item.comment,
|
||||
duration: item.duration * 1000,
|
||||
genres: [],
|
||||
@@ -424,8 +423,6 @@ const normalizePlaylist = (
|
||||
ownerId: item.ownerId,
|
||||
public: item.public,
|
||||
rules: item?.rules || null,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.NAVIDROME,
|
||||
size: item.size,
|
||||
songCount: item.songCount,
|
||||
sync: item.sync,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ssType } from '/@/shared/api/subsonic/subsonic-types';
|
||||
@@ -9,10 +8,10 @@ import {
|
||||
Genre,
|
||||
LibraryItem,
|
||||
Playlist,
|
||||
QueueSong,
|
||||
RelatedArtist,
|
||||
ServerListItemWithCredential,
|
||||
ServerType,
|
||||
Song,
|
||||
} from '/@/shared/types/domain-types';
|
||||
|
||||
const getCoverArtUrl = (args: {
|
||||
@@ -119,7 +118,7 @@ const normalizeSong = (
|
||||
item: z.infer<typeof ssType._response.song>,
|
||||
server?: null | ServerListItemWithCredential,
|
||||
size?: number,
|
||||
): QueueSong => {
|
||||
): Song => {
|
||||
const imageUrl =
|
||||
getCoverArtUrl({
|
||||
baseUrl: server?.url,
|
||||
@@ -131,6 +130,8 @@ const normalizeSong = (
|
||||
const streamUrl = `${server?.url}/rest/stream.view?id=${item.id}&v=1.13.0&c=Feishin&${server?.credential}`;
|
||||
|
||||
return {
|
||||
_serverId: server?.id || 'unknown',
|
||||
_serverType: ServerType.SUBSONIC,
|
||||
album: item.album || '',
|
||||
albumArtists: getArtistList(item.albumArtists, item.artistId, item.artist),
|
||||
albumId: item.albumId?.toString() || '',
|
||||
@@ -183,13 +184,10 @@ const normalizeSong = (
|
||||
releaseDate: null,
|
||||
releaseYear: item.year ? String(item.year) : null,
|
||||
sampleRate: item.samplingRate || null,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.SUBSONIC,
|
||||
size: item.size,
|
||||
streamUrl,
|
||||
tags: null,
|
||||
trackNumber: item.track || 1,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: '',
|
||||
userFavorite: item.starred || false,
|
||||
userRating: item.userRating || null,
|
||||
@@ -212,6 +210,8 @@ const normalizeAlbumArtist = (
|
||||
}) || null;
|
||||
|
||||
return {
|
||||
_serverId: server?.id || 'unknown',
|
||||
_serverType: ServerType.SUBSONIC,
|
||||
albumCount: item.albumCount ? Number(item.albumCount) : 0,
|
||||
backgroundImageUrl: null,
|
||||
biography: null,
|
||||
@@ -224,8 +224,6 @@ const normalizeAlbumArtist = (
|
||||
mbz: null,
|
||||
name: item.name,
|
||||
playCount: null,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.SUBSONIC,
|
||||
similarArtists: [],
|
||||
songCount: null,
|
||||
userFavorite: false,
|
||||
@@ -247,6 +245,8 @@ const normalizeAlbum = (
|
||||
}) || null;
|
||||
|
||||
return {
|
||||
_serverId: server?.id || 'unknown',
|
||||
_serverType: ServerType.SUBSONIC,
|
||||
albumArtist: item.artist,
|
||||
albumArtists: getArtistList(item.artists, item.artistId, item.artist),
|
||||
artists: [],
|
||||
@@ -276,8 +276,6 @@ const normalizeAlbum = (
|
||||
releaseDate: item.year ? new Date(Date.UTC(item.year, 0, 1)).toISOString() : null,
|
||||
releaseTypes: item.releaseTypes || [],
|
||||
releaseYear: item.year ? Number(item.year) : null,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.SUBSONIC,
|
||||
size: null,
|
||||
songCount: item.songCount,
|
||||
songs:
|
||||
@@ -285,7 +283,6 @@ const normalizeAlbum = (
|
||||
normalizeSong(song, server),
|
||||
) || [],
|
||||
tags: null,
|
||||
uniqueId: nanoid(),
|
||||
updatedAt: item.created,
|
||||
userFavorite: item.starred || false,
|
||||
userRating: item.userRating || null,
|
||||
@@ -300,6 +297,8 @@ const normalizePlaylist = (
|
||||
server?: null | ServerListItemWithCredential,
|
||||
): Playlist => {
|
||||
return {
|
||||
_serverId: server?.id || 'unknown',
|
||||
_serverType: ServerType.SUBSONIC,
|
||||
description: item.comment || null,
|
||||
duration: item.duration * 1000,
|
||||
genres: [],
|
||||
@@ -316,8 +315,6 @@ const normalizePlaylist = (
|
||||
owner: item.owner,
|
||||
ownerId: item.owner,
|
||||
public: item.public,
|
||||
serverId: server?.id || 'unknown',
|
||||
serverType: ServerType.SUBSONIC,
|
||||
size: null,
|
||||
songCount: item.songCount,
|
||||
};
|
||||
|
||||
@@ -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,29 +58,33 @@ 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 & {
|
||||
uniqueId: string;
|
||||
_uniqueId: string;
|
||||
};
|
||||
|
||||
export type ServerListItem = {
|
||||
@@ -161,6 +165,8 @@ export enum ImageType {
|
||||
}
|
||||
|
||||
export type Album = {
|
||||
_serverId: string;
|
||||
_serverType: ServerType;
|
||||
albumArtist: string;
|
||||
albumArtists: RelatedArtist[];
|
||||
artists: RelatedArtist[];
|
||||
@@ -185,13 +191,10 @@ export type Album = {
|
||||
releaseDate: null | string;
|
||||
releaseTypes: string[];
|
||||
releaseYear: null | number;
|
||||
serverId: string;
|
||||
serverType: ServerType;
|
||||
size: null | number;
|
||||
songCount: null | number;
|
||||
songs?: Song[];
|
||||
tags: null | Record<string, string[]>;
|
||||
uniqueId: string;
|
||||
updatedAt: string;
|
||||
userFavorite: boolean;
|
||||
userRating: null | number;
|
||||
@@ -199,6 +202,8 @@ export type Album = {
|
||||
} & { songs?: Song[] };
|
||||
|
||||
export type AlbumArtist = {
|
||||
_serverId: string;
|
||||
_serverType: ServerType;
|
||||
albumCount: null | number;
|
||||
backgroundImageUrl: null | string;
|
||||
biography: null | string;
|
||||
@@ -211,8 +216,6 @@ export type AlbumArtist = {
|
||||
mbz: null | string;
|
||||
name: string;
|
||||
playCount: null | number;
|
||||
serverId: string;
|
||||
serverType: ServerType;
|
||||
similarArtists: null | RelatedArtist[];
|
||||
songCount: null | number;
|
||||
userFavorite: boolean;
|
||||
@@ -220,15 +223,14 @@ export type AlbumArtist = {
|
||||
};
|
||||
|
||||
export type Artist = {
|
||||
_serverId: string;
|
||||
_serverType: ServerType;
|
||||
biography: null | string;
|
||||
createdAt: string;
|
||||
id: string;
|
||||
itemType: LibraryItem.ARTIST;
|
||||
name: string;
|
||||
remoteCreatedAt: null | string;
|
||||
serverFolderId: string;
|
||||
serverId: string;
|
||||
serverType: ServerType;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
@@ -297,6 +299,8 @@ export type MusicFolder = {
|
||||
export type MusicFoldersResponse = MusicFolder[];
|
||||
|
||||
export type Playlist = {
|
||||
_serverId: string;
|
||||
_serverType: ServerType;
|
||||
description: null | string;
|
||||
duration: null | number;
|
||||
genres: Genre[];
|
||||
@@ -309,8 +313,6 @@ export type Playlist = {
|
||||
ownerId: null | string;
|
||||
public: boolean | null;
|
||||
rules?: null | Record<string, any>;
|
||||
serverId: string;
|
||||
serverType: ServerType;
|
||||
size: null | number;
|
||||
songCount: null | number;
|
||||
sync?: boolean | null;
|
||||
@@ -328,6 +330,8 @@ export type RelatedArtist = {
|
||||
};
|
||||
|
||||
export type Song = {
|
||||
_serverId: string;
|
||||
_serverType: ServerType;
|
||||
album: null | string;
|
||||
albumArtists: RelatedArtist[];
|
||||
albumId: string;
|
||||
@@ -364,13 +368,10 @@ export type Song = {
|
||||
releaseDate: null | string;
|
||||
releaseYear: null | string;
|
||||
sampleRate: null | number;
|
||||
serverId: string;
|
||||
serverType: ServerType;
|
||||
size: number;
|
||||
streamUrl: string;
|
||||
tags: null | Record<string, string[]>;
|
||||
trackNumber: number;
|
||||
uniqueId: string;
|
||||
updatedAt: string;
|
||||
userFavorite: boolean;
|
||||
userRating: null | number;
|
||||
|
||||
@@ -101,20 +101,16 @@ export enum FontType {
|
||||
}
|
||||
|
||||
export enum Play {
|
||||
INDEX = 'index',
|
||||
LAST = 'last',
|
||||
NEXT = 'next',
|
||||
NOW = 'now',
|
||||
SHUFFLE = 'shuffle',
|
||||
}
|
||||
|
||||
export enum PlaybackStyle {
|
||||
CROSSFADE = 'crossfade',
|
||||
GAPLESS = 'gapless',
|
||||
}
|
||||
|
||||
export enum PlaybackType {
|
||||
LOCAL = 'local',
|
||||
WEB = 'web',
|
||||
export enum PlayerQueueType {
|
||||
DEFAULT = 'default',
|
||||
PRIORITY = 'priority',
|
||||
}
|
||||
|
||||
export enum PlayerRepeat {
|
||||
@@ -134,6 +130,16 @@ export enum PlayerStatus {
|
||||
PLAYING = 'playing',
|
||||
}
|
||||
|
||||
export enum PlayerStyle {
|
||||
CROSSFADE = 'crossfade',
|
||||
GAPLESS = 'gapless',
|
||||
}
|
||||
|
||||
export enum PlayerType {
|
||||
LOCAL = 'local',
|
||||
WEB = 'web',
|
||||
}
|
||||
|
||||
export enum TableColumn {
|
||||
ACTIONS = 'actions',
|
||||
ALBUM = 'album',
|
||||
|
||||
Reference in New Issue
Block a user