2025-06-24 00:04:36 -07:00
|
|
|
import { closeAllModals, openModal } from '@mantine/modals';
|
2025-06-29 18:56:46 -07:00
|
|
|
import clsx from 'clsx';
|
2025-06-24 00:04:36 -07:00
|
|
|
import { MouseEvent, useCallback, useMemo, useState } from 'react';
|
2023-10-30 19:22:45 -07:00
|
|
|
import { useTranslation } from 'react-i18next';
|
2023-02-05 18:59:39 -08:00
|
|
|
import { generatePath } from 'react-router';
|
|
|
|
|
import { Link } from 'react-router-dom';
|
2025-05-18 14:03:18 -07:00
|
|
|
|
2025-06-24 00:04:36 -07:00
|
|
|
import styles from './sidebar-playlist-list.module.css';
|
|
|
|
|
|
2023-02-05 18:59:39 -08:00
|
|
|
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
2025-06-24 00:04:36 -07:00
|
|
|
import { CreatePlaylistForm, usePlaylistList } from '/@/renderer/features/playlists';
|
|
|
|
|
import { SidebarItem } from '/@/renderer/features/sidebar/components/sidebar-item';
|
2025-05-18 14:03:18 -07:00
|
|
|
import { AppRoute } from '/@/renderer/router/routes';
|
2025-06-24 00:04:36 -07:00
|
|
|
import { useCurrentServer } from '/@/renderer/store';
|
|
|
|
|
import { Accordion } from '/@/shared/components/accordion/accordion';
|
|
|
|
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
|
|
|
|
import { ButtonProps } from '/@/shared/components/button/button';
|
|
|
|
|
import { Group } from '/@/shared/components/group/group';
|
|
|
|
|
import { Text } from '/@/shared/components/text/text';
|
|
|
|
|
import {
|
|
|
|
|
LibraryItem,
|
|
|
|
|
Playlist,
|
|
|
|
|
PlaylistListSort,
|
|
|
|
|
ServerType,
|
|
|
|
|
SortOrder,
|
|
|
|
|
} from '/@/shared/types/domain-types';
|
2025-05-20 19:23:36 -07:00
|
|
|
import { Play } from '/@/shared/types/types';
|
2023-02-05 18:59:39 -08:00
|
|
|
|
2025-06-24 00:04:36 -07:00
|
|
|
interface PlaylistRowButtonProps extends Omit<ButtonProps, 'onPlay'> {
|
|
|
|
|
name: string;
|
|
|
|
|
onPlay: (id: string, playType: Play.LAST | Play.NEXT | Play.NOW | Play.SHUFFLE) => void;
|
|
|
|
|
to: string;
|
|
|
|
|
}
|
2024-02-18 20:22:38 -08:00
|
|
|
|
2025-06-24 00:04:36 -07:00
|
|
|
const PlaylistRowButton = ({ name, onPlay, to, ...props }: PlaylistRowButtonProps) => {
|
|
|
|
|
const url = generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: to });
|
2024-02-18 20:22:38 -08:00
|
|
|
|
2025-06-24 00:04:36 -07:00
|
|
|
const [isHovered, setIsHovered] = useState(false);
|
2023-07-15 15:57:40 -07:00
|
|
|
|
2023-07-01 19:10:05 -07:00
|
|
|
return (
|
2024-08-25 22:17:11 -07:00
|
|
|
<div
|
2025-06-24 00:04:36 -07:00
|
|
|
className={styles.row}
|
|
|
|
|
onMouseEnter={() => setIsHovered(true)}
|
|
|
|
|
onMouseLeave={() => setIsHovered(false)}
|
|
|
|
|
>
|
|
|
|
|
<SidebarItem
|
2025-06-29 18:56:46 -07:00
|
|
|
className={clsx({
|
|
|
|
|
[styles.rowHover]: isHovered,
|
|
|
|
|
})}
|
2025-06-24 00:04:36 -07:00
|
|
|
to={url}
|
|
|
|
|
variant="subtle"
|
|
|
|
|
{...props}
|
|
|
|
|
>
|
|
|
|
|
{name}
|
|
|
|
|
</SidebarItem>
|
2025-07-12 11:17:54 -07:00
|
|
|
{isHovered && <RowControls id={to} onPlay={onPlay} />}
|
2025-06-24 00:04:36 -07:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
2024-08-25 22:17:11 -07:00
|
|
|
|
2025-06-24 00:04:36 -07:00
|
|
|
const RowControls = ({
|
|
|
|
|
id,
|
|
|
|
|
onPlay,
|
|
|
|
|
}: {
|
|
|
|
|
id: string;
|
|
|
|
|
onPlay: (id: string, playType: Play) => void;
|
|
|
|
|
}) => {
|
|
|
|
|
const { t } = useTranslation();
|
2024-08-25 22:17:11 -07:00
|
|
|
|
2025-06-24 00:04:36 -07:00
|
|
|
return (
|
2025-07-12 11:17:54 -07:00
|
|
|
<Group className={styles.controls} gap="xs" wrap="nowrap">
|
2025-06-24 00:04:36 -07:00
|
|
|
<ActionIcon
|
|
|
|
|
icon="mediaPlay"
|
|
|
|
|
iconProps={{
|
|
|
|
|
size: 'md',
|
|
|
|
|
}}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (!id) return;
|
|
|
|
|
onPlay(id, Play.NOW);
|
|
|
|
|
}}
|
|
|
|
|
size="xs"
|
|
|
|
|
tooltip={{
|
|
|
|
|
label: t('player.play', { postProcess: 'sentenceCase' }),
|
|
|
|
|
openDelay: 500,
|
|
|
|
|
}}
|
|
|
|
|
variant="subtle"
|
|
|
|
|
/>
|
|
|
|
|
<ActionIcon
|
|
|
|
|
icon="mediaShuffle"
|
|
|
|
|
iconProps={{
|
|
|
|
|
size: 'md',
|
|
|
|
|
}}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (!id) return;
|
|
|
|
|
onPlay(id, Play.SHUFFLE);
|
|
|
|
|
}}
|
|
|
|
|
size="xs"
|
|
|
|
|
tooltip={{
|
|
|
|
|
label: t('player.shuffle', { postProcess: 'sentenceCase' }),
|
|
|
|
|
openDelay: 500,
|
|
|
|
|
}}
|
|
|
|
|
variant="subtle"
|
|
|
|
|
/>
|
|
|
|
|
<ActionIcon
|
|
|
|
|
icon="mediaPlayLast"
|
|
|
|
|
iconProps={{
|
|
|
|
|
size: 'md',
|
|
|
|
|
}}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (!id) return;
|
|
|
|
|
onPlay(id, Play.LAST);
|
2023-07-01 19:10:05 -07:00
|
|
|
}}
|
2025-06-24 00:04:36 -07:00
|
|
|
size="xs"
|
|
|
|
|
tooltip={{
|
|
|
|
|
label: t('player.addLast', { postProcess: 'sentenceCase' }),
|
|
|
|
|
openDelay: 500,
|
|
|
|
|
}}
|
|
|
|
|
variant="subtle"
|
|
|
|
|
/>
|
|
|
|
|
<ActionIcon
|
|
|
|
|
icon="mediaPlayNext"
|
|
|
|
|
iconProps={{
|
|
|
|
|
size: 'md',
|
|
|
|
|
}}
|
|
|
|
|
onClick={() => {
|
|
|
|
|
if (!id) return;
|
|
|
|
|
onPlay(id, Play.NEXT);
|
|
|
|
|
}}
|
|
|
|
|
size="xs"
|
|
|
|
|
tooltip={{
|
|
|
|
|
label: t('player.addNext', { postProcess: 'sentenceCase' }),
|
|
|
|
|
openDelay: 500,
|
|
|
|
|
}}
|
|
|
|
|
variant="subtle"
|
|
|
|
|
/>
|
|
|
|
|
</Group>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const SidebarPlaylistList = () => {
|
|
|
|
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
|
|
|
|
const { t } = useTranslation();
|
|
|
|
|
const server = useCurrentServer();
|
|
|
|
|
|
|
|
|
|
const playlistsQuery = usePlaylistList({
|
|
|
|
|
query: {
|
|
|
|
|
sortBy: PlaylistListSort.NAME,
|
|
|
|
|
sortOrder: SortOrder.ASC,
|
|
|
|
|
startIndex: 0,
|
|
|
|
|
},
|
|
|
|
|
serverId: server?.id,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const handlePlayPlaylist = useCallback(
|
|
|
|
|
(id: string, playType: Play) => {
|
|
|
|
|
handlePlayQueueAdd?.({
|
|
|
|
|
byItemType: {
|
|
|
|
|
id: [id],
|
|
|
|
|
type: LibraryItem.PLAYLIST,
|
|
|
|
|
},
|
|
|
|
|
playType,
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
[handlePlayQueueAdd],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const data = playlistsQuery.data;
|
|
|
|
|
|
|
|
|
|
const memoizedItemData = useMemo(() => {
|
|
|
|
|
const base = { handlePlay: handlePlayPlaylist };
|
|
|
|
|
|
|
|
|
|
if (!server?.type || !server?.username || !data?.items) {
|
|
|
|
|
return { ...base, items: data?.items };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const owned: Array<[boolean, () => void] | Playlist> = [];
|
|
|
|
|
|
|
|
|
|
for (const playlist of data.items) {
|
2025-06-25 08:19:22 -07:00
|
|
|
if (!playlist.owner || playlist.owner === server.username) {
|
|
|
|
|
owned.push(playlist);
|
|
|
|
|
}
|
2025-06-24 00:04:36 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { ...base, items: owned };
|
|
|
|
|
}, [data?.items, handlePlayPlaylist, server?.type, server?.username]);
|
|
|
|
|
|
|
|
|
|
const handleCreatePlaylistModal = (e: MouseEvent<HTMLButtonElement>) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
|
|
|
|
|
openModal({
|
|
|
|
|
children: <CreatePlaylistForm onCancel={() => closeAllModals()} />,
|
|
|
|
|
size: server?.type === ServerType?.NAVIDROME ? 'lg' : 'sm',
|
|
|
|
|
title: t('form.createPlaylist.title', { postProcess: 'titleCase' }),
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Accordion.Item value="playlists">
|
2025-07-12 11:17:54 -07:00
|
|
|
<Accordion.Control component="div" role="button" style={{ userSelect: 'none' }}>
|
|
|
|
|
<Group justify="space-between" pr="var(--theme-spacing-md)">
|
2025-06-24 00:04:36 -07:00
|
|
|
<Text fw={600}>
|
|
|
|
|
{t('page.sidebar.playlists', {
|
|
|
|
|
postProcess: 'titleCase',
|
|
|
|
|
})}
|
|
|
|
|
</Text>
|
|
|
|
|
<Group gap="xs">
|
|
|
|
|
<ActionIcon
|
|
|
|
|
icon="add"
|
|
|
|
|
iconProps={{
|
|
|
|
|
size: 'lg',
|
|
|
|
|
}}
|
|
|
|
|
onClick={handleCreatePlaylistModal}
|
|
|
|
|
size="xs"
|
|
|
|
|
tooltip={{
|
|
|
|
|
label: t('action.createPlaylist', {
|
|
|
|
|
postProcess: 'sentenceCase',
|
|
|
|
|
}),
|
|
|
|
|
openDelay: 500,
|
|
|
|
|
}}
|
|
|
|
|
variant="subtle"
|
|
|
|
|
/>
|
|
|
|
|
<ActionIcon
|
|
|
|
|
component={Link}
|
|
|
|
|
icon="list"
|
|
|
|
|
iconProps={{
|
|
|
|
|
size: 'lg',
|
|
|
|
|
}}
|
|
|
|
|
onClick={(e) => e.stopPropagation()}
|
|
|
|
|
size="xs"
|
|
|
|
|
to={AppRoute.PLAYLISTS}
|
|
|
|
|
tooltip={{
|
|
|
|
|
label: t('action.viewPlaylists', {
|
|
|
|
|
postProcess: 'sentenceCase',
|
|
|
|
|
}),
|
|
|
|
|
openDelay: 500,
|
|
|
|
|
}}
|
|
|
|
|
variant="subtle"
|
|
|
|
|
/>
|
|
|
|
|
</Group>
|
2023-07-01 19:10:05 -07:00
|
|
|
</Group>
|
2025-06-24 00:04:36 -07:00
|
|
|
</Accordion.Control>
|
|
|
|
|
<Accordion.Panel>
|
|
|
|
|
{memoizedItemData?.items?.map((item, index) => (
|
|
|
|
|
<PlaylistRowButton
|
|
|
|
|
key={index}
|
|
|
|
|
name={item.name}
|
|
|
|
|
onPlay={handlePlayPlaylist}
|
|
|
|
|
to={item.id}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</Accordion.Panel>
|
|
|
|
|
</Accordion.Item>
|
2023-07-01 19:10:05 -07:00
|
|
|
);
|
2023-03-28 23:59:51 -07:00
|
|
|
};
|
2023-02-05 18:59:39 -08:00
|
|
|
|
2025-06-24 00:04:36 -07:00
|
|
|
export const SidebarSharedPlaylistList = () => {
|
2023-07-01 19:10:05 -07:00
|
|
|
const handlePlayQueueAdd = usePlayQueueAdd();
|
2025-06-24 00:04:36 -07:00
|
|
|
const { t } = useTranslation();
|
2024-09-26 04:23:08 +00:00
|
|
|
const server = useCurrentServer();
|
|
|
|
|
|
|
|
|
|
const playlistsQuery = usePlaylistList({
|
|
|
|
|
query: {
|
|
|
|
|
sortBy: PlaylistListSort.NAME,
|
|
|
|
|
sortOrder: SortOrder.ASC,
|
|
|
|
|
startIndex: 0,
|
|
|
|
|
},
|
|
|
|
|
serverId: server?.id,
|
|
|
|
|
});
|
2023-02-05 18:59:39 -08:00
|
|
|
|
2023-07-01 19:10:05 -07:00
|
|
|
const handlePlayPlaylist = useCallback(
|
|
|
|
|
(id: string, playType: Play) => {
|
|
|
|
|
handlePlayQueueAdd?.({
|
|
|
|
|
byItemType: {
|
|
|
|
|
id: [id],
|
|
|
|
|
type: LibraryItem.PLAYLIST,
|
|
|
|
|
},
|
|
|
|
|
playType,
|
|
|
|
|
});
|
2023-02-08 03:44:37 -08:00
|
|
|
},
|
2023-07-01 19:10:05 -07:00
|
|
|
[handlePlayQueueAdd],
|
|
|
|
|
);
|
2023-02-08 03:44:37 -08:00
|
|
|
|
2024-09-26 04:23:08 +00:00
|
|
|
const data = playlistsQuery.data;
|
|
|
|
|
|
2023-07-01 19:10:05 -07:00
|
|
|
const memoizedItemData = useMemo(() => {
|
2024-09-26 04:23:08 +00:00
|
|
|
const base = { handlePlay: handlePlayPlaylist };
|
2024-02-18 20:22:38 -08:00
|
|
|
|
2024-09-26 04:23:08 +00:00
|
|
|
if (!server?.type || !server?.username || !data?.items) {
|
2024-02-18 20:22:38 -08:00
|
|
|
return { ...base, items: data?.items };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const shared: Playlist[] = [];
|
|
|
|
|
|
|
|
|
|
for (const playlist of data.items) {
|
2024-09-26 04:23:08 +00:00
|
|
|
if (playlist.owner && playlist.owner !== server.username) {
|
2024-02-18 20:22:38 -08:00
|
|
|
shared.push(playlist);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-06-24 00:04:36 -07:00
|
|
|
return { ...base, items: shared };
|
|
|
|
|
}, [data?.items, handlePlayPlaylist, server?.type, server?.username]);
|
2024-02-18 20:22:38 -08:00
|
|
|
|
2025-06-24 00:04:36 -07:00
|
|
|
if (memoizedItemData?.items?.length === 0) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2023-02-05 18:59:39 -08:00
|
|
|
|
2023-07-01 19:10:05 -07:00
|
|
|
return (
|
2025-06-24 00:04:36 -07:00
|
|
|
<Accordion.Item value="shared-playlists">
|
|
|
|
|
<Accordion.Control>
|
2025-07-12 11:17:54 -07:00
|
|
|
<Text fw={600} variant="secondary">
|
2025-06-24 00:04:36 -07:00
|
|
|
{t('page.sidebar.shared', {
|
|
|
|
|
postProcess: 'titleCase',
|
|
|
|
|
})}
|
|
|
|
|
</Text>
|
|
|
|
|
</Accordion.Control>
|
|
|
|
|
<Accordion.Panel>
|
|
|
|
|
{memoizedItemData?.items?.map((item, index) => (
|
|
|
|
|
<PlaylistRowButton
|
|
|
|
|
key={index}
|
|
|
|
|
name={item.name}
|
|
|
|
|
onPlay={handlePlayPlaylist}
|
|
|
|
|
to={item.id}
|
|
|
|
|
/>
|
|
|
|
|
))}
|
|
|
|
|
</Accordion.Panel>
|
|
|
|
|
</Accordion.Item>
|
2023-07-01 19:10:05 -07:00
|
|
|
);
|
2023-02-05 18:59:39 -08:00
|
|
|
};
|