feat: playlist grid view (#1476)

* feat: grid view for playlists
This commit is contained in:
Flutter
2026-01-01 21:21:47 +01:00
committed by GitHub
parent b7627fd469
commit e5c5985f0f
4 changed files with 112 additions and 9 deletions
@@ -2,11 +2,12 @@ import { useQueryClient, useSuspenseQuery } from '@tanstack/react-query';
import { lazy, Suspense, useEffect, useMemo, useRef, useState } from 'react';
import { useParams } from 'react-router';
import { PlaylistDetailSongListEditTable } from './playlist-detail-song-list-table';
import { ItemListHandle } from '/@/renderer/components/item-list/types';
import { useListContext } from '/@/renderer/context/list-context';
import { eventEmitter } from '/@/renderer/events/event-emitter';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { PlaylistDetailSongListEditTable } from '/@/renderer/features/playlists/components/playlist-detail-song-list-table';
import { useCurrentServer, useListSettings } from '/@/renderer/store';
import { Spinner } from '/@/shared/components/spinner/spinner';
import { PlaylistSongListQuery, PlaylistSongListResponse } from '/@/shared/types/domain-types';
@@ -20,6 +21,14 @@ const PlaylistDetailSongListTable = lazy(() =>
),
);
const PlaylistDetailSongListGrid = lazy(() =>
import('/@/renderer/features/playlists/components/playlist-detail-song-list-grid').then(
(module) => ({
default: module.PlaylistDetailSongListGrid,
}),
),
);
export const PlaylistDetailSongListContent = () => {
const { playlistId } = useParams() as { playlistId: string };
const server = useCurrentServer();
@@ -82,6 +91,9 @@ export const PlaylistDetailSongListView = ({ data }: { data: PlaylistSongListRes
const { display, table } = useListSettings(ItemListKey.PLAYLIST_SONG);
switch (display) {
case ListDisplayType.GRID: {
return <PlaylistDetailSongListGrid data={data} serverId={server.id} />;
}
case ListDisplayType.TABLE: {
return (
<PlaylistDetailSongListTable
@@ -210,6 +222,7 @@ export const PlaylistDetailSongListEdit = ({ data }: { data: PlaylistSongListRes
}, [localData, setListData]);
switch (display) {
case ListDisplayType.GRID:
case ListDisplayType.TABLE: {
return (
<PlaylistDetailSongListEditTable
@@ -0,0 +1,76 @@
import { forwardRef, useMemo } from 'react';
import { useEffect } from 'react';
import { useGridRows } from '/@/renderer/components/item-list/helpers/use-grid-rows';
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 { useListContext } from '/@/renderer/context/list-context';
import { usePlaylistSongListFilters } from '/@/renderer/features/playlists/hooks/use-playlist-song-list-filters';
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
import { searchLibraryItems } from '/@/renderer/features/shared/utils';
import { useListSettings } from '/@/renderer/store';
import { sortSongList } from '/@/shared/api/utils';
import {
LibraryItem,
PlaylistSongListQuery,
PlaylistSongListResponse,
} from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
interface PlaylistDetailSongListGridProps
extends Omit<ItemListGridComponentProps<PlaylistSongListQuery>, 'query'> {
data: PlaylistSongListResponse;
}
export const PlaylistDetailSongListGrid = forwardRef<any, PlaylistDetailSongListGridProps>(
({ data, saveScrollOffset = true }) => {
const { handleOnScrollEnd, scrollOffset } = useItemListScrollPersist({
enabled: saveScrollOffset,
});
const { searchTerm } = useSearchTermFilter();
const { query } = usePlaylistSongListFilters();
const { setListData } = useListContext();
const songData = useMemo(() => {
let items = data?.items || [];
if (searchTerm) {
items = searchLibraryItems(items, searchTerm, LibraryItem.SONG);
}
return sortSongList(items, query.sortBy, query.sortOrder);
}, [data?.items, searchTerm, query.sortBy, query.sortOrder]);
useEffect(() => {
if (setListData) {
setListData(songData);
}
}, [songData, setListData]);
const gridProps = useListSettings(ItemListKey.PLAYLIST_SONG).grid;
const rows = useGridRows(
LibraryItem.PLAYLIST_SONG,
ItemListKey.PLAYLIST_SONG,
gridProps.size,
);
return (
<ItemGridList
data={songData}
gap={gridProps.itemGap}
initialTop={{
to: scrollOffset ?? 0,
type: 'offset',
}}
itemsPerRow={gridProps.itemsPerRowEnabled ? gridProps.itemsPerRow : undefined}
itemType={LibraryItem.SONG}
onScrollEnd={handleOnScrollEnd}
rows={rows}
size={gridProps.size}
/>
);
},
);
@@ -10,6 +10,7 @@ import { useListContext } from '/@/renderer/context/list-context';
import { ContextMenuController } from '/@/renderer/features/context-menu/context-menu-controller';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
import { ListDisplayTypeToggleButton } from '/@/renderer/features/shared/components/list-display-type-toggle-button';
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
import { ListSearchInput } from '/@/renderer/features/shared/components/list-search-input';
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
@@ -26,7 +27,7 @@ import { Icon } from '/@/shared/components/icon/icon';
import { Tooltip } from '/@/shared/components/tooltip/tooltip';
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
import { LibraryItem, SongListSort, SortOrder } from '/@/shared/types/domain-types';
import { ItemListKey, ListDisplayType } from '/@/shared/types/types';
import { ItemListKey } from '/@/shared/types/types';
interface PlaylistDetailSongListHeaderFiltersProps {
isSmartPlaylist?: boolean;
@@ -108,13 +109,8 @@ export const PlaylistDetailSongListHeaderFilters = ({
variant="subtle"
/>
</Tooltip>
<ListDisplayTypeToggleButton listKey={ItemListKey.PLAYLIST_SONG} />
<ListConfigMenu
displayTypes={[
{
hidden: true,
value: ListDisplayType.GRID,
},
]}
listKey={ItemListKey.PLAYLIST_SONG}
tableColumnsData={PLAYLIST_SONG_TABLE_COLUMNS}
/>
+19 -1
View File
@@ -1300,7 +1300,25 @@ const initialState: SettingsState = {
itemGap: 'sm',
itemsPerRow: 6,
itemsPerRowEnabled: false,
rows: [],
rows: pickGridRows({
alignLeftColumns: [TableColumn.TITLE, TableColumn.ARTIST],
columns: PLAYLIST_SONG_TABLE_COLUMNS,
enabledColumns: [TableColumn.TITLE, TableColumn.ARTIST],
pickColumns: [
TableColumn.TITLE,
TableColumn.ARTIST,
TableColumn.DURATION,
TableColumn.YEAR,
TableColumn.BIT_RATE,
TableColumn.BPM,
TableColumn.CODEC,
TableColumn.DATE_ADDED,
TableColumn.GENRE,
TableColumn.LAST_PLAYED,
TableColumn.RELEASE_DATE,
TableColumn.TRACK_NUMBER,
],
}),
size: 'default',
},
itemsPerPage: 100,