mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-09 20:29:36 +02:00
add new genre list
This commit is contained in:
@@ -159,6 +159,19 @@ export const queryKeys: Record<
|
|||||||
root: (serverId: string) => [serverId, 'artists'] as const,
|
root: (serverId: string) => [serverId, 'artists'] as const,
|
||||||
},
|
},
|
||||||
genres: {
|
genres: {
|
||||||
|
count: (serverId: string, query?: GenreListQuery) => {
|
||||||
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
|
|
||||||
|
if (query && pagination) {
|
||||||
|
return [serverId, 'genres', 'count', filter, pagination] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query) {
|
||||||
|
return [serverId, 'genres', 'count', filter] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [serverId, 'genres', 'count'] as const;
|
||||||
|
},
|
||||||
list: (serverId: string, query?: GenreListQuery) => {
|
list: (serverId: string, query?: GenreListQuery) => {
|
||||||
const { filter, pagination } = splitPaginatedQuery(query);
|
const { filter, pagination } = splitPaginatedQuery(query);
|
||||||
if (query && pagination) {
|
if (query && pagination) {
|
||||||
|
|||||||
@@ -435,27 +435,27 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
width: 80,
|
width: 80,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
align: 'start',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.image', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: 'left',
|
||||||
value: TableColumn.TITLE,
|
value: TableColumn.IMAGE,
|
||||||
width: 300,
|
width: 70,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
align: 'start',
|
align: 'start',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: false,
|
isEnabled: true,
|
||||||
label: i18n.t('table.config.label.titleCombined', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: 'left',
|
||||||
value: TableColumn.TITLE_COMBINED,
|
value: TableColumn.TITLE,
|
||||||
width: 300,
|
width: 300,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
align: 'center',
|
align: 'center',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.duration', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.duration', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.DURATION,
|
value: TableColumn.DURATION,
|
||||||
@@ -473,7 +473,7 @@ export const ALBUM_ARTIST_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
{
|
{
|
||||||
align: 'start',
|
align: 'start',
|
||||||
autoSize: false,
|
autoSize: false,
|
||||||
isEnabled: true,
|
isEnabled: false,
|
||||||
label: i18n.t('table.config.label.genre', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.genre', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
value: TableColumn.GENRE,
|
value: TableColumn.GENRE,
|
||||||
@@ -631,7 +631,7 @@ export const GENRE_TABLE_COLUMNS: DefaultTableColumn[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
align: 'start',
|
align: 'start',
|
||||||
autoSize: false,
|
autoSize: true,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
label: i18n.t('table.config.label.title', { postProcess: 'titleCase' }),
|
||||||
pinned: null,
|
pinned: null,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { queryOptions } from '@tanstack/react-query';
|
|||||||
import { api } from '/@/renderer/api';
|
import { api } from '/@/renderer/api';
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||||
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
import { QueryHookArgs } from '/@/renderer/lib/react-query';
|
||||||
import { GenreListQuery } from '/@/shared/types/domain-types';
|
import { GenreListQuery, ListCountQuery } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
export const genresQueries = {
|
export const genresQueries = {
|
||||||
list: (args: QueryHookArgs<GenreListQuery>) => {
|
list: (args: QueryHookArgs<GenreListQuery>) => {
|
||||||
@@ -19,4 +19,21 @@ export const genresQueries = {
|
|||||||
...args.options,
|
...args.options,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
listCount: (args: QueryHookArgs<ListCountQuery<GenreListQuery>>) => {
|
||||||
|
return queryOptions({
|
||||||
|
queryFn: ({ signal }) => {
|
||||||
|
return api.controller
|
||||||
|
.getGenreList({
|
||||||
|
apiClientProps: { serverId: args.serverId, signal },
|
||||||
|
query: { ...args.query, limit: 1, startIndex: 0 },
|
||||||
|
})
|
||||||
|
.then((result) => result?.totalRecordCount ?? 0);
|
||||||
|
},
|
||||||
|
queryKey: queryKeys.genres.count(
|
||||||
|
args.serverId,
|
||||||
|
Object.keys(args.query).length === 0 ? undefined : args.query,
|
||||||
|
),
|
||||||
|
...args.options,
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,42 +1,129 @@
|
|||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import { lazy, Suspense } from 'react';
|
||||||
|
|
||||||
import { lazy, MutableRefObject, Suspense } from 'react';
|
import { useGenreListFilters } from '/@/renderer/features/genres/hooks/use-genre-list-filters';
|
||||||
|
import { ItemListSettings, useCurrentServer, useListSettings } from '/@/renderer/store';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
|
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
|
||||||
import { useListStoreByKey } from '/@/renderer/store';
|
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
import { ListDisplayType } from '/@/shared/types/types';
|
import { ItemListKey, ListDisplayType, ListPaginationType } from '/@/shared/types/types';
|
||||||
|
|
||||||
const GenreListGridView = lazy(() =>
|
const GenreListInfiniteGrid = lazy(() =>
|
||||||
import('/@/renderer/features/genres/components/genre-list-grid-view').then((module) => ({
|
import('/@/renderer/features/genres/components/genre-list-infinite-grid').then((module) => ({
|
||||||
default: module.GenreListGridView,
|
default: module.GenreListInfiniteGrid,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
const GenreListTableView = lazy(() =>
|
const GenreListPaginatedGrid = lazy(() =>
|
||||||
import('/@/renderer/features/genres/components/genre-list-table-view').then((module) => ({
|
import('/@/renderer/features/genres/components/genre-list-paginated-grid').then((module) => ({
|
||||||
default: module.GenreListTableView,
|
default: module.GenreListPaginatedGrid,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
|
||||||
interface AlbumListContentProps {
|
const GenreListInfiniteTable = lazy(() =>
|
||||||
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
|
import('/@/renderer/features/genres/components/genre-list-infinite-table').then((module) => ({
|
||||||
itemCount?: number;
|
default: module.GenreListInfiniteTable,
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
})),
|
||||||
}
|
);
|
||||||
|
|
||||||
export const GenreListContent = ({ gridRef, itemCount, tableRef }: AlbumListContentProps) => {
|
const GenreListPaginatedTable = lazy(() =>
|
||||||
const { pageKey } = useListContext();
|
import('/@/renderer/features/genres/components/genre-list-paginated-table').then((module) => ({
|
||||||
const { display } = useListStoreByKey({ key: pageKey });
|
default: module.GenreListPaginatedTable,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const GenreListContent = () => {
|
||||||
|
const { display, grid, itemsPerPage, pagination, table } = useListSettings(ItemListKey.GENRE);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Spinner container />}>
|
<Suspense fallback={<Spinner container />}>
|
||||||
{display === ListDisplayType.CARD || display === ListDisplayType.GRID ? (
|
<GenreListView
|
||||||
<GenreListGridView gridRef={gridRef} itemCount={itemCount} />
|
display={display}
|
||||||
) : (
|
grid={grid}
|
||||||
<GenreListTableView itemCount={itemCount} tableRef={tableRef} />
|
itemsPerPage={itemsPerPage}
|
||||||
)}
|
pagination={pagination}
|
||||||
|
table={table}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const GenreListView = ({
|
||||||
|
display,
|
||||||
|
grid,
|
||||||
|
itemsPerPage,
|
||||||
|
pagination,
|
||||||
|
table,
|
||||||
|
}: ItemListSettings) => {
|
||||||
|
const server = useCurrentServer();
|
||||||
|
|
||||||
|
const { query } = useGenreListFilters();
|
||||||
|
|
||||||
|
switch (display) {
|
||||||
|
case ListDisplayType.GRID: {
|
||||||
|
switch (pagination) {
|
||||||
|
case ListPaginationType.INFINITE: {
|
||||||
|
return (
|
||||||
|
<GenreListInfiniteGrid
|
||||||
|
gap={grid.itemGap}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||||
|
query={query}
|
||||||
|
serverId={server.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case ListPaginationType.PAGINATED: {
|
||||||
|
return (
|
||||||
|
<GenreListPaginatedGrid
|
||||||
|
gap={grid.itemGap}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
itemsPerRow={grid.itemsPerRowEnabled ? grid.itemsPerRow : undefined}
|
||||||
|
query={query}
|
||||||
|
serverId={server.id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case ListDisplayType.TABLE: {
|
||||||
|
switch (pagination) {
|
||||||
|
case ListPaginationType.INFINITE: {
|
||||||
|
return (
|
||||||
|
<GenreListInfiniteTable
|
||||||
|
autoFitColumns={table.autoFitColumns}
|
||||||
|
columns={table.columns}
|
||||||
|
enableAlternateRowColors={table.enableAlternateRowColors}
|
||||||
|
enableHorizontalBorders={table.enableHorizontalBorders}
|
||||||
|
enableRowHoverHighlight={table.enableRowHoverHighlight}
|
||||||
|
enableVerticalBorders={table.enableVerticalBorders}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
query={query}
|
||||||
|
serverId={server.id}
|
||||||
|
size={table.size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case ListPaginationType.PAGINATED: {
|
||||||
|
return (
|
||||||
|
<GenreListPaginatedTable
|
||||||
|
autoFitColumns={table.autoFitColumns}
|
||||||
|
columns={table.columns}
|
||||||
|
enableAlternateRowColors={table.enableAlternateRowColors}
|
||||||
|
enableHorizontalBorders={table.enableHorizontalBorders}
|
||||||
|
enableRowHoverHighlight={table.enableRowHoverHighlight}
|
||||||
|
enableVerticalBorders={table.enableVerticalBorders}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
query={query}
|
||||||
|
serverId={server.id}
|
||||||
|
size={table.size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,354 +1,40 @@
|
|||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
|
||||||
|
|
||||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import { MouseEvent, MutableRefObject, useCallback, useMemo } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
|
||||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
|
|
||||||
import { GENRE_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
import { GENRE_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
|
||||||
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
|
|
||||||
import { FolderButton } from '/@/renderer/features/shared/components/folder-button';
|
|
||||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||||
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
|
import { ListFilters } from '/@/renderer/features/shared/components/list-filters';
|
||||||
import { OrderToggleButton } from '/@/renderer/features/shared/components/order-toggle-button';
|
import { ListMusicFolderDropdown } from '/@/renderer/features/shared/components/list-music-folder-dropdown';
|
||||||
import { RefreshButton } from '/@/renderer/features/shared/components/refresh-button';
|
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
|
||||||
|
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
|
||||||
|
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
import { useContainerQuery } from '/@/renderer/hooks';
|
||||||
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh';
|
|
||||||
import {
|
|
||||||
GenreListFilter,
|
|
||||||
GenreTarget,
|
|
||||||
PersistedTableColumn,
|
|
||||||
useCurrentServer,
|
|
||||||
useGeneralSettings,
|
|
||||||
useListStoreActions,
|
|
||||||
useListStoreByKey,
|
|
||||||
useSettingsStoreActions,
|
|
||||||
} from '/@/renderer/store';
|
|
||||||
import { Button } from '/@/shared/components/button/button';
|
|
||||||
import { Divider } from '/@/shared/components/divider/divider';
|
import { Divider } from '/@/shared/components/divider/divider';
|
||||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
import { Flex } from '/@/shared/components/flex/flex';
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { GenreListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-types';
|
||||||
import {
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
GenreListQuery,
|
|
||||||
GenreListSort,
|
|
||||||
LibraryItem,
|
|
||||||
ServerType,
|
|
||||||
SortOrder,
|
|
||||||
} from '/@/shared/types/domain-types';
|
|
||||||
import { ListDisplayType } from '/@/shared/types/types';
|
|
||||||
|
|
||||||
const FILTERS = {
|
export const GenreListHeaderFilters = () => {
|
||||||
jellyfin: [
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.ASC,
|
|
||||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
|
||||||
value: GenreListSort.NAME,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
navidrome: [
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.ASC,
|
|
||||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
|
||||||
value: GenreListSort.NAME,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
subsonic: [
|
|
||||||
{
|
|
||||||
defaultOrder: SortOrder.ASC,
|
|
||||||
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
|
||||||
value: GenreListSort.NAME,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
interface GenreListHeaderFiltersProps {
|
|
||||||
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
|
|
||||||
itemCount: number | undefined;
|
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GenreListHeaderFilters = ({
|
|
||||||
gridRef,
|
|
||||||
itemCount,
|
|
||||||
tableRef,
|
|
||||||
}: GenreListHeaderFiltersProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { customFilters, pageKey } = useListContext();
|
|
||||||
const server = useCurrentServer();
|
|
||||||
const { setDisplayType, setFilter, setGrid, setTable } = useListStoreActions();
|
|
||||||
const { display, filter, grid, table } = useListStoreByKey<GenreListQuery>({ key: pageKey });
|
|
||||||
const cq = useContainerQuery();
|
const cq = useContainerQuery();
|
||||||
const { genreTarget } = useGeneralSettings();
|
|
||||||
const { setGenreBehavior } = useSettingsStoreActions();
|
|
||||||
|
|
||||||
const { handleRefreshGrid, handleRefreshTable } = useListFilterRefresh({
|
|
||||||
itemCount,
|
|
||||||
itemType: LibraryItem.GENRE,
|
|
||||||
server,
|
|
||||||
});
|
|
||||||
|
|
||||||
const musicFoldersQuery = useQuery(
|
|
||||||
sharedQueries.musicFolders({ query: null, serverId: server?.id }),
|
|
||||||
);
|
|
||||||
|
|
||||||
const sortByLabel =
|
|
||||||
(server?.type &&
|
|
||||||
FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filter.sortBy)
|
|
||||||
?.name) ||
|
|
||||||
'Unknown';
|
|
||||||
|
|
||||||
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.GRID;
|
|
||||||
|
|
||||||
const onFilterChange = useCallback(
|
|
||||||
(filter: GenreListFilter) => {
|
|
||||||
if (isGrid) {
|
|
||||||
handleRefreshGrid(gridRef, {
|
|
||||||
...filter,
|
|
||||||
...customFilters,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
handleRefreshTable(tableRef, {
|
|
||||||
...filter,
|
|
||||||
...customFilters,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[customFilters, gridRef, handleRefreshGrid, handleRefreshTable, isGrid, tableRef],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.genres.list(server?.id || '') });
|
|
||||||
onFilterChange(filter);
|
|
||||||
}, [filter, onFilterChange, queryClient, server?.id]);
|
|
||||||
|
|
||||||
const handleSetSortBy = useCallback(
|
|
||||||
(e: MouseEvent<HTMLButtonElement>) => {
|
|
||||||
if (!e.currentTarget?.value || !server?.type) return;
|
|
||||||
|
|
||||||
const sortOrder = FILTERS[server.type as keyof typeof FILTERS].find(
|
|
||||||
(f) => f.value === e.currentTarget.value,
|
|
||||||
)?.defaultOrder;
|
|
||||||
|
|
||||||
const updatedFilters = setFilter({
|
|
||||||
customFilters,
|
|
||||||
data: {
|
|
||||||
sortBy: e.currentTarget.value as GenreListSort,
|
|
||||||
sortOrder: sortOrder || SortOrder.ASC,
|
|
||||||
},
|
|
||||||
itemType: LibraryItem.GENRE,
|
|
||||||
key: pageKey,
|
|
||||||
}) as GenreListFilter;
|
|
||||||
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
},
|
|
||||||
[customFilters, onFilterChange, pageKey, server?.type, setFilter],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSetMusicFolder = useCallback(
|
|
||||||
(e: MouseEvent<HTMLButtonElement>) => {
|
|
||||||
if (!e.currentTarget?.value) return;
|
|
||||||
|
|
||||||
let updatedFilters: GenreListFilter | null = null;
|
|
||||||
if (e.currentTarget.value === String(filter.musicFolderId)) {
|
|
||||||
updatedFilters = setFilter({
|
|
||||||
customFilters,
|
|
||||||
data: { musicFolderId: undefined },
|
|
||||||
itemType: LibraryItem.GENRE,
|
|
||||||
key: pageKey,
|
|
||||||
}) as GenreListFilter;
|
|
||||||
} else {
|
|
||||||
updatedFilters = setFilter({
|
|
||||||
customFilters,
|
|
||||||
data: { musicFolderId: e.currentTarget.value },
|
|
||||||
itemType: LibraryItem.GENRE,
|
|
||||||
key: pageKey,
|
|
||||||
}) as GenreListFilter;
|
|
||||||
}
|
|
||||||
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
},
|
|
||||||
[filter.musicFolderId, onFilterChange, setFilter, customFilters, pageKey],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleToggleSortOrder = useCallback(() => {
|
|
||||||
const newSortOrder = filter.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
|
|
||||||
const updatedFilters = setFilter({
|
|
||||||
customFilters,
|
|
||||||
data: { sortOrder: newSortOrder },
|
|
||||||
itemType: LibraryItem.GENRE,
|
|
||||||
key: pageKey,
|
|
||||||
}) as GenreListFilter;
|
|
||||||
onFilterChange(updatedFilters);
|
|
||||||
}, [customFilters, filter.sortOrder, onFilterChange, pageKey, setFilter]);
|
|
||||||
|
|
||||||
const handleItemSize = (e: number) => {
|
|
||||||
if (isGrid) {
|
|
||||||
setGrid({ data: { itemSize: e }, key: pageKey });
|
|
||||||
} else {
|
|
||||||
setTable({ data: { rowHeight: e }, key: pageKey });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const debouncedHandleItemSize = debounce(handleItemSize, 20);
|
|
||||||
|
|
||||||
const handleItemGap = (e: number) => {
|
|
||||||
setGrid({ data: { itemGap: e }, key: pageKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSetViewType = useCallback(
|
|
||||||
(displayType: ListDisplayType) => {
|
|
||||||
setDisplayType({ data: displayType, key: pageKey });
|
|
||||||
},
|
|
||||||
[pageKey, setDisplayType],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTableColumns = (values: string[]) => {
|
|
||||||
const existingColumns = table.columns;
|
|
||||||
|
|
||||||
if (values.length === 0) {
|
|
||||||
return setTable({
|
|
||||||
data: { columns: [] },
|
|
||||||
key: pageKey,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If adding a column
|
|
||||||
if (values.length > existingColumns.length) {
|
|
||||||
const newColumn = {
|
|
||||||
column: values[values.length - 1],
|
|
||||||
width: 100,
|
|
||||||
} as PersistedTableColumn;
|
|
||||||
|
|
||||||
setTable({ data: { columns: [...existingColumns, newColumn] }, key: pageKey });
|
|
||||||
} else {
|
|
||||||
// If removing a column
|
|
||||||
const removed = existingColumns.filter((column) => !values.includes(column.column));
|
|
||||||
const newColumns = existingColumns.filter((column) => !removed.includes(column));
|
|
||||||
|
|
||||||
setTable({ data: { columns: newColumns }, key: pageKey });
|
|
||||||
}
|
|
||||||
|
|
||||||
return tableRef.current?.api.sizeColumnsToFit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAutoFitColumns = (autoFitColumns: boolean) => {
|
|
||||||
setTable({ data: { autoFit: autoFitColumns }, key: pageKey });
|
|
||||||
|
|
||||||
if (autoFitColumns) {
|
|
||||||
tableRef.current?.api.sizeColumnsToFit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const isFolderFilterApplied = useMemo(() => {
|
|
||||||
return filter.musicFolderId !== undefined;
|
|
||||||
}, [filter.musicFolderId]);
|
|
||||||
|
|
||||||
const handleGenreToggle = useCallback(() => {
|
|
||||||
const newState = genreTarget === GenreTarget.ALBUM ? GenreTarget.TRACK : GenreTarget.ALBUM;
|
|
||||||
setGenreBehavior(newState);
|
|
||||||
}, [genreTarget, setGenreBehavior]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justify="space-between">
|
<Flex justify="space-between">
|
||||||
<Group gap="sm" ref={cq.ref} w="100%">
|
<Group gap="sm" ref={cq.ref} w="100%">
|
||||||
<DropdownMenu position="bottom-start">
|
<ListSortByDropdown
|
||||||
<DropdownMenu.Target>
|
defaultSortByValue={GenreListSort.NAME}
|
||||||
<Button variant="subtle">{sortByLabel}</Button>
|
itemType={LibraryItem.GENRE}
|
||||||
</DropdownMenu.Target>
|
listKey={ItemListKey.GENRE}
|
||||||
<DropdownMenu.Dropdown>
|
|
||||||
{FILTERS[server?.type as keyof typeof FILTERS].map((f) => (
|
|
||||||
<DropdownMenu.Item
|
|
||||||
isSelected={f.value === filter.sortBy}
|
|
||||||
key={`filter-${f.name}`}
|
|
||||||
onClick={handleSetSortBy}
|
|
||||||
value={f.value}
|
|
||||||
>
|
|
||||||
{f.name}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
))}
|
|
||||||
</DropdownMenu.Dropdown>
|
|
||||||
</DropdownMenu>
|
|
||||||
<Divider orientation="vertical" />
|
|
||||||
<OrderToggleButton onToggle={handleToggleSortOrder} sortOrder={filter.sortOrder} />
|
|
||||||
{server?.type === ServerType.JELLYFIN && (
|
|
||||||
<>
|
|
||||||
<Divider orientation="vertical" />
|
|
||||||
<DropdownMenu position="bottom-start">
|
|
||||||
<DropdownMenu.Target>
|
|
||||||
<FolderButton
|
|
||||||
isActive={isFolderFilterApplied}
|
|
||||||
onClick={handleSetMusicFolder}
|
|
||||||
/>
|
/>
|
||||||
</DropdownMenu.Target>
|
<Divider orientation="vertical" />
|
||||||
<DropdownMenu.Dropdown>
|
<ListSortOrderToggleButton
|
||||||
{musicFoldersQuery.data?.items.map((folder) => (
|
defaultSortOrder={SortOrder.ASC}
|
||||||
<DropdownMenu.Item
|
listKey={ItemListKey.GENRE}
|
||||||
isSelected={filter.musicFolderId === folder.id}
|
/>
|
||||||
key={`musicFolder-${folder.id}`}
|
<ListMusicFolderDropdown listKey={ItemListKey.GENRE} />
|
||||||
onClick={handleSetMusicFolder}
|
<ListFilters itemType={LibraryItem.GENRE} />
|
||||||
value={folder.id}
|
<ListRefreshButton listKey={ItemListKey.GENRE} />
|
||||||
>
|
|
||||||
{folder.name}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
))}
|
|
||||||
</DropdownMenu.Dropdown>
|
|
||||||
</DropdownMenu>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<RefreshButton onClick={handleRefresh} />
|
|
||||||
<DropdownMenu position="bottom-start">
|
|
||||||
<DropdownMenu.Target>
|
|
||||||
<MoreButton />
|
|
||||||
</DropdownMenu.Target>
|
|
||||||
<DropdownMenu.Dropdown>
|
|
||||||
<DropdownMenu.Item
|
|
||||||
leftSection={<Icon icon="refresh" />}
|
|
||||||
onClick={handleRefresh}
|
|
||||||
>
|
|
||||||
{t('common.refresh', { postProcess: 'titleCase' })}
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Dropdown>
|
|
||||||
<Button
|
|
||||||
onClick={handleGenreToggle}
|
|
||||||
size="compact-md"
|
|
||||||
tooltip={{
|
|
||||||
label: t(
|
|
||||||
genreTarget === GenreTarget.ALBUM
|
|
||||||
? 'page.genreList.showTracks'
|
|
||||||
: 'page.genreList.showAlbums',
|
|
||||||
{ postProcess: 'sentenceCase' },
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
variant="subtle"
|
|
||||||
>
|
|
||||||
{genreTarget === GenreTarget.ALBUM ? (
|
|
||||||
<Icon icon="itemAlbum" />
|
|
||||||
) : (
|
|
||||||
<Icon icon="itemSong" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</DropdownMenu>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
<ListConfigMenu
|
<ListConfigMenu
|
||||||
autoFitColumns={table.autoFit}
|
listKey={ItemListKey.GENRE}
|
||||||
disabledViewTypes={[ListDisplayType.LIST]}
|
|
||||||
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={GENRE_TABLE_COLUMNS}
|
tableColumnsData={GENRE_TABLE_COLUMNS}
|
||||||
/>
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -1,70 +1,40 @@
|
|||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
|
||||||
|
|
||||||
import debounce from 'lodash/debounce';
|
|
||||||
import { ChangeEvent, MutableRefObject } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
||||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { GenreListHeaderFilters } from '/@/renderer/features/genres/components/genre-list-header-filters';
|
import { GenreListHeaderFilters } from '/@/renderer/features/genres/components/genre-list-header-filters';
|
||||||
import { FilterBar } from '/@/renderer/features/shared/components/filter-bar';
|
import { FilterBar } from '/@/renderer/features/shared/components/filter-bar';
|
||||||
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
|
||||||
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
|
import { ListSearchInput } from '/@/renderer/features/shared/components/list-search-input';
|
||||||
import { useContainerQuery } from '/@/renderer/hooks';
|
|
||||||
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
|
|
||||||
import { GenreListFilter, useCurrentServer } from '/@/renderer/store';
|
|
||||||
import { Flex } from '/@/shared/components/flex/flex';
|
|
||||||
import { Group } from '/@/shared/components/group/group';
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Stack } from '/@/shared/components/stack/stack';
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { GenreListQuery, LibraryItem } from '/@/shared/types/domain-types';
|
|
||||||
|
|
||||||
interface GenreListHeaderProps {
|
interface GenreListHeaderProps {
|
||||||
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
|
title?: string;
|
||||||
itemCount?: number;
|
|
||||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GenreListHeader = ({ gridRef, itemCount, tableRef }: GenreListHeaderProps) => {
|
export const GenreListHeader = ({ title }: GenreListHeaderProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const cq = useContainerQuery();
|
|
||||||
const server = useCurrentServer();
|
|
||||||
const { filter, refresh, search } = useDisplayRefresh<GenreListQuery>({
|
|
||||||
gridRef,
|
|
||||||
itemType: LibraryItem.GENRE,
|
|
||||||
server,
|
|
||||||
tableRef,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSearch = debounce((e: ChangeEvent<HTMLInputElement>) => {
|
const { itemCount } = useListContext();
|
||||||
const updatedFilters = search(e) as GenreListFilter;
|
const pageTitle = title || t('page.genreList.title', { postProcess: 'titleCase' });
|
||||||
refresh(updatedFilters);
|
|
||||||
}, 500);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={0} ref={cq.ref}>
|
<Stack gap={0}>
|
||||||
<PageHeader>
|
<PageHeader backgroundColor="var(--theme-colors-background)">
|
||||||
<Flex justify="space-between" w="100%">
|
|
||||||
<LibraryHeaderBar>
|
<LibraryHeaderBar>
|
||||||
<LibraryHeaderBar.Title>
|
<LibraryHeaderBar.PlayButton />
|
||||||
{t('page.genreList.title', { postProcess: 'titleCase' })}
|
<LibraryHeaderBar.Title>{pageTitle}</LibraryHeaderBar.Title>
|
||||||
</LibraryHeaderBar.Title>
|
<LibraryHeaderBar.Badge isLoading={!itemCount}>
|
||||||
<LibraryHeaderBar.Badge
|
|
||||||
isLoading={itemCount === null || itemCount === undefined}
|
|
||||||
>
|
|
||||||
{itemCount}
|
{itemCount}
|
||||||
</LibraryHeaderBar.Badge>
|
</LibraryHeaderBar.Badge>
|
||||||
</LibraryHeaderBar>
|
</LibraryHeaderBar>
|
||||||
<Group>
|
<Group>
|
||||||
<SearchInput defaultValue={filter.searchTerm} onChange={handleSearch} />
|
<ListSearchInput />
|
||||||
</Group>
|
</Group>
|
||||||
</Flex>
|
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<FilterBar>
|
<FilterBar>
|
||||||
<GenreListHeaderFilters
|
<GenreListHeaderFilters />
|
||||||
gridRef={gridRef}
|
|
||||||
itemCount={itemCount}
|
|
||||||
tableRef={tableRef}
|
|
||||||
/>
|
|
||||||
</FilterBar>
|
</FilterBar>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { UseSuspenseQueryOptions } from '@tanstack/react-query';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { useItemListInfiniteLoader } from '/@/renderer/components/item-list/helpers/item-list-infinite-loader';
|
||||||
|
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||||
|
import { ItemGridList } from '/@/renderer/components/item-list/item-grid-list/item-grid-list';
|
||||||
|
import { ItemListGridComponentProps } from '/@/renderer/components/item-list/types';
|
||||||
|
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
||||||
|
import {
|
||||||
|
GenreListQuery,
|
||||||
|
GenreListSort,
|
||||||
|
LibraryItem,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
interface GenreListInfiniteGridProps extends ItemListGridComponentProps<GenreListQuery> {}
|
||||||
|
|
||||||
|
export const GenreListInfiniteGrid = forwardRef<any, GenreListInfiniteGridProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
gap = 'md',
|
||||||
|
itemsPerPage = 100,
|
||||||
|
itemsPerRow,
|
||||||
|
query = {
|
||||||
|
sortBy: GenreListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
},
|
||||||
|
saveScrollOffset = true,
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const listCountQuery = genresQueries.listCount({
|
||||||
|
query: { ...query },
|
||||||
|
serverId: serverId,
|
||||||
|
}) as UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
|
||||||
|
|
||||||
|
const listQueryFn = api.controller.getGenreList;
|
||||||
|
|
||||||
|
const { data, onRangeChanged } = useItemListInfiniteLoader({
|
||||||
|
eventKey: ItemListKey.GENRE,
|
||||||
|
itemsPerPage,
|
||||||
|
itemType: LibraryItem.GENRE,
|
||||||
|
listCountQuery,
|
||||||
|
listQueryFn,
|
||||||
|
query,
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
|
||||||
|
enabled: saveScrollOffset,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemGridList
|
||||||
|
data={data}
|
||||||
|
gap={gap}
|
||||||
|
initialTop={scrollOffset ?? 0}
|
||||||
|
itemsPerRow={itemsPerRow}
|
||||||
|
itemType={LibraryItem.GENRE}
|
||||||
|
onRangeChanged={onRangeChanged}
|
||||||
|
onScrollEnd={handleOnScrollEnd}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { UseSuspenseQueryOptions } from '@tanstack/react-query';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { useItemListInfiniteLoader } from '/@/renderer/components/item-list/helpers/item-list-infinite-loader';
|
||||||
|
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||||
|
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||||
|
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||||
|
import { ItemListTableComponentProps } from '/@/renderer/components/item-list/types';
|
||||||
|
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
||||||
|
import {
|
||||||
|
GenreListQuery,
|
||||||
|
GenreListSort,
|
||||||
|
LibraryItem,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
interface GenreListInfiniteTableProps extends ItemListTableComponentProps<GenreListQuery> {}
|
||||||
|
|
||||||
|
export const GenreListInfiniteTable = forwardRef<any, GenreListInfiniteTableProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
autoFitColumns = false,
|
||||||
|
columns,
|
||||||
|
enableAlternateRowColors = false,
|
||||||
|
enableHorizontalBorders = false,
|
||||||
|
enableRowHoverHighlight = true,
|
||||||
|
enableSelection = true,
|
||||||
|
enableVerticalBorders = false,
|
||||||
|
itemsPerPage = 100,
|
||||||
|
query = {
|
||||||
|
sortBy: GenreListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
},
|
||||||
|
saveScrollOffset = true,
|
||||||
|
serverId,
|
||||||
|
size = 'default',
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const listCountQuery = genresQueries.listCount({
|
||||||
|
query: { ...query },
|
||||||
|
serverId: serverId,
|
||||||
|
}) as UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
|
||||||
|
|
||||||
|
const listQueryFn = api.controller.getGenreList;
|
||||||
|
|
||||||
|
const { data, onRangeChanged } = useItemListInfiniteLoader({
|
||||||
|
eventKey: ItemListKey.GENRE,
|
||||||
|
itemsPerPage,
|
||||||
|
itemType: LibraryItem.GENRE,
|
||||||
|
listCountQuery,
|
||||||
|
listQueryFn,
|
||||||
|
query,
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
|
||||||
|
enabled: saveScrollOffset,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemTableList
|
||||||
|
autoFitColumns={autoFitColumns}
|
||||||
|
CellComponent={ItemTableListColumn}
|
||||||
|
columns={columns}
|
||||||
|
data={data}
|
||||||
|
enableAlternateRowColors={enableAlternateRowColors}
|
||||||
|
enableExpansion={false}
|
||||||
|
enableHorizontalBorders={enableHorizontalBorders}
|
||||||
|
enableRowHoverHighlight={enableRowHoverHighlight}
|
||||||
|
enableSelection={enableSelection}
|
||||||
|
enableVerticalBorders={enableVerticalBorders}
|
||||||
|
initialTop={{
|
||||||
|
to: scrollOffset ?? 0,
|
||||||
|
type: 'offset',
|
||||||
|
}}
|
||||||
|
itemType={LibraryItem.GENRE}
|
||||||
|
onRangeChanged={onRangeChanged}
|
||||||
|
onScrollEnd={handleOnScrollEnd}
|
||||||
|
ref={ref}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { UseSuspenseQueryOptions } from '@tanstack/react-query';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { useItemListPaginatedLoader } from '/@/renderer/components/item-list/helpers/item-list-paginated-loader';
|
||||||
|
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||||
|
import { ItemGridList } from '/@/renderer/components/item-list/item-grid-list/item-grid-list';
|
||||||
|
import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination';
|
||||||
|
import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination';
|
||||||
|
import { ItemListGridComponentProps } from '/@/renderer/components/item-list/types';
|
||||||
|
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
||||||
|
import {
|
||||||
|
GenreListQuery,
|
||||||
|
GenreListSort,
|
||||||
|
LibraryItem,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
interface GenreListPaginatedGridProps extends ItemListGridComponentProps<GenreListQuery> {}
|
||||||
|
|
||||||
|
export const GenreListPaginatedGrid = forwardRef<any, GenreListPaginatedGridProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
gap = 'md',
|
||||||
|
itemsPerPage = 100,
|
||||||
|
itemsPerRow,
|
||||||
|
query = {
|
||||||
|
sortBy: GenreListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
},
|
||||||
|
saveScrollOffset = true,
|
||||||
|
serverId,
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const listCountQuery = genresQueries.listCount({
|
||||||
|
query: { ...query },
|
||||||
|
serverId: serverId,
|
||||||
|
}) as UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
|
||||||
|
|
||||||
|
const listQueryFn = api.controller.getGenreList;
|
||||||
|
|
||||||
|
const { currentPage, onChange } = useItemListPagination();
|
||||||
|
|
||||||
|
const { data, pageCount, totalItemCount } = useItemListPaginatedLoader({
|
||||||
|
currentPage,
|
||||||
|
itemsPerPage,
|
||||||
|
itemType: LibraryItem.GENRE,
|
||||||
|
listCountQuery,
|
||||||
|
listQueryFn,
|
||||||
|
query,
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
|
||||||
|
enabled: saveScrollOffset,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemListWithPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
onChange={onChange}
|
||||||
|
pageCount={pageCount}
|
||||||
|
totalItemCount={totalItemCount}
|
||||||
|
>
|
||||||
|
<ItemGridList
|
||||||
|
currentPage={currentPage}
|
||||||
|
data={data || []}
|
||||||
|
gap={gap}
|
||||||
|
initialTop={{
|
||||||
|
to: scrollOffset ?? 0,
|
||||||
|
type: 'offset',
|
||||||
|
}}
|
||||||
|
itemsPerRow={itemsPerRow}
|
||||||
|
itemType={LibraryItem.GENRE}
|
||||||
|
onScrollEnd={handleOnScrollEnd}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
</ItemListWithPagination>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import { UseSuspenseQueryOptions } from '@tanstack/react-query';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
|
||||||
|
import { api } from '/@/renderer/api';
|
||||||
|
import { useItemListPaginatedLoader } from '/@/renderer/components/item-list/helpers/item-list-paginated-loader';
|
||||||
|
import { useItemListScrollPersist } from '/@/renderer/components/item-list/helpers/use-item-list-scroll-persist';
|
||||||
|
import { ItemListWithPagination } from '/@/renderer/components/item-list/item-list-pagination/item-list-pagination';
|
||||||
|
import { useItemListPagination } from '/@/renderer/components/item-list/item-list-pagination/use-item-list-pagination';
|
||||||
|
import { ItemTableList } from '/@/renderer/components/item-list/item-table-list/item-table-list';
|
||||||
|
import { ItemTableListColumn } from '/@/renderer/components/item-list/item-table-list/item-table-list-column';
|
||||||
|
import { ItemListTableComponentProps } from '/@/renderer/components/item-list/types';
|
||||||
|
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
||||||
|
import {
|
||||||
|
GenreListQuery,
|
||||||
|
GenreListSort,
|
||||||
|
LibraryItem,
|
||||||
|
SortOrder,
|
||||||
|
} from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
|
interface GenreListPaginatedTableProps extends ItemListTableComponentProps<GenreListQuery> {}
|
||||||
|
|
||||||
|
export const GenreListPaginatedTable = forwardRef<any, GenreListPaginatedTableProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
autoFitColumns = false,
|
||||||
|
columns,
|
||||||
|
enableAlternateRowColors = false,
|
||||||
|
enableHorizontalBorders = false,
|
||||||
|
enableRowHoverHighlight = true,
|
||||||
|
enableSelection = true,
|
||||||
|
enableVerticalBorders = false,
|
||||||
|
itemsPerPage = 100,
|
||||||
|
query = {
|
||||||
|
sortBy: GenreListSort.NAME,
|
||||||
|
sortOrder: SortOrder.ASC,
|
||||||
|
},
|
||||||
|
saveScrollOffset = true,
|
||||||
|
serverId,
|
||||||
|
size = 'default',
|
||||||
|
},
|
||||||
|
ref,
|
||||||
|
) => {
|
||||||
|
const listCountQuery = genresQueries.listCount({
|
||||||
|
query: { ...query },
|
||||||
|
serverId: serverId,
|
||||||
|
}) as UseSuspenseQueryOptions<number, Error, number, readonly unknown[]>;
|
||||||
|
|
||||||
|
const listQueryFn = api.controller.getGenreList;
|
||||||
|
|
||||||
|
const { currentPage, onChange } = useItemListPagination();
|
||||||
|
|
||||||
|
const { data, pageCount, totalItemCount } = useItemListPaginatedLoader({
|
||||||
|
currentPage,
|
||||||
|
itemsPerPage,
|
||||||
|
itemType: LibraryItem.GENRE,
|
||||||
|
listCountQuery,
|
||||||
|
listQueryFn,
|
||||||
|
query,
|
||||||
|
serverId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
|
||||||
|
enabled: saveScrollOffset,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ItemListWithPagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
itemsPerPage={itemsPerPage}
|
||||||
|
onChange={onChange}
|
||||||
|
pageCount={pageCount}
|
||||||
|
totalItemCount={totalItemCount}
|
||||||
|
>
|
||||||
|
<ItemTableList
|
||||||
|
autoFitColumns={autoFitColumns}
|
||||||
|
CellComponent={ItemTableListColumn}
|
||||||
|
columns={columns}
|
||||||
|
currentPage={currentPage}
|
||||||
|
data={data || []}
|
||||||
|
enableAlternateRowColors={enableAlternateRowColors}
|
||||||
|
enableExpansion={false}
|
||||||
|
enableHorizontalBorders={enableHorizontalBorders}
|
||||||
|
enableRowHoverHighlight={enableRowHoverHighlight}
|
||||||
|
enableSelection={enableSelection}
|
||||||
|
enableVerticalBorders={enableVerticalBorders}
|
||||||
|
initialTop={{
|
||||||
|
to: scrollOffset ?? 0,
|
||||||
|
type: 'offset',
|
||||||
|
}}
|
||||||
|
itemType={LibraryItem.GENRE}
|
||||||
|
onScrollEnd={handleOnScrollEnd}
|
||||||
|
ref={ref}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
</ItemListWithPagination>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { useMusicFolderIdFilter } from '/@/renderer/features/shared/hooks/use-music-folder-id-filter';
|
||||||
|
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
|
||||||
|
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
|
||||||
|
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
|
||||||
|
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||||
|
import { GenreListSort } from '/@/shared/types/domain-types';
|
||||||
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
|
export const useGenreListFilters = () => {
|
||||||
|
const { sortBy } = useSortByFilter<GenreListSort>(null, ItemListKey.GENRE);
|
||||||
|
|
||||||
|
const { sortOrder } = useSortOrderFilter(null, ItemListKey.GENRE);
|
||||||
|
|
||||||
|
const { musicFolderId } = useMusicFolderIdFilter(null, ItemListKey.GENRE);
|
||||||
|
|
||||||
|
const { searchTerm, setSearchTerm } = useSearchTermFilter('');
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
[FILTER_KEYS.SHARED.MUSIC_FOLDER_ID]: musicFolderId ?? undefined,
|
||||||
|
[FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
|
||||||
|
[FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
|
||||||
|
[FILTER_KEYS.SHARED.SORT_ORDER]: sortOrder ?? undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
setSearchTerm,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,50 +1,30 @@
|
|||||||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
import { useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { useQuery } from '@tanstack/react-query';
|
|
||||||
import { useMemo, useRef } from 'react';
|
|
||||||
|
|
||||||
import { ListContext } from '/@/renderer/context/list-context';
|
import { ListContext } from '/@/renderer/context/list-context';
|
||||||
import { genresQueries } from '/@/renderer/features/genres/api/genres-api';
|
import { GenreListContent } from '/@/renderer/features/genres/components/genre-list-content';
|
||||||
import { GenreListHeader } from '/@/renderer/features/genres/components/genre-list-header';
|
import { GenreListHeader } from '/@/renderer/features/genres/components/genre-list-header';
|
||||||
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
import { useListStoreByKey } from '/@/renderer/store/list.store';
|
|
||||||
import { GenreListQuery } from '/@/shared/types/domain-types';
|
|
||||||
|
|
||||||
const GenreListRoute = () => {
|
const GenreListRoute = () => {
|
||||||
const gridRef = useRef<null>(null);
|
const pageKey = ItemListKey.GENRE;
|
||||||
const tableRef = useRef<AgGridReactType | null>(null);
|
|
||||||
const server = useCurrentServer();
|
|
||||||
const pageKey = 'genre';
|
|
||||||
const { filter } = useListStoreByKey<GenreListQuery>({ key: pageKey });
|
|
||||||
|
|
||||||
const itemCountCheck = useQuery(
|
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
|
||||||
genresQueries.list({
|
|
||||||
query: {
|
|
||||||
...filter,
|
|
||||||
limit: 1,
|
|
||||||
startIndex: 0,
|
|
||||||
},
|
|
||||||
serverId: server?.id,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const itemCount =
|
|
||||||
itemCountCheck.data?.totalRecordCount === null
|
|
||||||
? undefined
|
|
||||||
: itemCountCheck.data?.totalRecordCount;
|
|
||||||
|
|
||||||
const providerValue = useMemo(() => {
|
const providerValue = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
|
id: undefined,
|
||||||
|
itemCount,
|
||||||
pageKey,
|
pageKey,
|
||||||
|
setItemCount,
|
||||||
};
|
};
|
||||||
}, []);
|
}, [itemCount, pageKey, setItemCount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
<ListContext.Provider value={providerValue}>
|
<ListContext.Provider value={providerValue}>
|
||||||
<GenreListHeader gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} />
|
<GenreListHeader />
|
||||||
{/* <GenreListContent gridRef={gridRef} itemCount={itemCount} tableRef={tableRef} /> */}
|
<GenreListContent />
|
||||||
</ListContext.Provider>
|
</ListContext.Provider>
|
||||||
</AnimatedPage>
|
</AnimatedPage>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
|||||||
import {
|
import {
|
||||||
AlbumArtistListSort,
|
AlbumArtistListSort,
|
||||||
AlbumListSort,
|
AlbumListSort,
|
||||||
|
GenreListSort,
|
||||||
LibraryItem,
|
LibraryItem,
|
||||||
ServerType,
|
ServerType,
|
||||||
SongListSort,
|
SongListSort,
|
||||||
@@ -444,8 +445,35 @@ const ALBUM_ARTIST_LIST_FILTERS: Partial<
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const GENRE_LIST_FILTERS: Partial<
|
||||||
|
Record<ServerType, Array<{ defaultOrder: SortOrder; name: string; value: string }>>
|
||||||
|
> = {
|
||||||
|
[ServerType.JELLYFIN]: [
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||||
|
value: GenreListSort.NAME,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[ServerType.NAVIDROME]: [
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||||
|
value: GenreListSort.NAME,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[ServerType.SUBSONIC]: [
|
||||||
|
{
|
||||||
|
defaultOrder: SortOrder.ASC,
|
||||||
|
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
|
||||||
|
value: GenreListSort.NAME,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const FILTERS: Partial<Record<LibraryItem, any>> = {
|
const FILTERS: Partial<Record<LibraryItem, any>> = {
|
||||||
[LibraryItem.ALBUM]: ALBUM_LIST_FILTERS,
|
[LibraryItem.ALBUM]: ALBUM_LIST_FILTERS,
|
||||||
[LibraryItem.ALBUM_ARTIST]: ALBUM_ARTIST_LIST_FILTERS,
|
[LibraryItem.ALBUM_ARTIST]: ALBUM_ARTIST_LIST_FILTERS,
|
||||||
|
[LibraryItem.GENRE]: GENRE_LIST_FILTERS,
|
||||||
[LibraryItem.SONG]: SONG_LIST_FILTERS,
|
[LibraryItem.SONG]: SONG_LIST_FILTERS,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ import { createWithEqualityFn } from 'zustand/traditional';
|
|||||||
|
|
||||||
import i18n from '/@/i18n/i18n';
|
import i18n from '/@/i18n/i18n';
|
||||||
import {
|
import {
|
||||||
|
ALBUM_ARTIST_TABLE_COLUMNS,
|
||||||
ALBUM_TABLE_COLUMNS,
|
ALBUM_TABLE_COLUMNS,
|
||||||
|
GENRE_TABLE_COLUMNS,
|
||||||
pickTableColumns,
|
pickTableColumns,
|
||||||
PLAYLIST_SONG_TABLE_COLUMNS,
|
PLAYLIST_SONG_TABLE_COLUMNS,
|
||||||
PLAYLIST_TABLE_COLUMNS,
|
PLAYLIST_TABLE_COLUMNS,
|
||||||
SONG_TABLE_COLUMNS,
|
SONG_TABLE_COLUMNS,
|
||||||
} from '/@/renderer/components/item-list/item-table-list/default-columns';
|
} from '/@/renderer/components/item-list/item-table-list/default-columns';
|
||||||
import { ALBUMARTIST_TABLE_COLUMNS } from '/@/renderer/components/virtual-table/table-config-dropdown';
|
|
||||||
import { ContextMenuItemType } from '/@/renderer/features/context-menu/events';
|
import { ContextMenuItemType } from '/@/renderer/features/context-menu/events';
|
||||||
import { AppRoute } from '/@/renderer/router/routes';
|
import { AppRoute } from '/@/renderer/router/routes';
|
||||||
import { mergeOverridingColumns } from '/@/renderer/store/utils';
|
import { mergeOverridingColumns } from '/@/renderer/store/utils';
|
||||||
@@ -706,13 +707,13 @@ const initialState: SettingsState = {
|
|||||||
pagination: ListPaginationType.INFINITE,
|
pagination: ListPaginationType.INFINITE,
|
||||||
table: {
|
table: {
|
||||||
autoFitColumns: false,
|
autoFitColumns: false,
|
||||||
columns: ALBUMARTIST_TABLE_COLUMNS.map((column) => ({
|
columns: ALBUM_ARTIST_TABLE_COLUMNS.map((column) => ({
|
||||||
align: 'start' as const,
|
align: column.align,
|
||||||
autoSize: false,
|
autoSize: column.autoSize,
|
||||||
id: column.value,
|
id: column.value,
|
||||||
isEnabled: true,
|
isEnabled: column.isEnabled,
|
||||||
pinned: null,
|
pinned: column.pinned,
|
||||||
width: 200,
|
width: column.width,
|
||||||
})),
|
})),
|
||||||
enableAlternateRowColors: true,
|
enableAlternateRowColors: true,
|
||||||
enableHorizontalBorders: true,
|
enableHorizontalBorders: true,
|
||||||
@@ -732,13 +733,13 @@ const initialState: SettingsState = {
|
|||||||
pagination: ListPaginationType.INFINITE,
|
pagination: ListPaginationType.INFINITE,
|
||||||
table: {
|
table: {
|
||||||
autoFitColumns: false,
|
autoFitColumns: false,
|
||||||
columns: ALBUMARTIST_TABLE_COLUMNS.map((column) => ({
|
columns: ALBUM_ARTIST_TABLE_COLUMNS.map((column) => ({
|
||||||
align: 'start' as const,
|
align: column.align,
|
||||||
autoSize: false,
|
autoSize: column.autoSize,
|
||||||
id: column.value,
|
id: column.value,
|
||||||
isEnabled: true,
|
isEnabled: column.isEnabled,
|
||||||
pinned: null,
|
pinned: column.pinned,
|
||||||
width: 200,
|
width: column.width,
|
||||||
})),
|
})),
|
||||||
enableAlternateRowColors: true,
|
enableAlternateRowColors: true,
|
||||||
enableHorizontalBorders: true,
|
enableHorizontalBorders: true,
|
||||||
@@ -747,6 +748,32 @@ const initialState: SettingsState = {
|
|||||||
size: 'default',
|
size: 'default',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[LibraryItem.GENRE]: {
|
||||||
|
display: ListDisplayType.TABLE,
|
||||||
|
grid: {
|
||||||
|
itemGap: 'md',
|
||||||
|
itemsPerRow: 6,
|
||||||
|
itemsPerRowEnabled: false,
|
||||||
|
},
|
||||||
|
itemsPerPage: 100,
|
||||||
|
pagination: ListPaginationType.INFINITE,
|
||||||
|
table: {
|
||||||
|
autoFitColumns: false,
|
||||||
|
columns: GENRE_TABLE_COLUMNS.map((column) => ({
|
||||||
|
align: column.align,
|
||||||
|
autoSize: column.autoSize,
|
||||||
|
id: column.value,
|
||||||
|
isEnabled: column.isEnabled,
|
||||||
|
pinned: column.pinned,
|
||||||
|
width: column.width,
|
||||||
|
})),
|
||||||
|
enableAlternateRowColors: true,
|
||||||
|
enableHorizontalBorders: true,
|
||||||
|
enableRowHoverHighlight: true,
|
||||||
|
enableVerticalBorders: false,
|
||||||
|
size: 'compact',
|
||||||
|
},
|
||||||
|
},
|
||||||
[LibraryItem.PLAYLIST]: {
|
[LibraryItem.PLAYLIST]: {
|
||||||
display: ListDisplayType.TABLE,
|
display: ListDisplayType.TABLE,
|
||||||
grid: {
|
grid: {
|
||||||
@@ -872,8 +899,8 @@ const initialState: SettingsState = {
|
|||||||
TableColumn.USER_FAVORITE,
|
TableColumn.USER_FAVORITE,
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
enableAlternateRowColors: true,
|
enableAlternateRowColors: false,
|
||||||
enableHorizontalBorders: true,
|
enableHorizontalBorders: false,
|
||||||
enableRowHoverHighlight: true,
|
enableRowHoverHighlight: true,
|
||||||
enableVerticalBorders: false,
|
enableVerticalBorders: false,
|
||||||
size: 'default',
|
size: 'default',
|
||||||
|
|||||||
Reference in New Issue
Block a user