temp commit for breaking changes

This commit is contained in:
jeffvli
2025-10-13 20:17:36 -07:00
parent 56907436a3
commit 6ff5affb58
30 changed files with 112 additions and 1423 deletions
@@ -1,312 +0,0 @@
import type {
BodyScrollEvent,
ColDef,
GridReadyEvent,
PaginationChangedEvent,
RowDoubleClickedEvent,
RowDragEvent,
} from '@ag-grid-community/core';
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 { AnimatePresence } from 'motion/react';
import { MutableRefObject, useCallback, useMemo } from 'react';
import { useParams } from 'react-router';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { getColumnDefs, TablePagination, VirtualTable } from '/@/renderer/components/virtual-table';
import { useCurrentSongRowStyles } from '/@/renderer/components/virtual-table/hooks/use-current-song-row-styles';
import {
PLAYLIST_SONG_CONTEXT_MENU_ITEMS,
SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS,
} from '/@/renderer/features/context-menu/context-menu-items';
import { useHandleTableContextMenu } from '/@/renderer/features/context-menu/hooks/use-handle-context-menu';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
import { useAppFocus } from '/@/renderer/hooks';
import {
useCurrentServer,
usePlayerSong,
usePlayerStatus,
usePlaylistDetailStore,
usePlaylistDetailTablePagination,
useSetPlaylistDetailTable,
useSetPlaylistDetailTablePagination,
} from '/@/renderer/store';
import { PersistedTableColumn, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { toast } from '/@/shared/components/toast/toast';
import {
LibraryItem,
PlaylistSongListQueryClientSide,
QueueSong,
ServerType,
Song,
SongListResponse,
SongListSort,
SortOrder,
} from '/@/shared/types/domain-types';
import { ListDisplayType } from '/@/shared/types/types';
interface PlaylistDetailContentProps {
songs?: Song[];
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const PlaylistDetailSongListContent = ({ songs, tableRef }: PlaylistDetailContentProps) => {
const { playlistId } = useParams() as { playlistId: string };
const queryClient = useQueryClient();
const status = usePlayerStatus();
const isFocused = useAppFocus();
const currentSong = usePlayerSong();
const server = useCurrentServer();
const page = usePlaylistDetailStore();
const filters: PlaylistSongListQueryClientSide = useMemo(() => {
return {
sortBy: page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID,
sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC,
};
}, [page?.table.id, playlistId]);
const detailQuery = useQuery(
playlistsQueries.detail({ query: { id: playlistId }, serverId: server?.id }),
);
const p = usePlaylistDetailTablePagination(playlistId);
const pagination = {
currentPage: p?.currentPage || 0,
itemsPerPage: p?.itemsPerPage || 100,
scrollOffset: p?.scrollOffset || 0,
totalItems: p?.totalItems || 1,
totalPages: p?.totalPages || 1,
};
const setPagination = useSetPlaylistDetailTablePagination();
const setTable = useSetPlaylistDetailTable();
const handlePlayQueueAdd = usePlayQueueAdd();
const playButtonBehavior = usePlayButtonBehavior();
const isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED;
const columnDefs: ColDef[] = useMemo(
() => getColumnDefs(page.table.columns, false, 'generic'),
[page.table.columns],
);
const onGridReady = useCallback(
(params: GridReadyEvent) => {
params.api?.ensureIndexVisible(pagination.scrollOffset, 'top');
},
[pagination.scrollOffset],
);
const handleDragEnd = useCallback(
async (e: RowDragEvent<Song>) => {
if (!e.nodes.length) return;
const trackId = e.node.data?.playlistItemId;
if (trackId && e.node.rowIndex !== null && e.overIndex !== e.node.rowIndex) {
try {
await api.controller.movePlaylistItem({
apiClientProps: {
serverId: server?.id || '',
},
query: {
endingIndex: e.overIndex,
playlistId,
startingIndex: e.node.rowIndex + 1,
trackId,
},
});
queryClient.setQueryData<SongListResponse>(
queryKeys.playlists.songList(server?.id || '', playlistId),
(previous) => {
if (previous?.items) {
const from = e.node.rowIndex!;
const to = e.overIndex;
const item = previous.items[from];
const remaining = previous.items.toSpliced(from, 1);
remaining.splice(to, 0, item);
return {
error: previous.error,
items: remaining,
startIndex: previous.startIndex,
totalRecordCount: previous.totalRecordCount,
};
}
return previous;
},
);
// Nodes have to be redrawn, otherwise the row indexes will be wrong
// Maybe it's possible to only redraw necessary rows to not be as expensive?
tableRef.current?.api.redrawRows();
} catch (error) {
toast.error({
message: (error as Error).message,
title: `Failed to move song ${e.node.data?.name} to ${e.overIndex}`,
});
}
}
},
[playlistId, queryClient, server, tableRef],
);
const handleGridSizeChange = () => {
if (page.table.autoFit) {
tableRef?.current?.api?.sizeColumnsToFit();
}
};
const onPaginationChanged = useCallback(
(event: PaginationChangedEvent) => {
if (!isPaginationEnabled || !event.api) return;
try {
// Scroll to top of page on pagination change
const currentPageStartIndex = pagination.currentPage * pagination.itemsPerPage;
event.api?.ensureIndexVisible(currentPageStartIndex, 'top');
} catch (err) {
console.error(err);
}
setPagination(playlistId, {
itemsPerPage: event.api.paginationGetPageSize(),
totalItems: event.api.paginationGetRowCount(),
totalPages: event.api.paginationGetTotalPages() + 1,
});
},
[
isPaginationEnabled,
pagination.currentPage,
pagination.itemsPerPage,
playlistId,
setPagination,
],
);
const handleColumnChange = useCallback(() => {
const { columnApi } = tableRef?.current || {};
const columnsOrder = columnApi?.getAllGridColumns();
if (!columnsOrder) return;
const columnsInSettings = page.table.columns;
const updatedColumns: PersistedTableColumn[] = [];
for (const column of columnsOrder) {
const columnInSettings = columnsInSettings.find(
(c) => c.column === column.getColDef().colId,
);
if (columnInSettings) {
updatedColumns.push({
...columnInSettings,
...(!page.table.autoFit && {
width: column.getActualWidth(),
}),
});
}
}
setTable({ columns: updatedColumns });
}, [page.table.autoFit, page.table.columns, setTable, tableRef]);
const debouncedColumnChange = debounce(handleColumnChange, 200);
const handleScroll = (e: BodyScrollEvent) => {
const scrollOffset = Number((e.top / page.table.rowHeight).toFixed(0));
setPagination(playlistId, { scrollOffset });
};
const contextMenuItems = useMemo(() => {
if (detailQuery?.data?.rules) {
return SMART_PLAYLIST_SONG_CONTEXT_MENU_ITEMS;
}
return PLAYLIST_SONG_CONTEXT_MENU_ITEMS;
}, [detailQuery?.data?.rules]);
const handleContextMenu = useHandleTableContextMenu(LibraryItem.SONG, contextMenuItems, {
playlistId,
tableRef,
});
const handleRowDoubleClick = (e: RowDoubleClickedEvent<QueueSong>) => {
if (!e.data) return;
handlePlayQueueAdd?.({
byItemType: {
id: [playlistId],
type: LibraryItem.PLAYLIST,
},
initialSongId: e.data.id,
playType: playButtonBehavior,
});
};
const { rowClassRules } = useCurrentSongRowStyles({ tableRef });
const canDrag =
filters.sortBy === SongListSort.ID &&
!detailQuery?.data?.rules &&
server?.type !== ServerType.SUBSONIC;
return (
<>
<VirtualGridAutoSizerContainer>
<VirtualTable
alwaysShowHorizontalScroll
autoFitColumns={page.table.autoFit}
columnDefs={columnDefs}
context={{
currentSong,
isFocused,
itemType: LibraryItem.SONG,
onCellContextMenu: handleContextMenu,
status,
}}
getRowId={(data) => data.data.uniqueId}
// 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}
onColumnResized={debouncedColumnChange}
onGridReady={onGridReady}
onGridSizeChanged={handleGridSizeChange}
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="clientSide"
shouldUpdateSong
/>
</VirtualGridAutoSizerContainer>
{isPaginationEnabled && (
<AnimatePresence initial={false} mode="wait" presenceAffectsLayout>
{page.display === ListDisplayType.TABLE_PAGINATED && (
<TablePagination
pageKey={playlistId}
pagination={pagination}
setIdPagination={setPagination}
tableRef={tableRef}
/>
)}
</AnimatePresence>
)}
</>
);
};
@@ -1,174 +0,0 @@
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 { controller } from '/@/renderer/api/controller';
import { queryKeys } from '/@/renderer/api/query-keys';
import { PLAYLIST_CARD_ROWS } from '/@/renderer/components/card/card-rows';
import { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import {
VirtualInfiniteGrid,
VirtualInfiniteGridRef,
} from '/@/renderer/components/virtual-grid/virtual-infinite-grid';
import { useListContext } from '/@/renderer/context/list-context';
import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add';
import { useHandleFavorite } from '/@/renderer/features/shared/hooks/use-handle-favorite';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer, useListStoreByKey } from '/@/renderer/store';
import { useListStoreActions } from '/@/renderer/store/list.store';
import {
LibraryItem,
Playlist,
PlaylistListQuery,
PlaylistListResponse,
PlaylistListSort,
} from '/@/shared/types/domain-types';
import { CardRow, ListDisplayType } from '/@/shared/types/types';
interface PlaylistListGridViewProps {
gridRef: MutableRefObject<null | VirtualInfiniteGridRef>;
itemCount?: number;
}
export const PlaylistListGridView = ({ gridRef, itemCount }: PlaylistListGridViewProps) => {
const { pageKey } = useListContext();
const queryClient = useQueryClient();
const server = useCurrentServer();
const handlePlayQueueAdd = usePlayQueueAdd();
const { display, filter, grid } = useListStoreByKey<PlaylistListQuery>({ key: pageKey });
const { setGrid } = useListStoreActions();
const handleFavorite = useHandleFavorite({ gridRef });
const cardRows = useMemo(() => {
const rows: CardRow<Playlist>[] = [PLAYLIST_CARD_ROWS.nameFull];
switch (filter.sortBy) {
case PlaylistListSort.DURATION:
rows.push(PLAYLIST_CARD_ROWS.duration);
break;
case PlaylistListSort.NAME:
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;
}
return rows;
}, [filter.sortBy]);
const handleGridScroll = useCallback(
(e: ListOnScrollProps) => {
setGrid({ data: { scrollOffset: e.scrollOffset }, key: pageKey });
},
[pageKey, setGrid],
);
const fetchInitialData = useCallback(() => {
const query: Omit<PlaylistListQuery, 'limit' | 'startIndex'> = {
...filter,
};
const queriesFromCache: [QueryKey, PlaylistListResponse | undefined][] =
queryClient.getQueriesData({
exact: false,
fetchStatus: 'idle',
queryKey: queryKeys.playlists.list(server?.id || '', query),
stale: false,
});
const itemData: Playlist[] = [];
for (const [, data] of queriesFromCache) {
const { items, startIndex } = data || {};
if (items && items.length !== 1 && startIndex !== undefined) {
let itemIndex = 0;
for (
let rowIndex = startIndex;
rowIndex < startIndex + items.length;
rowIndex += 1
) {
itemData[rowIndex] = items[itemIndex];
itemIndex += 1;
}
}
}
return itemData;
}, [filter, queryClient, server?.id]);
const fetch = useCallback(
async ({ skip, take }: { skip: number; take: number }) => {
if (!server) {
return [];
}
const query: PlaylistListQuery = {
limit: take,
...filter,
_custom: {},
startIndex: skip,
};
const queryKey = queryKeys.playlists.list(server?.id || '', query);
const playlists = await queryClient.fetchQuery({
queryFn: async ({ signal }) =>
controller.getPlaylistList({
apiClientProps: {
serverId: server?.id || '',
signal,
},
query,
}),
queryKey,
});
return playlists;
},
[filter, queryClient, server],
);
return (
<VirtualGridAutoSizerContainer>
<AutoSizer>
{({ height, width }: Size) => (
<VirtualInfiniteGrid
cardRows={cardRows}
display={display || ListDisplayType.CARD}
fetchFn={fetch}
fetchInitialData={fetchInitialData}
handleFavorite={handleFavorite}
handlePlayQueueAdd={handlePlayQueueAdd}
height={height}
initialScrollOffset={grid?.scrollOffset || 0}
itemCount={itemCount || 0}
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}
/>
)}
</AutoSizer>
</VirtualGridAutoSizerContainer>
);
};
@@ -1,49 +0,0 @@
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 { VirtualGridAutoSizerContainer } from '/@/renderer/components/virtual-grid/virtual-grid-wrapper';
import { VirtualTable } from '/@/renderer/components/virtual-table';
import { useVirtualTable } from '/@/renderer/components/virtual-table/hooks/use-virtual-table';
import { PLAYLIST_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items';
import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store';
import { LibraryItem } from '/@/shared/types/domain-types';
interface PlaylistListTableViewProps {
itemCount?: number;
tableRef: MutableRefObject<AgGridReactType | null>;
}
export const PlaylistListTableView = ({ itemCount, tableRef }: PlaylistListTableViewProps) => {
const navigate = useNavigate();
const server = useCurrentServer();
const pageKey = 'playlist';
const handleRowDoubleClick = (e: RowDoubleClickedEvent) => {
if (!e.data) return;
navigate(generatePath(AppRoute.PLAYLISTS_DETAIL_SONGS, { playlistId: e.data.id }));
};
const tableProps = useVirtualTable({
contextMenu: PLAYLIST_CONTEXT_MENU_ITEMS,
itemCount,
itemType: LibraryItem.PLAYLIST,
pageKey,
server,
tableRef,
});
return (
<VirtualGridAutoSizerContainer>
<VirtualTable
key={`table-${tableProps.rowHeight}-${server?.id}`}
ref={tableRef}
{...tableProps}
onRowDoubleClicked={handleRowDoubleClick}
/>
</VirtualGridAutoSizerContainer>
);
};