mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 02:13:33 +00:00
Migrate to Mantine v8 and Design Changes (#961)
* mantine v8 migration * various design changes and improvements
This commit is contained in:
parent
bea55d48a8
commit
c1330d92b2
473 changed files with 12469 additions and 11607 deletions
|
|
@ -1,4 +1,3 @@
|
|||
import { Box, Group, Stack } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { closeModal, ContextModalProps } from '@mantine/modals';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
|
@ -6,12 +5,17 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { Button, MultiSelect, Switch, toast } from '/@/renderer/components';
|
||||
import { getGenreSongsById } from '/@/renderer/features/player';
|
||||
import { useAddToPlaylist } from '/@/renderer/features/playlists/mutations/add-to-playlist-mutation';
|
||||
import { usePlaylistList } from '/@/renderer/features/playlists/queries/playlist-list-query';
|
||||
import { queryClient } from '/@/renderer/lib/react-query';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { MultiSelect } from '/@/shared/components/multi-select/multi-select';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import {
|
||||
PlaylistListSort,
|
||||
SongListQuery,
|
||||
|
|
@ -218,7 +222,7 @@ export const AddToPlaylistContextModal = ({
|
|||
});
|
||||
|
||||
return (
|
||||
<Box p="1rem">
|
||||
<div style={{ padding: '1rem' }}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<Stack>
|
||||
<MultiSelect
|
||||
|
|
@ -240,29 +244,27 @@ export const AddToPlaylistContextModal = ({
|
|||
})}
|
||||
{...form.getInputProps('skipDuplicates', { type: 'checkbox' })}
|
||||
/>
|
||||
<Group position="right">
|
||||
<Group>
|
||||
<Button
|
||||
disabled={addToPlaylistMutation.isLoading}
|
||||
onClick={() => closeModal(id)}
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
{t('common.cancel', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isSubmitDisabled}
|
||||
loading={isLoading}
|
||||
size="md"
|
||||
type="submit"
|
||||
variant="filled"
|
||||
>
|
||||
{t('common.add', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
disabled={addToPlaylistMutation.isLoading}
|
||||
onClick={() => closeModal(id)}
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
{t('common.cancel', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={isSubmitDisabled}
|
||||
loading={isLoading}
|
||||
size="md"
|
||||
type="submit"
|
||||
variant="filled"
|
||||
>
|
||||
{t('common.add', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</form>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
import { Group, Stack } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button, Switch, Text, TextInput, toast } from '/@/renderer/components';
|
||||
import {
|
||||
PlaylistQueryBuilder,
|
||||
PlaylistQueryBuilderRef,
|
||||
|
|
@ -12,6 +10,14 @@ import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/crea
|
|||
import { convertQueryGroupToNDQuery } from '/@/renderer/features/playlists/utils';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
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 { toast } from '/@/shared/components/toast/toast';
|
||||
import { CreatePlaylistBody, ServerType, SongListSort } from '/@/shared/types/domain-types';
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
|
||||
|
|
@ -104,11 +110,13 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
|||
{...form.getInputProps('name')}
|
||||
/>
|
||||
{server?.type === ServerType.NAVIDROME && (
|
||||
<TextInput
|
||||
<Textarea
|
||||
autosize
|
||||
label={t('form.createPlaylist.input', {
|
||||
context: 'description',
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
minRows={5}
|
||||
{...form.getInputProps('comment')}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -146,7 +154,7 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
|
|||
</Stack>
|
||||
)}
|
||||
|
||||
<Group position="right">
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
variant="subtle"
|
||||
|
|
|
|||
|
|
@ -10,14 +10,13 @@ import type {
|
|||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import { MutableRefObject, useCallback, useMemo } from 'react';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { toast } from '/@/renderer/components';
|
||||
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
|
||||
import { getColumnDefs, TablePagination, VirtualTable } from '/@/renderer/components/virtual-table';
|
||||
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
|
||||
|
|
@ -40,6 +39,7 @@ import {
|
|||
useSetPlaylistDetailTablePagination,
|
||||
} from '/@/renderer/store';
|
||||
import { PersistedTableColumn, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import {
|
||||
LibraryItem,
|
||||
PlaylistSongListQuery,
|
||||
|
|
|
|||
|
|
@ -1,45 +1,28 @@
|
|||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import { IDatasource } from '@ag-grid-community/core';
|
||||
import { Divider, Flex, Group, Stack } from '@mantine/core';
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { MouseEvent, MutableRefObject, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
RiAddBoxFill,
|
||||
RiAddCircleFill,
|
||||
RiDeleteBinFill,
|
||||
RiEditFill,
|
||||
RiMoreFill,
|
||||
RiPlayFill,
|
||||
RiRefreshLine,
|
||||
RiSettings3Fill,
|
||||
} from 'react-icons/ri';
|
||||
import { useNavigate, useParams } from 'react-router';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import {
|
||||
Button,
|
||||
ConfirmModal,
|
||||
DropdownMenu,
|
||||
MultiSelect,
|
||||
Slider,
|
||||
Switch,
|
||||
Text,
|
||||
toast,
|
||||
} from '/@/renderer/components';
|
||||
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
|
||||
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
|
||||
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
||||
import { OrderToggleButton } from '/@/renderer/features/shared';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import {
|
||||
PersistedTableColumn,
|
||||
SongListFilter,
|
||||
useCurrentServer,
|
||||
usePlaylistDetailStore,
|
||||
|
|
@ -48,6 +31,15 @@ import {
|
|||
useSetPlaylistStore,
|
||||
useSetPlaylistTablePagination,
|
||||
} from '/@/renderer/store';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { ConfirmModal } from '/@/shared/components/modal/modal';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import {
|
||||
LibraryItem,
|
||||
PlaylistSongListQuery,
|
||||
|
|
@ -55,7 +47,7 @@ import {
|
|||
SongListSort,
|
||||
SortOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ListDisplayType, Play, TableColumn } from '/@/shared/types/types';
|
||||
import { ListDisplayType, Play } from '/@/shared/types/types';
|
||||
|
||||
const FILTERS = {
|
||||
jellyfin: [
|
||||
|
|
@ -303,6 +295,8 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
setTable({ rowHeight: e });
|
||||
};
|
||||
|
||||
const debouncedHandleItemSize = debounce(handleItemSize, 20);
|
||||
|
||||
const handleFilterChange = useCallback(
|
||||
async (filters: SongListFilter) => {
|
||||
if (server?.type !== ServerType.SUBSONIC) {
|
||||
|
|
@ -392,14 +386,13 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
}, [filters.sortOrder, handleFilterChange, playlistId, setFilter]);
|
||||
|
||||
const handleSetViewType = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value) return;
|
||||
setPage({ detail: { ...page, display: e.currentTarget.value as ListDisplayType } });
|
||||
(displayType: ListDisplayType) => {
|
||||
setPage({ detail: { ...page, display: displayType } });
|
||||
},
|
||||
[page, setPage],
|
||||
);
|
||||
|
||||
const handleTableColumns = (values: TableColumn[]) => {
|
||||
const handleTableColumns = (values: string[]) => {
|
||||
const existingColumns = page.table.columns;
|
||||
|
||||
if (values.length === 0) {
|
||||
|
|
@ -410,7 +403,10 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
|
||||
// If adding a column
|
||||
if (values.length > existingColumns.length) {
|
||||
const newColumn = { column: values[values.length - 1], width: 100 };
|
||||
const newColumn = {
|
||||
column: values[values.length - 1],
|
||||
width: 100,
|
||||
} as PersistedTableColumn;
|
||||
|
||||
setTable({ columns: [...existingColumns, newColumn] });
|
||||
} else {
|
||||
|
|
@ -424,10 +420,10 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
return tableRef.current?.api.sizeColumnsToFit();
|
||||
};
|
||||
|
||||
const handleAutoFitColumns = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setTable({ autoFit: e.currentTarget.checked });
|
||||
const handleAutoFitColumns = (autoFitColumns: boolean) => {
|
||||
setTable({ autoFit: autoFitColumns });
|
||||
|
||||
if (e.currentTarget.checked) {
|
||||
if (autoFitColumns) {
|
||||
tableRef.current?.api.sizeColumnsToFit();
|
||||
}
|
||||
};
|
||||
|
|
@ -474,16 +470,13 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
return (
|
||||
<Flex justify="space-between">
|
||||
<Group
|
||||
gap="sm"
|
||||
ref={cq.ref}
|
||||
spacing="sm"
|
||||
w="100%"
|
||||
>
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
size="md"
|
||||
tooltip={{
|
||||
label: t('page.playlist.reorder', { postProcess: 'sentenceCase' }),
|
||||
}}
|
||||
|
|
@ -495,7 +488,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
<DropdownMenu.Dropdown>
|
||||
{FILTERS[server?.type as keyof typeof FILTERS].map((filter) => (
|
||||
<DropdownMenu.Item
|
||||
$isActive={filter.value === filters.sortBy}
|
||||
isSelected={filter.value === filters.sortBy}
|
||||
key={`filter-${filter.name}`}
|
||||
onClick={handleSetSortBy}
|
||||
value={filter.value}
|
||||
|
|
@ -511,40 +504,32 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
onToggle={handleToggleSortOrder}
|
||||
sortOrder={filters.sortOrder || SortOrder.ASC}
|
||||
/>
|
||||
<Divider orientation="vertical" />
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
<RiMoreFill size="1.3rem" />
|
||||
</Button>
|
||||
<MoreButton />
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiPlayFill />}
|
||||
leftSection={<Icon icon="mediaPlay" />}
|
||||
onClick={() => handlePlay(Play.NOW)}
|
||||
>
|
||||
{t('player.play', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiAddBoxFill />}
|
||||
leftSection={<Icon icon="mediaPlayLast" />}
|
||||
onClick={() => handlePlay(Play.LAST)}
|
||||
>
|
||||
{t('player.addLast', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiAddCircleFill />}
|
||||
leftSection={<Icon icon="mediaPlayNext" />}
|
||||
onClick={() => handlePlay(Play.NEXT)}
|
||||
>
|
||||
{t('player.addNext', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
icon={<RiEditFill />}
|
||||
leftSection={<Icon icon="edit" />}
|
||||
onClick={() =>
|
||||
openUpdatePlaylistModal({
|
||||
playlist: detailQuery.data!,
|
||||
|
|
@ -555,14 +540,14 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
{t('action.editPlaylist', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiDeleteBinFill />}
|
||||
leftSection={<Icon icon="delete" />}
|
||||
onClick={openDeletePlaylistModal}
|
||||
>
|
||||
{t('action.deletePlaylist', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
icon={<RiRefreshLine />}
|
||||
leftSection={<Icon icon="refresh" />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
{t('action.refresh', { postProcess: 'sentenceCase' })}
|
||||
|
|
@ -571,7 +556,7 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
<>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
$danger
|
||||
isDanger
|
||||
onClick={handleToggleShowQueryBuilder}
|
||||
>
|
||||
{t('action.toggleSmartPlaylistEditor', {
|
||||
|
|
@ -584,82 +569,18 @@ export const PlaylistDetailSongListHeaderFilters = ({
|
|||
</DropdownMenu>
|
||||
</Group>
|
||||
<Group>
|
||||
<DropdownMenu
|
||||
position="bottom-end"
|
||||
width={425}
|
||||
>
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
<RiSettings3Fill size="1.3rem" />
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.general.displayType', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
$isActive={page.display === ListDisplayType.TABLE}
|
||||
onClick={handleSetViewType}
|
||||
value={ListDisplayType.TABLE}
|
||||
>
|
||||
Table
|
||||
</DropdownMenu.Item>
|
||||
{/* <DropdownMenu.Item
|
||||
$isActive={page.display === ListDisplayType.TABLE_PAGINATED}
|
||||
value={ListDisplayType.TABLE_PAGINATED}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
Table (paginated)
|
||||
</DropdownMenu.Item> */}
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.general.itemSize', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
<Slider
|
||||
defaultValue={page.table.rowHeight}
|
||||
label={null}
|
||||
max={100}
|
||||
min={25}
|
||||
onChangeEnd={handleItemSize}
|
||||
/>
|
||||
</DropdownMenu.Item>
|
||||
{(page.display === ListDisplayType.TABLE ||
|
||||
page.display === ListDisplayType.TABLE_PAGINATED) && (
|
||||
<>
|
||||
<DropdownMenu.Label>Table Columns</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
closeMenuOnClick={false}
|
||||
component="div"
|
||||
sx={{ cursor: 'default' }}
|
||||
>
|
||||
<Stack>
|
||||
<MultiSelect
|
||||
clearable
|
||||
data={SONG_TABLE_COLUMNS}
|
||||
defaultValue={page.table?.columns.map(
|
||||
(column) => column.column,
|
||||
)}
|
||||
onChange={handleTableColumns}
|
||||
width={300}
|
||||
/>
|
||||
<Group position="apart">
|
||||
<Text>Auto Fit Columns</Text>
|
||||
<Switch
|
||||
defaultChecked={page.table.autoFit}
|
||||
onChange={handleAutoFitColumns}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
<ListConfigMenu
|
||||
autoFitColumns={page.table.autoFit}
|
||||
disabledViewTypes={[ListDisplayType.GRID, ListDisplayType.LIST]}
|
||||
displayType={page.display}
|
||||
itemSize={page.table.rowHeight}
|
||||
onChangeAutoFitColumns={handleAutoFitColumns}
|
||||
onChangeDisplayType={handleSetViewType}
|
||||
onChangeItemSize={debouncedHandleItemSize}
|
||||
onChangeTableColumns={handleTableColumns}
|
||||
tableColumns={page.table.columns.map((column) => column.column)}
|
||||
tableColumnsData={SONG_TABLE_COLUMNS}
|
||||
/>
|
||||
</Group>
|
||||
</Flex>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import { Stack } from '@mantine/core';
|
||||
import { MutableRefObject } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router';
|
||||
|
||||
import { Badge, PageHeader, Paper, SpinnerIcon } from '/@/renderer/components';
|
||||
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
||||
import { usePlayQueueAdd } from '/@/renderer/features/player';
|
||||
import { PlaylistDetailSongListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-detail-song-list-header-filters';
|
||||
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
|
||||
import { LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
|
||||
import { Badge } from '/@/shared/components/badge/badge';
|
||||
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
|
|
@ -45,32 +47,27 @@ export const PlaylistDetailSongListHeader = ({
|
|||
const isSmartPlaylist = detailQuery?.data?.rules;
|
||||
|
||||
return (
|
||||
<Stack spacing={0}>
|
||||
<PageHeader backgroundColor="var(--titlebar-bg)">
|
||||
<Stack gap={0}>
|
||||
<PageHeader>
|
||||
<LibraryHeaderBar>
|
||||
<LibraryHeaderBar.PlayButton onClick={() => handlePlay(playButtonBehavior)} />
|
||||
<LibraryHeaderBar.Title>{detailQuery?.data?.name}</LibraryHeaderBar.Title>
|
||||
<Paper
|
||||
fw="600"
|
||||
px="1rem"
|
||||
py="0.3rem"
|
||||
radius="sm"
|
||||
>
|
||||
<Badge>
|
||||
{itemCount === null || itemCount === undefined ? (
|
||||
<SpinnerIcon />
|
||||
) : (
|
||||
itemCount
|
||||
)}
|
||||
</Paper>
|
||||
</Badge>
|
||||
{isSmartPlaylist && <Badge size="lg">{t('entity.smartPlaylist')}</Badge>}
|
||||
</LibraryHeaderBar>
|
||||
</PageHeader>
|
||||
<Paper p="1rem">
|
||||
<FilterBar>
|
||||
<PlaylistDetailSongListHeaderFilters
|
||||
handleToggleShowQueryBuilder={handleToggleShowQueryBuilder}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</Paper>
|
||||
</FilterBar>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
|
|||
|
||||
import { lazy, MutableRefObject, Suspense } from 'react';
|
||||
|
||||
import { Spinner } from '/@/renderer/components';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { useListStoreByKey } from '/@/renderer/store/list.store';
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
import { ListDisplayType } from '/@/shared/types/types';
|
||||
|
||||
const PlaylistListTableView = lazy(() =>
|
||||
|
|
@ -32,7 +32,7 @@ export const PlaylistListContent = ({ gridRef, itemCount, tableRef }: PlaylistLi
|
|||
|
||||
return (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
{display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? (
|
||||
{display === ListDisplayType.CARD || display === ListDisplayType.GRID ? (
|
||||
<PlaylistListGridView
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { ListOnScrollProps } from 'react-window';
|
|||
|
||||
import { controller } from '/@/renderer/api/controller';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { PLAYLIST_CARD_ROWS } from '/@/renderer/components';
|
||||
import { PLAYLIST_CARD_ROWS } from '/@/renderer/components/card/card-rows';
|
||||
import {
|
||||
VirtualGridAutoSizerContainer,
|
||||
VirtualInfiniteGrid,
|
||||
|
|
|
|||
|
|
@ -1,30 +1,45 @@
|
|||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import { IDatasource } from '@ag-grid-community/core';
|
||||
import { Divider, Flex, Group, Stack } from '@mantine/core';
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { MouseEvent, MutableRefObject, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react-icons/ri';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { PLAYLIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { CreatePlaylistForm } from '/@/renderer/features/playlists/components/create-playlist-form';
|
||||
import { OrderToggleButton } from '/@/renderer/features/shared';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
|
||||
import { RefreshButton } from '/@/renderer/features/shared/components/refresh-button';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { PlaylistListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store';
|
||||
import {
|
||||
PersistedTableColumn,
|
||||
PlaylistListFilter,
|
||||
useCurrentServer,
|
||||
useListStoreActions,
|
||||
} from '/@/renderer/store';
|
||||
import { useListStoreByKey } from '/@/renderer/store/list.store';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import {
|
||||
LibraryItem,
|
||||
PlaylistListQuery,
|
||||
PlaylistListSort,
|
||||
ServerType,
|
||||
SortOrder,
|
||||
} from '/@/shared/types/domain-types';
|
||||
import { ListDisplayType, TableColumn } from '/@/shared/types/types';
|
||||
import { ListDisplayType } from '/@/shared/types/types';
|
||||
|
||||
const FILTERS = {
|
||||
jellyfin: [
|
||||
|
|
@ -128,7 +143,7 @@ export const PlaylistListHeaderFilters = ({
|
|||
const { display, filter, grid, table } = useListStoreByKey<PlaylistListQuery>({ key: pageKey });
|
||||
const cq = useContainerQuery();
|
||||
|
||||
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER;
|
||||
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.GRID;
|
||||
|
||||
const sortByLabel =
|
||||
(server?.type &&
|
||||
|
|
@ -267,14 +282,13 @@ export const PlaylistListHeaderFilters = ({
|
|||
}, [filter.sortOrder, handleFilterChange, pageKey, setFilter]);
|
||||
|
||||
const handleSetViewType = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value) return;
|
||||
setDisplayType({ data: e.currentTarget.value as ListDisplayType, key: pageKey });
|
||||
(displayType: ListDisplayType) => {
|
||||
setDisplayType({ data: displayType, key: pageKey });
|
||||
},
|
||||
[pageKey, setDisplayType],
|
||||
);
|
||||
|
||||
const handleTableColumns = (values: TableColumn[]) => {
|
||||
const handleTableColumns = (values: string[]) => {
|
||||
const existingColumns = table.columns;
|
||||
|
||||
if (values.length === 0) {
|
||||
|
|
@ -286,7 +300,10 @@ export const PlaylistListHeaderFilters = ({
|
|||
|
||||
// If adding a column
|
||||
if (values.length > existingColumns.length) {
|
||||
const newColumn = { column: values[values.length - 1], width: 100 };
|
||||
const newColumn = {
|
||||
column: values[values.length - 1],
|
||||
width: 100,
|
||||
} as PersistedTableColumn;
|
||||
|
||||
return setTable({ data: { columns: [...existingColumns, newColumn] }, key: pageKey });
|
||||
}
|
||||
|
|
@ -298,10 +315,10 @@ export const PlaylistListHeaderFilters = ({
|
|||
return setTable({ data: { columns: newColumns }, key: pageKey });
|
||||
};
|
||||
|
||||
const handleAutoFitColumns = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setTable({ data: { autoFit: e.currentTarget.checked }, key: pageKey });
|
||||
const handleAutoFitColumns = (autoFitColumns: boolean) => {
|
||||
setTable({ data: { autoFit: autoFitColumns }, key: pageKey });
|
||||
|
||||
if (e.currentTarget.checked) {
|
||||
if (autoFitColumns) {
|
||||
tableRef.current?.api.sizeColumnsToFit();
|
||||
}
|
||||
};
|
||||
|
|
@ -314,6 +331,8 @@ export const PlaylistListHeaderFilters = ({
|
|||
}
|
||||
};
|
||||
|
||||
const debouncedHandleItemSize = debounce(handleItemSize, 20);
|
||||
|
||||
const handleItemGap = (e: number) => {
|
||||
setGrid({ data: { itemGap: e }, key: pageKey });
|
||||
};
|
||||
|
|
@ -323,28 +342,32 @@ export const PlaylistListHeaderFilters = ({
|
|||
handleFilterChange(filter);
|
||||
};
|
||||
|
||||
const handleCreatePlaylistModal = () => {
|
||||
openModal({
|
||||
children: <CreatePlaylistForm onCancel={() => closeAllModals()} />,
|
||||
onClose: () => {
|
||||
tableRef?.current?.api?.purgeInfiniteCache();
|
||||
},
|
||||
size: server?.type === ServerType?.NAVIDROME ? 'lg' : 'sm',
|
||||
title: t('form.createPlaylist.title', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex justify="space-between">
|
||||
<Group
|
||||
gap="sm"
|
||||
ref={cq.ref}
|
||||
spacing="sm"
|
||||
w="100%"
|
||||
>
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
{sortByLabel}
|
||||
</Button>
|
||||
<Button variant="subtle">{sortByLabel}</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{FILTERS[server?.type as keyof typeof FILTERS].map((f) => (
|
||||
<DropdownMenu.Item
|
||||
$isActive={f.value === filter.sortBy}
|
||||
isSelected={f.value === filter.sortBy}
|
||||
key={`filter-${f.name}`}
|
||||
onClick={handleSetSortBy}
|
||||
value={f.value}
|
||||
|
|
@ -359,31 +382,14 @@ export const PlaylistListHeaderFilters = ({
|
|||
onToggle={handleToggleSortOrder}
|
||||
sortOrder={filter.sortOrder}
|
||||
/>
|
||||
<Divider orientation="vertical" />
|
||||
<Button
|
||||
compact
|
||||
onClick={handleRefresh}
|
||||
size="md"
|
||||
tooltip={{ label: t('common.refresh', { postProcess: 'titleCase' }) }}
|
||||
variant="subtle"
|
||||
>
|
||||
<RiRefreshLine size="1.3rem" />
|
||||
</Button>
|
||||
<Divider orientation="vertical" />
|
||||
<RefreshButton onClick={handleRefresh} />
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
fw="600"
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
<RiMoreFill size="1.3rem" />
|
||||
</Button>
|
||||
<MoreButton />
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiRefreshLine />}
|
||||
leftSection={<Icon icon="refresh" />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
{t('common.refresh', { postProcess: 'titleCase' })}
|
||||
|
|
@ -391,120 +397,29 @@ export const PlaylistListHeaderFilters = ({
|
|||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
</Group>
|
||||
<Group>
|
||||
<DropdownMenu
|
||||
position="bottom-end"
|
||||
width={425}
|
||||
<Group
|
||||
gap="xs"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Button
|
||||
onClick={handleCreatePlaylistModal}
|
||||
variant="subtle"
|
||||
>
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
<RiSettings3Fill size="1.3rem" />
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.general.displayType', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.CARD}
|
||||
onClick={handleSetViewType}
|
||||
value={ListDisplayType.CARD}
|
||||
>
|
||||
{t('table.config.view.card', { postProcess: 'titleCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.POSTER}
|
||||
onClick={handleSetViewType}
|
||||
value={ListDisplayType.POSTER}
|
||||
>
|
||||
{t('table.config.view.poster', { postProcess: 'titleCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.TABLE}
|
||||
onClick={handleSetViewType}
|
||||
value={ListDisplayType.TABLE}
|
||||
>
|
||||
{t('table.config.view.table', { postProcess: 'titleCase' })}
|
||||
</DropdownMenu.Item>
|
||||
{/* <DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.TABLE_PAGINATED}
|
||||
value={ListDisplayType.TABLE_PAGINATED}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
Table (paginated)
|
||||
</DropdownMenu.Item> */}
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.general.itemSize', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
<Slider
|
||||
defaultValue={isGrid ? grid?.itemSize || 0 : table.rowHeight}
|
||||
max={isGrid ? 300 : 100}
|
||||
min={isGrid ? 100 : 25}
|
||||
onChangeEnd={handleItemSize}
|
||||
/>
|
||||
</DropdownMenu.Item>
|
||||
{isGrid && (
|
||||
<>
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.general.itemGap', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
<Slider
|
||||
defaultValue={grid?.itemGap || 0}
|
||||
max={30}
|
||||
min={0}
|
||||
onChangeEnd={handleItemGap}
|
||||
/>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
{!isGrid && (
|
||||
<>
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.generaltableColumns', {
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
closeMenuOnClick={false}
|
||||
component="div"
|
||||
sx={{ cursor: 'default' }}
|
||||
>
|
||||
<Stack>
|
||||
<MultiSelect
|
||||
clearable
|
||||
data={PLAYLIST_TABLE_COLUMNS}
|
||||
defaultValue={table?.columns.map(
|
||||
(column) => column.column,
|
||||
)}
|
||||
onChange={handleTableColumns}
|
||||
width={300}
|
||||
/>
|
||||
<Group position="apart">
|
||||
<Text>
|
||||
{t('table.config.general.autoFitColumns', {
|
||||
postProcess: 'titleCase',
|
||||
})}
|
||||
</Text>
|
||||
<Switch
|
||||
defaultChecked={table.autoFit}
|
||||
onChange={handleAutoFitColumns}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
{t('action.createPlaylist', { postProcess: 'sentenceCase' })}
|
||||
</Button>
|
||||
<ListConfigMenu
|
||||
autoFitColumns={table.autoFit}
|
||||
displayType={display}
|
||||
itemGap={grid?.itemGap || 0}
|
||||
itemSize={isGrid ? grid?.itemSize || 0 : table.rowHeight}
|
||||
onChangeAutoFitColumns={handleAutoFitColumns}
|
||||
onChangeDisplayType={handleSetViewType}
|
||||
onChangeItemGap={handleItemGap}
|
||||
onChangeItemSize={debouncedHandleItemSize}
|
||||
onChangeTableColumns={handleTableColumns}
|
||||
tableColumns={table?.columns.map((column) => column.column)}
|
||||
tableColumnsData={PLAYLIST_TABLE_COLUMNS}
|
||||
/>
|
||||
</Group>
|
||||
</Flex>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import { Flex, Group, Stack } from '@mantine/core';
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { ChangeEvent, MutableRefObject } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiFileAddFill } from 'react-icons/ri';
|
||||
|
||||
import { Button, PageHeader, Paper, SearchInput, SpinnerIcon } from '/@/renderer/components';
|
||||
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { CreatePlaylistForm } from '/@/renderer/features/playlists/components/create-playlist-form';
|
||||
import { PlaylistListHeaderFilters } from '/@/renderer/features/playlists/components/playlist-list-header-filters';
|
||||
import { LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
|
||||
import { PlaylistListFilter, useCurrentServer } from '/@/renderer/store';
|
||||
import { LibraryItem, PlaylistListQuery, ServerType } from '/@/shared/types/domain-types';
|
||||
import { Badge } from '/@/shared/components/badge/badge';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { LibraryItem, PlaylistListQuery } from '/@/shared/types/domain-types';
|
||||
|
||||
interface PlaylistListHeaderProps {
|
||||
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
|
||||
|
|
@ -28,17 +30,6 @@ export const PlaylistListHeader = ({ gridRef, itemCount, tableRef }: PlaylistLis
|
|||
const cq = useContainerQuery();
|
||||
const server = useCurrentServer();
|
||||
|
||||
const handleCreatePlaylistModal = () => {
|
||||
openModal({
|
||||
children: <CreatePlaylistForm onCancel={() => closeAllModals()} />,
|
||||
onClose: () => {
|
||||
tableRef?.current?.api?.purgeInfiniteCache();
|
||||
},
|
||||
size: server?.type === ServerType?.NAVIDROME ? 'xl' : 'sm',
|
||||
title: t('form.createPlaylist.title', { postProcess: 'sentenceCase' }),
|
||||
});
|
||||
};
|
||||
|
||||
const { filter, refresh, search } = useDisplayRefresh<PlaylistListQuery>({
|
||||
gridRef,
|
||||
itemCount,
|
||||
|
|
@ -54,10 +45,10 @@ export const PlaylistListHeader = ({ gridRef, itemCount, tableRef }: PlaylistLis
|
|||
|
||||
return (
|
||||
<Stack
|
||||
gap={0}
|
||||
ref={cq.ref}
|
||||
spacing={0}
|
||||
>
|
||||
<PageHeader backgroundColor="var(--titlebar-bg)">
|
||||
<PageHeader>
|
||||
<Flex
|
||||
align="center"
|
||||
justify="space-between"
|
||||
|
|
@ -67,44 +58,28 @@ export const PlaylistListHeader = ({ gridRef, itemCount, tableRef }: PlaylistLis
|
|||
<LibraryHeaderBar.Title>
|
||||
{t('page.playlistList.title', { postProcess: 'titleCase' })}
|
||||
</LibraryHeaderBar.Title>
|
||||
<Paper
|
||||
fw="600"
|
||||
px="1rem"
|
||||
py="0.3rem"
|
||||
radius="sm"
|
||||
>
|
||||
<Badge>
|
||||
{itemCount === null || itemCount === undefined ? (
|
||||
<SpinnerIcon />
|
||||
) : (
|
||||
itemCount
|
||||
)}
|
||||
</Paper>
|
||||
<Button
|
||||
onClick={handleCreatePlaylistModal}
|
||||
tooltip={{
|
||||
label: t('action.createPlaylist', { postProcess: 'sentenceCase' }),
|
||||
openDelay: 500,
|
||||
}}
|
||||
variant="filled"
|
||||
>
|
||||
<RiFileAddFill />
|
||||
</Button>
|
||||
</Badge>
|
||||
</LibraryHeaderBar>
|
||||
<Group>
|
||||
<SearchInput
|
||||
defaultValue={filter.searchTerm}
|
||||
onChange={handleSearch}
|
||||
openedWidth={cq.isMd ? 250 : cq.isSm ? 200 : 150}
|
||||
/>
|
||||
</Group>
|
||||
</Flex>
|
||||
</PageHeader>
|
||||
<Paper p="1rem">
|
||||
<FilterBar>
|
||||
<PlaylistListHeaderFilters
|
||||
gridRef={gridRef}
|
||||
tableRef={tableRef}
|
||||
/>
|
||||
</Paper>
|
||||
</FilterBar>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Group } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { openModal } from '@mantine/modals';
|
||||
import clone from 'lodash/clone';
|
||||
|
|
@ -7,17 +6,8 @@ import setWith from 'lodash/setWith';
|
|||
import { nanoid } from 'nanoid';
|
||||
import { forwardRef, Ref, useImperativeHandle, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { RiMore2Fill, RiSaveLine } from 'react-icons/ri';
|
||||
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
MotionFlex,
|
||||
NumberInput,
|
||||
QueryBuilder,
|
||||
ScrollArea,
|
||||
Select,
|
||||
} from '/@/renderer/components';
|
||||
import { QueryBuilder } from '/@/renderer/components/query-builder';
|
||||
import { usePlaylistList } from '/@/renderer/features/playlists/queries/playlist-list-query';
|
||||
import {
|
||||
convertNDQueryToQueryGroup,
|
||||
|
|
@ -33,6 +23,15 @@ import {
|
|||
NDSongQueryPlaylistOperators,
|
||||
NDSongQueryStringOperators,
|
||||
} from '/@/shared/api/navidrome.types';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { PlaylistListSort, SongListSort, SortOrder } from '/@/shared/types/domain-types';
|
||||
import { QueryBuilderGroup, QueryBuilderRule } from '/@/shared/types/types';
|
||||
|
||||
|
|
@ -411,15 +410,12 @@ export const PlaylistQueryBuilder = forwardRef(
|
|||
];
|
||||
|
||||
return (
|
||||
<MotionFlex
|
||||
<Flex
|
||||
direction="column"
|
||||
h="calc(100% - 3.5rem)"
|
||||
h="calc(100% - 2rem)"
|
||||
justify="space-between"
|
||||
>
|
||||
<ScrollArea
|
||||
h="100%"
|
||||
p="1rem"
|
||||
>
|
||||
<ScrollArea>
|
||||
<QueryBuilder
|
||||
data={filters}
|
||||
filters={NDSongQueryFields}
|
||||
|
|
@ -448,14 +444,14 @@ export const PlaylistQueryBuilder = forwardRef(
|
|||
</ScrollArea>
|
||||
<Group
|
||||
align="flex-end"
|
||||
justify="space-between"
|
||||
m="1rem"
|
||||
noWrap
|
||||
position="apart"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Group
|
||||
noWrap
|
||||
spacing="sm"
|
||||
gap="sm"
|
||||
w="100%"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Select
|
||||
data={sortOptions}
|
||||
|
|
@ -490,37 +486,38 @@ export const PlaylistQueryBuilder = forwardRef(
|
|||
</Group>
|
||||
{onSave && onSaveAs && (
|
||||
<Group
|
||||
noWrap
|
||||
spacing="sm"
|
||||
gap="sm"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<Button
|
||||
loading={isSaving}
|
||||
onClick={handleSaveAs}
|
||||
variant="filled"
|
||||
>
|
||||
{t('common.saveAs', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={openPreviewModal}
|
||||
p="0.5em"
|
||||
variant="default"
|
||||
variant="subtle"
|
||||
>
|
||||
{t('common.preview', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
<DropdownMenu position="bottom-end">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
<ActionIcon
|
||||
disabled={isSaving}
|
||||
p="0.5em"
|
||||
variant="default"
|
||||
>
|
||||
<RiMore2Fill size={15} />
|
||||
</Button>
|
||||
icon="ellipsisHorizontal"
|
||||
variant="subtle"
|
||||
/>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Item
|
||||
$danger
|
||||
icon={<RiSaveLine color="var(--danger-color)" />}
|
||||
isDanger
|
||||
leftSection={
|
||||
<Icon
|
||||
color="error"
|
||||
icon="save"
|
||||
/>
|
||||
}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t('common.saveAndReplace', { postProcess: 'titleCase' })}
|
||||
|
|
@ -530,7 +527,7 @@ export const PlaylistQueryBuilder = forwardRef(
|
|||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
</MotionFlex>
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import { Group, Stack } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Button, Switch, TextInput, toast } from '/@/renderer/components';
|
||||
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import {
|
||||
CreatePlaylistBody,
|
||||
CreatePlaylistResponse,
|
||||
|
|
@ -98,7 +102,7 @@ export const SaveAsPlaylistForm = ({
|
|||
{...form.getInputProps('public', { type: 'checkbox' })}
|
||||
/>
|
||||
)}
|
||||
<Group position="right">
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
variant="subtle"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { Group, Stack } from '@mantine/core';
|
||||
import { useForm } from '@mantine/form';
|
||||
import { closeAllModals, openModal } from '@mantine/modals';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
|
@ -6,11 +5,17 @@ import { useTranslation } from 'react-i18next';
|
|||
import i18n from '/@/i18n/i18n';
|
||||
import { api } from '/@/renderer/api';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { Button, Select, Switch, TextInput, toast } from '/@/renderer/components';
|
||||
import { useUpdatePlaylist } from '/@/renderer/features/playlists/mutations/update-playlist-mutation';
|
||||
import { queryClient } from '/@/renderer/lib/react-query';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||
import { toast } from '/@/shared/components/toast/toast';
|
||||
import {
|
||||
PlaylistDetailResponse,
|
||||
ServerListItem,
|
||||
|
|
@ -134,7 +139,7 @@ export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlayl
|
|||
/>
|
||||
</>
|
||||
)}
|
||||
<Group position="right">
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
onClick={onCancel}
|
||||
variant="subtle"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue