restructure files onto electron-vite boilerplate

This commit is contained in:
jeffvli 2025-05-18 14:03:18 -07:00
parent 91ce2cd8a1
commit 1cf587bc8f
457 changed files with 9927 additions and 11705 deletions

View file

@ -1,7 +1,9 @@
import { useMemo, useState } from 'react';
import { Box, Group, Stack } from '@mantine/core';
import { useForm } from '@mantine/form';
import { closeModal, ContextModalProps } from '@mantine/modals';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { PlaylistListSort, SongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types';
@ -11,7 +13,6 @@ import { useAddToPlaylist } from '/@/renderer/features/playlists/mutations/add-t
import { usePlaylistList } from '/@/renderer/features/playlists/queries/playlist-list-query';
import { queryClient } from '/@/renderer/lib/react-query';
import { useCurrentServer } from '/@/renderer/store';
import { useTranslation } from 'react-i18next';
export const AddToPlaylistContextModal = ({
id,
@ -217,13 +218,13 @@ export const AddToPlaylistContextModal = ({
<Stack>
<MultiSelect
clearable
searchable
data={playlistSelect}
disabled={playlistList.isLoading}
label={t('form.addToPlaylist.input', {
context: 'playlists',
postProcess: 'titleCase',
})}
searchable
size="md"
{...form.getInputProps('playlistId')}
/>
@ -238,9 +239,9 @@ export const AddToPlaylistContextModal = ({
<Group>
<Button
disabled={addToPlaylistMutation.isLoading}
onClick={() => closeModal(id)}
size="md"
variant="subtle"
onClick={() => closeModal(id)}
>
{t('common.cancel', { postProcess: 'titleCase' })}
</Button>

View file

@ -1,7 +1,11 @@
import { useRef, useState } from 'react';
import { Group, Stack } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { ServerFeature } from '/@/renderer/api/features-types';
import { CreatePlaylistBody, ServerType, SongListSort } from '/@/renderer/api/types';
import { hasFeature } from '/@/renderer/api/utils';
import { Button, Switch, Text, TextInput, toast } from '/@/renderer/components';
import {
PlaylistQueryBuilder,
@ -10,9 +14,6 @@ import {
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
import { convertQueryGroupToNDQuery } from '/@/renderer/features/playlists/utils';
import { useCurrentServer } from '/@/renderer/store';
import { useTranslation } from 'react-i18next';
import { hasFeature } from '/@/renderer/api/utils';
import { ServerFeature } from '/@/renderer/api/features-types';
interface CreatePlaylistFormProps {
onCancel: () => void;
@ -95,11 +96,11 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
<Stack>
<TextInput
data-autofocus
required
label={t('form.createPlaylist.input', {
context: 'name',
postProcess: 'titleCase',
})}
required
{...form.getInputProps('name')}
/>
{server?.type === ServerType.NAVIDROME && (
@ -135,10 +136,10 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
<Stack pt="1rem">
<Text>Query Editor</Text>
<PlaylistQueryBuilder
ref={queryBuilderRef}
isSaving={false}
limit={undefined}
query={undefined}
ref={queryBuilderRef}
sortBy={SongListSort.ALBUM}
sortOrder="asc"
/>
@ -147,8 +148,8 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
<Group position="right">
<Button
variant="subtle"
onClick={onCancel}
variant="subtle"
>
{t('common.cancel', { postProcess: 'titleCase' })}
</Button>

View file

@ -8,11 +8,13 @@ import type {
RowDragEvent,
} from '@ag-grid-community/core';
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 { MutableRefObject, useCallback, useMemo } from 'react';
import { useParams } from 'react-router';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import {
@ -23,8 +25,9 @@ import {
SongListSort,
SortOrder,
} from '/@/renderer/api/types';
import { toast } from '/@/renderer/components';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
import { TablePagination, VirtualTable, getColumnDefs } from '/@/renderer/components/virtual-table';
import { getColumnDefs, TablePagination, VirtualTable } from '/@/renderer/components/virtual-table';
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu';
import {
@ -34,6 +37,7 @@ import {
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { usePlaylistSongList } from '/@/renderer/features/playlists/queries/playlist-song-list-query';
import { useAppFocus } from '/@/renderer/hooks';
import {
useCurrentServer,
useCurrentSong,
@ -45,8 +49,6 @@ import {
} from '/@/renderer/store';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { ListDisplayType, ServerType } from '/@/renderer/types';
import { useAppFocus } from '/@/renderer/hooks';
import { toast } from '/@/renderer/components';
interface PlaylistDetailContentProps {
songs?: Song[];
@ -290,12 +292,7 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai
<>
<VirtualGridAutoSizerContainer>
<VirtualTable
// https://github.com/ag-grid/ag-grid/issues/5284
// Key is used to force remount of table when display, rowHeight, or server changes
key={`table-${page.display}-${page.table.rowHeight}-${server?.id}`}
ref={tableRef}
alwaysShowHorizontalScroll
shouldUpdateSong
autoFitColumns={page.table.autoFit}
columnDefs={columnDefs}
context={{
@ -309,14 +306,9 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai
infiniteInitialRowCount={
iSClientSide ? undefined : checkPlaylistList.data?.totalRecordCount || 100
}
pagination={isPaginationEnabled}
paginationAutoPageSize={isPaginationEnabled}
paginationPageSize={pagination.itemsPerPage || 100}
rowClassRules={rowClassRules}
rowData={songs}
rowDragEntireRow={canDrag}
rowHeight={page.table.rowHeight || 40}
rowModelType={iSClientSide ? 'clientSide' : 'infinite'}
// https://github.com/ag-grid/ag-grid/issues/5284
// Key is used to force remount of table when display, rowHeight, or server changes
key={`table-${page.display}-${page.table.rowHeight}-${server?.id}`}
onBodyScrollEnd={handleScroll}
onCellContextMenu={handleContextMenu}
onColumnMoved={handleColumnChange}
@ -326,13 +318,23 @@ export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetai
onPaginationChanged={onPaginationChanged}
onRowDoubleClicked={handleRowDoubleClick}
onRowDragEnd={handleDragEnd}
pagination={isPaginationEnabled}
paginationAutoPageSize={isPaginationEnabled}
paginationPageSize={pagination.itemsPerPage || 100}
ref={tableRef}
rowClassRules={rowClassRules}
rowData={songs}
rowDragEntireRow={canDrag}
rowHeight={page.table.rowHeight || 40}
rowModelType={iSClientSide ? 'clientSide' : 'infinite'}
shouldUpdateSong
/>
</VirtualGridAutoSizerContainer>
{isPaginationEnabled && (
<AnimatePresence
presenceAffectsLayout
initial={false}
mode="wait"
presenceAffectsLayout
>
{page.display === ListDisplayType.TABLE_PAGINATED && (
<TablePagination

View file

@ -1,20 +1,24 @@
import { useCallback, ChangeEvent, MutableRefObject, MouseEvent } from 'react';
import { IDatasource } from '@ag-grid-community/core';
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 { useTranslation } from 'react-i18next';
import {
RiMoreFill,
RiSettings3Fill,
RiPlayFill,
RiAddCircleFill,
RiAddBoxFill,
RiEditFill,
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 {
@ -25,20 +29,26 @@ import {
SortOrder,
} from '/@/renderer/api/types';
import {
DropdownMenu,
Button,
Slider,
ConfirmModal,
DropdownMenu,
MultiSelect,
Slider,
Switch,
Text,
ConfirmModal,
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 { useContainerQuery } from '/@/renderer/hooks';
import { AppRoute } from '/@/renderer/router/routes';
import {
useCurrentServer,
SongListFilter,
useCurrentServer,
usePlaylistDetailStore,
useSetPlaylistDetailFilters,
useSetPlaylistDetailTable,
@ -46,14 +56,6 @@ import {
useSetPlaylistTablePagination,
} from '/@/renderer/store';
import { ListDisplayType, Play, TableColumn } from '/@/renderer/types';
import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query';
import { useParams, useNavigate } from 'react-router';
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
import { openUpdatePlaylistModal } from '/@/renderer/features/playlists/components/update-playlist-form';
import { useDeletePlaylist } from '/@/renderer/features/playlists/mutations/delete-playlist-mutation';
import { AppRoute } from '/@/renderer/router/routes';
import { OrderToggleButton } from '/@/renderer/features/shared';
import i18n from '/@/i18n/i18n';
const FILTERS = {
jellyfin: [
@ -265,8 +267,8 @@ interface PlaylistDetailSongListHeaderFiltersProps {
}
export const PlaylistDetailSongListHeaderFilters = ({
tableRef,
handleToggleShowQueryBuilder,
tableRef,
}: PlaylistDetailSongListHeaderFiltersProps) => {
const { t } = useTranslation();
const { playlistId } = useParams() as { playlistId: string };
@ -493,10 +495,10 @@ export const PlaylistDetailSongListHeaderFilters = ({
<DropdownMenu.Dropdown>
{FILTERS[server?.type as keyof typeof FILTERS].map((filter) => (
<DropdownMenu.Item
key={`filter-${filter.name}`}
$isActive={filter.value === filters.sortBy}
value={filter.value}
key={`filter-${filter.name}`}
onClick={handleSetSortBy}
value={filter.value}
>
{filter.name}
</DropdownMenu.Item>
@ -506,8 +508,8 @@ export const PlaylistDetailSongListHeaderFilters = ({
<Divider orientation="vertical" />
<OrderToggleButton
sortOrder={filters.sortOrder || SortOrder.ASC}
onToggle={handleToggleSortOrder}
sortOrder={filters.sortOrder || SortOrder.ASC}
/>
<Divider orientation="vertical" />
<DropdownMenu position="bottom-start">
@ -601,8 +603,8 @@ export const PlaylistDetailSongListHeaderFilters = ({
</DropdownMenu.Label>
<DropdownMenu.Item
$isActive={page.display === ListDisplayType.TABLE}
value={ListDisplayType.TABLE}
onClick={handleSetViewType}
value={ListDisplayType.TABLE}
>
Table
</DropdownMenu.Item>
@ -642,8 +644,8 @@ export const PlaylistDetailSongListHeaderFilters = ({
defaultValue={page.table?.columns.map(
(column) => column.column,
)}
width={300}
onChange={handleTableColumns}
width={300}
/>
<Group position="apart">
<Text>Auto Fit Columns</Text>

View file

@ -1,8 +1,10 @@
import { MutableRefObject } from 'react';
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 { LibraryItem } from '/@/renderer/api/types';
import { Badge, PageHeader, Paper, SpinnerIcon } from '/@/renderer/components';
import { usePlayQueueAdd } from '/@/renderer/features/player';
@ -20,9 +22,9 @@ interface PlaylistDetailHeaderProps {
}
export const PlaylistDetailSongListHeader = ({
tableRef,
itemCount,
handleToggleShowQueryBuilder,
itemCount,
tableRef,
}: PlaylistDetailHeaderProps) => {
const { t } = useTranslation();
const { playlistId } = useParams() as { playlistId: string };

View file

@ -1,10 +1,13 @@
import { lazy, MutableRefObject, Suspense } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { lazy, MutableRefObject, Suspense } from 'react';
import { useListContext } from '../../../context/list-context';
import { useListStoreByKey } from '../../../store/list.store';
import { Spinner } from '/@/renderer/components';
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
import { ListDisplayType } from '/@/renderer/types';
import { useListContext } from '../../../context/list-context';
import { useListStoreByKey } from '../../../store/list.store';
const PlaylistListTableView = lazy(() =>
import('/@/renderer/features/playlists/components/playlist-list-table-view').then((module) => ({
@ -19,12 +22,12 @@ const PlaylistListGridView = lazy(() =>
);
interface PlaylistListContentProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const PlaylistListContent = ({ gridRef, tableRef, itemCount }: PlaylistListContentProps) => {
export const PlaylistListContent = ({ gridRef, itemCount, tableRef }: PlaylistListContentProps) => {
const { pageKey } = useListContext();
const { display } = useListStoreByKey({ key: pageKey });

View file

@ -2,8 +2,10 @@ import { QueryKey, useQueryClient } from '@tanstack/react-query';
import { MutableRefObject, useCallback, useMemo } from 'react';
import AutoSizer, { Size } from 'react-virtualized-auto-sizer';
import { ListOnScrollProps } from 'react-window';
import { useListContext } from '../../../context/list-context';
import { useListStoreActions } from '../../../store/list.store';
import { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys';
import {
@ -20,13 +22,13 @@ import {
VirtualInfiniteGridRef,
} from '/@/renderer/components/virtual-grid';
import { usePlayQueueAdd } from '/@/renderer/features/player';
import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useListStoreByKey } from '/@/renderer/store';
import { CardRow, ListDisplayType } from '/@/renderer/types';
import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite';
interface PlaylistListGridViewProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
itemCount?: number;
}
@ -35,7 +37,7 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
const queryClient = useQueryClient();
const server = useCurrentServer();
const handlePlayQueueAdd = usePlayQueueAdd();
const { display, grid, filter } = useListStoreByKey<PlaylistListQuery>({ key: pageKey });
const { display, filter, grid } = useListStoreByKey<PlaylistListQuery>({ key: pageKey });
const { setGrid } = useListStoreActions();
const handleFavorite = useHandleFavorite({ gridRef, server });
@ -49,15 +51,15 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
case PlaylistListSort.NAME:
rows.push(PLAYLIST_CARD_ROWS.songCount);
break;
case PlaylistListSort.SONG_COUNT:
rows.push(PLAYLIST_CARD_ROWS.songCount);
break;
case PlaylistListSort.OWNER:
rows.push(PLAYLIST_CARD_ROWS.owner);
break;
case PlaylistListSort.PUBLIC:
rows.push(PLAYLIST_CARD_ROWS.public);
break;
case PlaylistListSort.SONG_COUNT:
rows.push(PLAYLIST_CARD_ROWS.songCount);
break;
case PlaylistListSort.UPDATED_AT:
break;
}
@ -73,7 +75,7 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
);
const fetchInitialData = useCallback(() => {
const query: Omit<PlaylistListQuery, 'startIndex' | 'limit'> = {
const query: Omit<PlaylistListQuery, 'limit' | 'startIndex'> = {
...filter,
};
@ -140,8 +142,6 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
<AutoSizer>
{({ height, width }: Size) => (
<VirtualInfiniteGrid
key={`playlist-list-${server?.id}`}
ref={gridRef}
cardRows={cardRows}
display={display || ListDisplayType.CARD}
fetchFn={fetch}
@ -154,14 +154,16 @@ export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridVie
itemGap={grid?.itemGap ?? 10}
itemSize={grid?.itemSize || 200}
itemType={LibraryItem.PLAYLIST}
key={`playlist-list-${server?.id}`}
loading={itemCount === undefined || itemCount === null}
minimumBatchSize={40}
onScroll={handleGridScroll}
ref={gridRef}
route={{
route: AppRoute.PLAYLISTS_DETAIL_SONGS,
slugs: [{ idProperty: 'id', slugProperty: 'playlistId' }],
}}
width={width}
onScroll={handleGridScroll}
/>
)}
</AutoSizer>

View file

@ -1,12 +1,16 @@
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react';
import { IDatasource } from '@ag-grid-community/core';
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 { useQueryClient } from '@tanstack/react-query';
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { RiMoreFill, RiRefreshLine, RiSettings3Fill } from 'react-icons/ri';
import { useListContext } from '../../../context/list-context';
import { useListStoreByKey } from '../../../store/list.store';
import i18n from '/@/i18n/i18n';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { LibraryItem, PlaylistListQuery, PlaylistListSort, SortOrder } from '/@/renderer/api/types';
@ -17,7 +21,6 @@ import { OrderToggleButton } from '/@/renderer/features/shared';
import { useContainerQuery } from '/@/renderer/hooks';
import { PlaylistListFilter, useCurrentServer, useListStoreActions } from '/@/renderer/store';
import { ListDisplayType, TableColumn } from '/@/renderer/types';
import i18n from '/@/i18n/i18n';
const FILTERS = {
jellyfin: [
@ -104,7 +107,7 @@ const FILTERS = {
};
interface PlaylistListHeaderFiltersProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
tableRef: MutableRefObject<AgGridReactType | null>;
}
@ -116,9 +119,9 @@ export const PlaylistListHeaderFilters = ({
const { pageKey } = useListContext();
const queryClient = useQueryClient();
const server = useCurrentServer();
const { setFilter, setTable, setTablePagination, setGrid, setDisplayType } =
const { setDisplayType, setFilter, setGrid, setTable, setTablePagination } =
useListStoreActions();
const { display, filter, table, grid } = useListStoreByKey<PlaylistListQuery>({ key: pageKey });
const { display, filter, grid, table } = useListStoreByKey<PlaylistListQuery>({ key: pageKey });
const cq = useContainerQuery();
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER;
@ -337,10 +340,10 @@ export const PlaylistListHeaderFilters = ({
<DropdownMenu.Dropdown>
{FILTERS[server?.type as keyof typeof FILTERS].map((f) => (
<DropdownMenu.Item
key={`filter-${f.name}`}
$isActive={f.value === filter.sortBy}
value={f.value}
key={`filter-${f.name}`}
onClick={handleSetSortBy}
value={f.value}
>
{f.name}
</DropdownMenu.Item>
@ -349,16 +352,16 @@ export const PlaylistListHeaderFilters = ({
</DropdownMenu>
<Divider orientation="vertical" />
<OrderToggleButton
sortOrder={filter.sortOrder}
onToggle={handleToggleSortOrder}
sortOrder={filter.sortOrder}
/>
<Divider orientation="vertical" />
<Button
compact
onClick={handleRefresh}
size="md"
tooltip={{ label: t('common.refresh', { postProcess: 'titleCase' }) }}
variant="subtle"
onClick={handleRefresh}
>
<RiRefreshLine size="1.3rem" />
</Button>
@ -404,22 +407,22 @@ export const PlaylistListHeaderFilters = ({
</DropdownMenu.Label>
<DropdownMenu.Item
$isActive={display === ListDisplayType.CARD}
value={ListDisplayType.CARD}
onClick={handleSetViewType}
value={ListDisplayType.CARD}
>
{t('table.config.view.card', { postProcess: 'titleCase' })}
</DropdownMenu.Item>
<DropdownMenu.Item
$isActive={display === ListDisplayType.POSTER}
value={ListDisplayType.POSTER}
onClick={handleSetViewType}
value={ListDisplayType.POSTER}
>
{t('table.config.view.poster', { postProcess: 'titleCase' })}
</DropdownMenu.Item>
<DropdownMenu.Item
$isActive={display === ListDisplayType.TABLE}
value={ListDisplayType.TABLE}
onClick={handleSetViewType}
value={ListDisplayType.TABLE}
>
{t('table.config.view.table', { postProcess: 'titleCase' })}
</DropdownMenu.Item>
@ -478,8 +481,8 @@ export const PlaylistListHeaderFilters = ({
defaultValue={table?.columns.map(
(column) => column.column,
)}
width={300}
onChange={handleTableColumns}
width={300}
/>
<Group position="apart">
<Text>

View file

@ -1,27 +1,29 @@
import { ChangeEvent, MutableRefObject } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Flex, Group, Stack } from '@mantine/core';
import { openModal, closeAllModals } from '@mantine/modals';
import { PageHeader, SpinnerIcon, Paper, Button, SearchInput } from '/@/renderer/components';
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 { LibraryItem, PlaylistListQuery, ServerType } from '/@/renderer/api/types';
import { Button, PageHeader, Paper, SearchInput, SpinnerIcon } from '/@/renderer/components';
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 { useContainerQuery } from '/@/renderer/hooks';
import { PlaylistListFilter, useCurrentServer } from '/@/renderer/store';
import debounce from 'lodash/debounce';
import { useTranslation } from 'react-i18next';
import { RiFileAddFill } from 'react-icons/ri';
import { LibraryItem, PlaylistListQuery, ServerType } from '/@/renderer/api/types';
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
import { PlaylistListFilter, useCurrentServer } from '/@/renderer/store';
interface PlaylistListHeaderProps {
gridRef: MutableRefObject<VirtualInfiniteGridRef | null>;
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistListHeaderProps) => {
export const PlaylistListHeader = ({ gridRef, itemCount, tableRef }: PlaylistListHeaderProps) => {
const { t } = useTranslation();
const cq = useContainerQuery();
const server = useCurrentServer();
@ -78,12 +80,12 @@ export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistLis
)}
</Paper>
<Button
onClick={handleCreatePlaylistModal}
tooltip={{
label: t('action.createPlaylist', { postProcess: 'sentenceCase' }),
openDelay: 500,
}}
variant="filled"
onClick={handleCreatePlaylistModal}
>
<RiFileAddFill />
</Button>
@ -91,8 +93,8 @@ export const PlaylistListHeader = ({ itemCount, tableRef, gridRef }: PlaylistLis
<Group>
<SearchInput
defaultValue={filter.searchTerm}
openedWidth={cq.isMd ? 250 : cq.isSm ? 200 : 150}
onChange={handleSearch}
openedWidth={cq.isMd ? 250 : cq.isSm ? 200 : 150}
/>
</Group>
</Flex>

View file

@ -1,7 +1,9 @@
import { MutableRefObject } from 'react';
import { RowDoubleClickedEvent } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { RowDoubleClickedEvent } from '@ag-grid-community/core';
import { MutableRefObject } from 'react';
import { generatePath, useNavigate } from 'react-router';
import { LibraryItem } from '/@/renderer/api/types';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid';
import { VirtualTable } from '/@/renderer/components/virtual-table';
@ -15,7 +17,7 @@ interface PlaylistListTableViewProps {
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const PlaylistListTableView = ({ tableRef, itemCount }: PlaylistListTableViewProps) => {
export const PlaylistListTableView = ({ itemCount, tableRef }: PlaylistListTableViewProps) => {
const navigate = useNavigate();
const server = useCurrentServer();
const pageKey = 'playlist';

View file

@ -1,4 +1,3 @@
import { forwardRef, Ref, useImperativeHandle, useMemo, useState } from 'react';
import { Group } from '@mantine/core';
import { useForm } from '@mantine/form';
import { openModal } from '@mantine/modals';
@ -6,6 +5,19 @@ import clone from 'lodash/clone';
import get from 'lodash/get';
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 {
NDSongQueryBooleanOperators,
NDSongQueryDateOperators,
NDSongQueryFields,
NDSongQueryNumberOperators,
NDSongQueryPlaylistOperators,
NDSongQueryStringOperators,
} from '/@/renderer/api/navidrome.types';
import { PlaylistListSort, SongListSort, SortOrder } from '/@/renderer/api/types';
import {
Button,
DropdownMenu,
@ -15,25 +27,14 @@ import {
ScrollArea,
Select,
} from '/@/renderer/components';
import { usePlaylistList } from '/@/renderer/features/playlists/queries/playlist-list-query';
import {
convertNDQueryToQueryGroup,
convertQueryGroupToNDQuery,
} from '/@/renderer/features/playlists/utils';
import { QueryBuilderGroup, QueryBuilderRule } from '/@/renderer/types';
import { useTranslation } from 'react-i18next';
import { RiMore2Fill, RiSaveLine } from 'react-icons/ri';
import { PlaylistListSort, SongListSort, SortOrder } from '/@/renderer/api/types';
import {
NDSongQueryBooleanOperators,
NDSongQueryDateOperators,
NDSongQueryFields,
NDSongQueryNumberOperators,
NDSongQueryPlaylistOperators,
NDSongQueryStringOperators,
} from '/@/renderer/api/navidrome.types';
import { usePlaylistList } from '/@/renderer/features/playlists/queries/playlist-list-query';
import { useCurrentServer } from '/@/renderer/store';
import { JsonPreview } from '/@/renderer/features/shared/components/json-preview';
import { useCurrentServer } from '/@/renderer/store';
import { QueryBuilderGroup, QueryBuilderRule } from '/@/renderer/types';
type AddArgs = {
groupIndex: number[];
@ -91,14 +92,14 @@ export type PlaylistQueryBuilderRef = {
export const PlaylistQueryBuilder = forwardRef(
(
{
sortOrder,
sortBy,
limit,
isSaving,
query,
limit,
onSave,
onSaveAs,
playlistId,
query,
sortBy,
sortOrder,
}: PlaylistQueryBuilderProps,
ref: Ref<PlaylistQueryBuilderRef>,
) => {
@ -177,7 +178,7 @@ export const PlaylistQueryBuilder = forwardRef(
};
const handleAddRuleGroup = (args: AddArgs) => {
const { level, groupIndex } = args;
const { groupIndex, level } = args;
const filtersCopy = clone(filters);
const getPath = (level: number) => {
@ -218,7 +219,7 @@ export const PlaylistQueryBuilder = forwardRef(
};
const handleDeleteRuleGroup = (args: DeleteArgs) => {
const { uniqueId, level, groupIndex } = args;
const { groupIndex, level, uniqueId } = args;
const filtersCopy = clone(filters);
const getPath = (level: number) => {
@ -264,7 +265,7 @@ export const PlaylistQueryBuilder = forwardRef(
};
const handleAddRule = (args: AddArgs) => {
const { level, groupIndex } = args;
const { groupIndex, level } = args;
const filtersCopy = clone(filters);
const path = getRulePath(level, groupIndex);
@ -287,7 +288,7 @@ export const PlaylistQueryBuilder = forwardRef(
};
const handleDeleteRule = (args: DeleteArgs) => {
const { uniqueId, level, groupIndex } = args;
const { groupIndex, level, uniqueId } = args;
const filtersCopy = clone(filters);
const path = getRulePath(level, groupIndex);
@ -304,7 +305,7 @@ export const PlaylistQueryBuilder = forwardRef(
};
const handleChangeField = (args: any) => {
const { uniqueId, level, groupIndex, value } = args;
const { groupIndex, level, uniqueId, value } = args;
const filtersCopy = clone(filters);
const path = getRulePath(level, groupIndex);
@ -327,7 +328,7 @@ export const PlaylistQueryBuilder = forwardRef(
};
const handleChangeType = (args: any) => {
const { level, groupIndex, value } = args;
const { groupIndex, level, value } = args;
const filtersCopy = clone(filters);
@ -359,7 +360,7 @@ export const PlaylistQueryBuilder = forwardRef(
};
const handleChangeOperator = (args: any) => {
const { uniqueId, level, groupIndex, value } = args;
const { groupIndex, level, uniqueId, value } = args;
const filtersCopy = clone(filters);
const path = getRulePath(level, groupIndex);
@ -380,7 +381,7 @@ export const PlaylistQueryBuilder = forwardRef(
};
const handleChangeValue = (args: any) => {
const { uniqueId, level, groupIndex, value } = args;
const { groupIndex, level, uniqueId, value } = args;
const filtersCopy = clone(filters);
const path = getRulePath(level, groupIndex);
@ -424,15 +425,6 @@ export const PlaylistQueryBuilder = forwardRef(
filters={NDSongQueryFields}
groupIndex={[]}
level={0}
operators={{
boolean: NDSongQueryBooleanOperators,
date: NDSongQueryDateOperators,
number: NDSongQueryNumberOperators,
playlist: NDSongQueryPlaylistOperators,
string: NDSongQueryStringOperators,
}}
playlists={playlistData}
uniqueId={filters.uniqueId}
onAddRule={handleAddRule}
onAddRuleGroup={handleAddRuleGroup}
onChangeField={handleChangeField}
@ -443,12 +435,21 @@ export const PlaylistQueryBuilder = forwardRef(
onDeleteRule={handleDeleteRule}
onDeleteRuleGroup={handleDeleteRuleGroup}
onResetFilters={handleResetFilters}
operators={{
boolean: NDSongQueryBooleanOperators,
date: NDSongQueryDateOperators,
number: NDSongQueryNumberOperators,
playlist: NDSongQueryPlaylistOperators,
string: NDSongQueryStringOperators,
}}
playlists={playlistData}
uniqueId={filters.uniqueId}
/>
</ScrollArea>
<Group
noWrap
align="flex-end"
m="1rem"
noWrap
position="apart"
>
<Group
@ -457,10 +458,10 @@ export const PlaylistQueryBuilder = forwardRef(
w="100%"
>
<Select
searchable
data={sortOptions}
label="Sort"
maxWidth="20%"
searchable
width={150}
{...extraFiltersForm.getInputProps('sortBy')}
/>
@ -494,15 +495,15 @@ export const PlaylistQueryBuilder = forwardRef(
>
<Button
loading={isSaving}
variant="filled"
onClick={handleSaveAs}
variant="filled"
>
{t('common.saveAs', { postProcess: 'titleCase' })}
</Button>
<Button
onClick={openPreviewModal}
p="0.5em"
variant="default"
onClick={openPreviewModal}
>
{t('common.preview', { postProcess: 'titleCase' })}
</Button>

View file

@ -1,12 +1,13 @@
import { Group, Stack } from '@mantine/core';
import { useForm } from '@mantine/form';
import { useTranslation } from 'react-i18next';
import { ServerFeature } from '/@/renderer/api/features-types';
import { CreatePlaylistBody, CreatePlaylistResponse, ServerType } from '/@/renderer/api/types';
import { hasFeature } from '/@/renderer/api/utils';
import { Button, Switch, TextInput, toast } from '/@/renderer/components';
import { useCreatePlaylist } from '/@/renderer/features/playlists/mutations/create-playlist-mutation';
import { useCurrentServer } from '/@/renderer/store';
import { useTranslation } from 'react-i18next';
import { ServerFeature } from '/@/renderer/api/features-types';
import { hasFeature } from '/@/renderer/api/utils';
interface SaveAsPlaylistFormProps {
body: Partial<CreatePlaylistBody>;
@ -17,9 +18,9 @@ interface SaveAsPlaylistFormProps {
export const SaveAsPlaylistForm = ({
body,
serverId,
onSuccess,
onCancel,
onSuccess,
serverId,
}: SaveAsPlaylistFormProps) => {
const { t } = useTranslation();
const mutation = useCreatePlaylist({});
@ -68,11 +69,11 @@ export const SaveAsPlaylistForm = ({
<Stack>
<TextInput
data-autofocus
required
label={t('form.createPlaylist.input', {
context: 'name',
postProcess: 'titleCase',
})}
required
{...form.getInputProps('name')}
/>
{server?.type === ServerType.NAVIDROME && (
@ -95,8 +96,8 @@ export const SaveAsPlaylistForm = ({
)}
<Group position="right">
<Button
variant="subtle"
onClick={onCancel}
variant="subtle"
>
{t('common.cancel', { postProcess: 'titleCase' })}
</Button>

View file

@ -1,7 +1,11 @@
import { Group, Stack } from '@mantine/core';
import { useForm } from '@mantine/form';
import { openModal, closeAllModals } from '@mantine/modals';
import { closeAllModals, openModal } from '@mantine/modals';
import { useTranslation } from 'react-i18next';
import i18n from '/@/i18n/i18n';
import { api } from '/@/renderer/api';
import { ServerFeature } from '/@/renderer/api/features-types';
import { queryKeys } from '/@/renderer/api/query-keys';
import {
PlaylistDetailResponse,
@ -14,14 +18,11 @@ import {
UserListQuery,
UserListSort,
} from '/@/renderer/api/types';
import { hasFeature } from '/@/renderer/api/utils';
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 { useTranslation } from 'react-i18next';
import i18n from '/@/i18n/i18n';
import { hasFeature } from '/@/renderer/api/utils';
import { ServerFeature } from '/@/renderer/api/features-types';
interface UpdatePlaylistFormProps {
body: Partial<UpdatePlaylistBody>;
@ -30,7 +31,7 @@ interface UpdatePlaylistFormProps {
users?: User[];
}
export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlaylistFormProps) => {
export const UpdatePlaylistForm = ({ body, onCancel, query, users }: UpdatePlaylistFormProps) => {
const { t } = useTranslation();
const mutation = useUpdatePlaylist({});
const server = useCurrentServer();
@ -89,11 +90,11 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
<Stack>
<TextInput
data-autofocus
required
label={t('form.createPlaylist.input', {
context: 'name',
postProcess: 'titleCase',
})}
required
{...form.getInputProps('name')}
/>
{server?.type === ServerType.NAVIDROME && (
@ -135,8 +136,8 @@ export const UpdatePlaylistForm = ({ users, query, body, onCancel }: UpdatePlayl
)}
<Group position="right">
<Button
variant="subtle"
onClick={onCancel}
variant="subtle"
>
{t('common.cancel', { postProcess: 'titleCase' })}
</Button>
@ -200,9 +201,9 @@ export const openUpdatePlaylistModal = async (args: {
name: playlist?.name,
public: playlist?.public || false,
}}
onCancel={closeAllModals}
query={{ id: playlist?.id }}
users={users?.items}
onCancel={closeAllModals}
/>
),
title: i18n.t('form.editPlaylist.title', { postProcess: 'titleCase' }) as string,