mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-21 18:06:30 +02:00
feat: added playlist from queue creation (#2210)
* feat: added playlist from queue creation - added functionality to create a playlist from queue - prefilling is done as an extra function to be api agnostic since navidrome native api for example does not offer a parameter for filling a playlist with songs on creation #2204 * fix: fixed wrong declaration
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
"collapseAllFolders": "Collapse all folders",
|
||||
"expandAllFolders": "Expand all folders",
|
||||
"createPlaylist": "Create $t(entity.playlist, {\"count\": 1})",
|
||||
"createPlaylistFromQueue": "Create $t(entity.playlist, {\"count\": 1}) from queue",
|
||||
"createRadioStation": "Create $t(entity.radioStation, {\"count\": 1})",
|
||||
"deletePlaylist": "Delete $t(entity.playlist, {\"count\": 1})",
|
||||
"deleteRadioStation": "Delete $t(entity.radioStation, {\"count\": 1})",
|
||||
@@ -365,6 +366,9 @@
|
||||
"success": "$t(entity.playlist, {\"count\": 1}) created successfully",
|
||||
"title": "Create $t(entity.playlist, {\"count\": 1})"
|
||||
},
|
||||
"createPrefilledPlaylist": {
|
||||
"title": "Create $t(entity.playlist, {\"count\": 1}) from selected songs"
|
||||
},
|
||||
"createRadioStation": {
|
||||
"success": "Radio station created successfully",
|
||||
"title": "Create radio station",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useIsFetching } from '@tanstack/react-query';
|
||||
import { t } from 'i18next';
|
||||
import { RefObject, useCallback } from 'react';
|
||||
import { MouseEvent, RefObject, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import styles from './play-queue-list-controls.module.css';
|
||||
@@ -10,6 +10,7 @@ import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-
|
||||
import { ItemListHandle } from '/@/renderer/components/item-list/types';
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import { useRestoreQueue, useSaveQueue } from '/@/renderer/features/player/hooks/use-queue-restore';
|
||||
import { openCreatePrefilledPlaylistModal } from '/@/renderer/features/playlists/components/create-playlist-form';
|
||||
import {
|
||||
ListConfigMenu,
|
||||
SONG_DISPLAY_TYPES,
|
||||
@@ -53,6 +54,7 @@ export const PlayQueueListControls = ({
|
||||
<Group gap="xs" style={{ flexShrink: 0 }} wrap="nowrap">
|
||||
<QueueRestoreActions />
|
||||
<QueuePlaybackIcons tableRef={tableRef} />
|
||||
<QueuePlaylistIcons />
|
||||
</Group>
|
||||
<Divider h="60%" orientation="vertical" style={{ alignSelf: 'center' }} />
|
||||
<Box style={{ display: 'flex', flex: 1, minWidth: 0 }}>
|
||||
@@ -84,6 +86,29 @@ export const PlayQueueListControls = ({
|
||||
);
|
||||
};
|
||||
|
||||
const QueuePlaylistIcons = () => {
|
||||
const server = useCurrentServer();
|
||||
const player = usePlayer();
|
||||
|
||||
const handleCreatePlaylistFromQueue = (e: MouseEvent<HTMLButtonElement>) => {
|
||||
const queueSongs = player.getQueue();
|
||||
|
||||
openCreatePrefilledPlaylistModal(server, queueSongs, e);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ActionIcon
|
||||
icon="playlistAdd"
|
||||
iconProps={{ size: 'lg' }}
|
||||
onClick={handleCreatePlaylistFromQueue}
|
||||
tooltip={{ label: t('action.createPlaylistFromQueue') }}
|
||||
variant="subtle"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const QueuePlaybackIcons = ({ tableRef }: { tableRef: RefObject<ItemListHandle | null> }) => {
|
||||
const { t } = useTranslation();
|
||||
const player = usePlayer();
|
||||
|
||||
@@ -60,6 +60,7 @@ export interface PlayerContext {
|
||||
clearQueue: () => void;
|
||||
clearSelected: (items: QueueSong[]) => void;
|
||||
decreaseVolume: (amount: number) => void;
|
||||
getQueue: () => QueueSong[];
|
||||
increaseVolume: (amount: number) => void;
|
||||
mediaNext: (toNextAlbum: boolean) => void;
|
||||
mediaPause: () => void;
|
||||
@@ -95,6 +96,7 @@ export const PlayerContext = createContext<PlayerContext>({
|
||||
clearQueue: () => {},
|
||||
clearSelected: () => {},
|
||||
decreaseVolume: () => {},
|
||||
getQueue: () => [],
|
||||
increaseVolume: () => {},
|
||||
mediaNext: () => {},
|
||||
mediaPause: () => {},
|
||||
@@ -567,6 +569,15 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
[storeActions],
|
||||
);
|
||||
|
||||
const getQueue = useCallback(() => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].clearQueue, {
|
||||
category: LogCategory.PLAYER,
|
||||
});
|
||||
|
||||
const queue = storeActions.getQueue();
|
||||
return queue.items;
|
||||
}, [storeActions]);
|
||||
|
||||
const increaseVolume = useCallback(
|
||||
(amount: number) => {
|
||||
logFn.debug(logMsg[LogCategory.PLAYER].increaseVolume, {
|
||||
@@ -853,6 +864,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
clearQueue,
|
||||
clearSelected,
|
||||
decreaseVolume,
|
||||
getQueue,
|
||||
increaseVolume,
|
||||
mediaNext,
|
||||
mediaPause,
|
||||
@@ -887,6 +899,7 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
clearQueue,
|
||||
clearSelected,
|
||||
decreaseVolume,
|
||||
getQueue,
|
||||
increaseVolume,
|
||||
mediaNext,
|
||||
mediaPause,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
PlaylistQueryBuilder,
|
||||
PlaylistQueryBuilderRef,
|
||||
} from '/@/renderer/features/playlists/components/playlist-query-builder';
|
||||
import { useAddToPlaylist } from '/@/renderer/features/playlists/mutations/add-to-playlist-mutation';
|
||||
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
|
||||
import { convertQueryGroupToNDQuery } from '/@/renderer/features/playlists/utils';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
@@ -24,17 +25,22 @@ import {
|
||||
CreatePlaylistBody,
|
||||
ServerListItem,
|
||||
ServerType,
|
||||
Song,
|
||||
SongListSort,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
|
||||
interface CreatePlaylistFormProps {
|
||||
onCancel: () => void;
|
||||
songs?: Song[];
|
||||
}
|
||||
|
||||
export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
||||
export const CreatePlaylistForm = ({ onCancel, songs }: CreatePlaylistFormProps) => {
|
||||
const { t } = useTranslation();
|
||||
const mutation = useCreatePlaylist({});
|
||||
|
||||
const createPlaylistMutation = useCreatePlaylist({});
|
||||
const addToPlaylistMutation = useAddToPlaylist({});
|
||||
|
||||
const server = useCurrentServer();
|
||||
const queryBuilderRef = useRef<PlaylistQueryBuilderRef>(null);
|
||||
|
||||
@@ -48,6 +54,8 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
||||
const [isSmartPlaylist, setIsSmartPlaylist] = useState(false);
|
||||
const [step, setStep] = useState<1 | 2>(1);
|
||||
|
||||
const isPrefilledPlaylist = !!songs && songs.length > 0;
|
||||
|
||||
const handleSubmit = form.onSubmit((values) => {
|
||||
if (!server) return;
|
||||
|
||||
@@ -78,7 +86,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
||||
}
|
||||
: undefined;
|
||||
|
||||
mutation.mutate(
|
||||
createPlaylistMutation.mutate(
|
||||
{
|
||||
apiClientProps: { serverId: server.id },
|
||||
body: {
|
||||
@@ -93,18 +101,45 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
||||
title: t('error.genericError'),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
onSuccess: (data) => {
|
||||
toast.success({
|
||||
message: t('form.createPlaylist.success'),
|
||||
});
|
||||
|
||||
handlePlaylistPrefilling(data?.id);
|
||||
|
||||
onCancel();
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
const handlePlaylistPrefilling = (playlistId?: string) => {
|
||||
if (!songs || !playlistId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const allSongIds = songs.map((song) => song.id);
|
||||
|
||||
addToPlaylistMutation.mutate(
|
||||
{
|
||||
apiClientProps: { serverId: server.id },
|
||||
body: { songId: allSongIds },
|
||||
query: { id: playlistId },
|
||||
},
|
||||
{
|
||||
onError: (err) => {
|
||||
toast.error({
|
||||
message: `${err.message}`,
|
||||
title: t('error.genericError'),
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const isPublicDisplayed = hasFeature(server, ServerFeature.PUBLIC_PLAYLIST);
|
||||
const isSubmitDisabled = !form.values.name || mutation.isPending;
|
||||
const isSubmitDisabled = !form.values.name || createPlaylistMutation.isPending;
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
@@ -141,7 +176,8 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
||||
/>
|
||||
)}
|
||||
{server?.type === ServerType.NAVIDROME &&
|
||||
hasFeature(server, ServerFeature.PLAYLISTS_SMART) && (
|
||||
hasFeature(server, ServerFeature.PLAYLISTS_SMART) &&
|
||||
!isPrefilledPlaylist && (
|
||||
<Switch
|
||||
checked={isSmartPlaylist}
|
||||
label="Is smart playlist?"
|
||||
@@ -182,7 +218,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
||||
</ModalButton>
|
||||
<ModalButton
|
||||
disabled={isSubmitDisabled}
|
||||
loading={mutation.isPending}
|
||||
loading={createPlaylistMutation.isPending}
|
||||
type="submit"
|
||||
variant="filled"
|
||||
>
|
||||
@@ -206,3 +242,17 @@ export const openCreatePlaylistModal = (
|
||||
title: t('form.createPlaylist.title'),
|
||||
});
|
||||
};
|
||||
|
||||
export const openCreatePrefilledPlaylistModal = (
|
||||
server?: ServerListItem,
|
||||
songs?: Song[],
|
||||
e?: MouseEvent<HTMLButtonElement>,
|
||||
) => {
|
||||
e?.stopPropagation();
|
||||
|
||||
openModal({
|
||||
children: <CreatePlaylistForm onCancel={() => closeAllModals()} songs={songs} />,
|
||||
size: server?.type === ServerType?.NAVIDROME ? 'xl' : 'sm',
|
||||
title: t('form.createPrefilledPlaylist.title'),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -45,10 +45,7 @@ interface Actions {
|
||||
getCurrentSong: () => QueueSong | undefined;
|
||||
getPlayerData: () => PlayerData;
|
||||
getQueue: (groupBy?: QueueGroupingProperty) => GroupedQueue;
|
||||
getQueueOrder: () => {
|
||||
groups: { count: number; name: string }[];
|
||||
items: QueueSong[];
|
||||
};
|
||||
getQueueOrder: () => GroupedQueue;
|
||||
increaseVolume: (value: number) => void;
|
||||
isFirstTrackInQueue: () => boolean;
|
||||
isLastTrackInQueue: () => boolean;
|
||||
|
||||
Reference in New Issue
Block a user