diff --git a/src/renderer/api/jellyfin.api.ts b/src/renderer/api/jellyfin.api.ts index 74e058e3..4810b3d5 100644 --- a/src/renderer/api/jellyfin.api.ts +++ b/src/renderer/api/jellyfin.api.ts @@ -376,8 +376,11 @@ const getPlaylistSongList = async (args: PlaylistSongListArgs): Promise` - position: absolute; - top: 0; - z-index: 0; - width: 100%; - height: 100%; - background: ${(props) => props.background}; -`; - -const BackgroundImageOverlay = styled.div` - position: absolute; - top: 0; - left: 0; - z-index: 0; - width: 100%; - height: 100%; - background: linear-gradient(180deg, rgba(25, 26, 28, 5%), var(--main-bg)), var(--background-noise); -`; - -interface PlaylistDetailHeaderProps { - background: string; - imageUrl?: string; -} - -export const PlaylistDetailHeader = forwardRef( - ({ background, imageUrl }: PlaylistDetailHeaderProps, ref) => { - const { playlistId } = useParams() as { playlistId: string }; - const detailQuery = usePlaylistDetail({ id: playlistId }); - const cq = useContainerQuery(); - - const mergedRef = useMergedRef(ref, cq.ref); - - const titleSize = cq.isXl - ? '6rem' - : cq.isLg - ? '5.5rem' - : cq.isMd - ? '4.5rem' - : cq.isSm - ? '3.5rem' - : '2rem'; - - return ( - <> - - - - - {imageUrl ? ( - - ) : ( -
- -
- )} -
- - - - Playlist - - - - {detailQuery?.data?.name} - - - - - -
- - ); - }, -); diff --git a/src/renderer/features/playlists/components/playlist-detail-content.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx similarity index 51% rename from src/renderer/features/playlists/components/playlist-detail-content.tsx rename to src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx index 22007d9a..7b1162eb 100644 --- a/src/renderer/features/playlists/components/playlist-detail-content.tsx +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-content.tsx @@ -3,72 +3,73 @@ import type { BodyScrollEvent, CellContextMenuEvent, ColDef, + GridReadyEvent, + IDatasource, + PaginationChangedEvent, RowDoubleClickedEvent, } from '@ag-grid-community/core'; import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; -import { getColumnDefs, VirtualTable } from '/@/renderer/components'; -import { useCurrentServer, useSetSongTable, useSongListStore } from '/@/renderer/store'; -import { LibraryItem } from '/@/renderer/types'; +import { getColumnDefs, TablePagination, VirtualTable } from '/@/renderer/components'; +import { + useCurrentServer, + usePlaylistDetailStore, + usePlaylistDetailTablePagination, + useSetPlaylistDetailTable, + useSetPlaylistDetailTablePagination, +} from '/@/renderer/store'; +import { LibraryItem, ListDisplayType } from '/@/renderer/types'; +import { useQueryClient } from '@tanstack/react-query'; +import { AnimatePresence } from 'framer-motion'; import debounce from 'lodash/debounce'; import { openContextMenu } from '/@/renderer/features/context-menu'; import { SONG_CONTEXT_MENU_ITEMS } from '/@/renderer/features/context-menu/context-menu-items'; import sortBy from 'lodash/sortBy'; import { usePlayButtonBehavior } from '/@/renderer/store/settings.store'; -import { QueueSong } from '/@/renderer/api/types'; +import { PlaylistSongListQuery, QueueSong, SongListSort, SortOrder } from '/@/renderer/api/types'; import { usePlaylistSongList } from '/@/renderer/features/playlists/queries/playlist-song-list-query'; import { useParams } from 'react-router'; -import styled from 'styled-components'; import { usePlayQueueAdd } from '/@/renderer/features/player'; - -const ContentContainer = styled.div` - display: flex; - flex-direction: column; - max-width: 1920px; - padding: 1rem 2rem; - overflow: hidden; - - .ag-theme-alpine-dark { - --ag-header-background-color: rgba(0, 0, 0, 0%); - } - - .ag-header-container { - z-index: 1000; - } - - .ag-header-cell-resize { - top: 25%; - width: 7px; - height: 50%; - background-color: rgb(70, 70, 70, 20%); - } -`; +import { api } from '/@/renderer/api'; +import { queryKeys } from '/@/renderer/api/query-keys'; interface PlaylistDetailContentProps { tableRef: MutableRefObject; } -export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps) => { +export const PlaylistDetailSongListContent = ({ tableRef }: PlaylistDetailContentProps) => { const { playlistId } = useParams() as { playlistId: string }; - // const queryClient = useQueryClient(); + const queryClient = useQueryClient(); const server = useCurrentServer(); - const page = useSongListStore(); + const page = usePlaylistDetailStore(); + const filters: Partial = 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 pagination = useSongTablePagination(); - // const setPagination = useSetSongTablePagination(); - const setTable = useSetSongTable(); + 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 isPaginationEnabled = page.display === ListDisplayType.TABLE_PAGINATED; - const playlistSongsQuery = usePlaylistSongList({ + const checkPlaylistList = usePlaylistSongList({ id: playlistId, - limit: 50, + limit: 1, startIndex: 0, }); - console.log('checkPlaylistList.data', playlistSongsQuery.data); - const columnDefs: ColDef[] = useMemo( () => getColumnDefs(page.table.columns), [page.table.columns], @@ -82,58 +83,66 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps) }; }, []); - // const onGridReady = useCallback( - // (params: GridReadyEvent) => { - // const dataSource: IDatasource = { - // getRows: async (params) => { - // const limit = params.endRow - params.startRow; - // const startIndex = params.startRow; + const onGridReady = useCallback( + (params: GridReadyEvent) => { + const dataSource: IDatasource = { + getRows: async (params) => { + const limit = params.endRow - params.startRow; + const startIndex = params.startRow; - // const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId, { - // id: playlistId, - // limit, - // startIndex, - // }); + const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId, { + id: playlistId, + limit, + startIndex, + ...filters, + }); - // const songsRes = await queryClient.fetchQuery(queryKey, async ({ signal }) => - // api.controller.getPlaylistSongList({ - // query: { - // id: playlistId, - // limit, - // startIndex, - // }, - // server, - // signal, - // }), - // ); + const songsRes = await queryClient.fetchQuery(queryKey, async ({ signal }) => + api.controller.getPlaylistSongList({ + query: { + id: playlistId, + limit, + startIndex, + ...filters, + }, + server, + signal, + }), + ); - // const songs = api.normalize.songList(songsRes, server); - // params.successCallback(songs?.items || [], songsRes?.totalRecordCount); - // }, - // rowCount: undefined, - // }; - // params.api.setDatasource(dataSource); - // params.api.ensureIndexVisible(page.table.scrollOffset, 'top'); - // }, - // [page.table.scrollOffset, playlistId, queryClient, server], - // ); + const songs = api.normalize.songList(songsRes, server); + params.successCallback(songs?.items || [], songsRes?.totalRecordCount); + }, + rowCount: undefined, + }; + params.api.setDatasource(dataSource); + params.api.ensureIndexVisible(pagination.scrollOffset, 'top'); + }, + [filters, pagination.scrollOffset, playlistId, queryClient, server], + ); - // const onPaginationChanged = useCallback( - // (event: PaginationChangedEvent) => { - // if (!isPaginationEnabled || !event.api) return; + const onPaginationChanged = useCallback( + (event: PaginationChangedEvent) => { + if (!isPaginationEnabled || !event.api) return; - // // Scroll to top of page on pagination change - // const currentPageStartIndex = pagination.currentPage * pagination.itemsPerPage; - // event.api?.ensureIndexVisible(currentPageStartIndex, 'top'); + // Scroll to top of page on pagination change + const currentPageStartIndex = pagination.currentPage * pagination.itemsPerPage; + event.api?.ensureIndexVisible(currentPageStartIndex, 'top'); - // setPagination({ - // itemsPerPage: event.api.paginationGetPageSize(), - // totalItems: event.api.paginationGetRowCount(), - // totalPages: event.api.paginationGetTotalPages() + 1, - // }); - // }, - // [isPaginationEnabled, pagination.currentPage, pagination.itemsPerPage, setPagination], - // ); + setPagination(playlistId, { + itemsPerPage: event.api.paginationGetPageSize(), + totalItems: event.api.paginationGetRowCount(), + totalPages: event.api.paginationGetTotalPages() + 1, + }); + }, + [ + isPaginationEnabled, + pagination.currentPage, + pagination.itemsPerPage, + playlistId, + setPagination, + ], + ); const handleGridSizeChange = () => { if (page.table.autoFit) { @@ -169,7 +178,7 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps) const handleScroll = (e: BodyScrollEvent) => { const scrollOffset = Number((e.top / page.table.rowHeight).toFixed(0)); - setTable({ scrollOffset }); + setPagination(playlistId, { scrollOffset }); }; const handleContextMenu = (e: CellContextMenuEvent) => { @@ -205,50 +214,58 @@ export const PlaylistDetailContent = ({ tableRef }: PlaylistDetailContentProps) }; return ( - + <> data.data.id} + infiniteInitialRowCount={checkPlaylistList.data?.totalRecordCount || 100} + pagination={isPaginationEnabled} + paginationAutoPageSize={isPaginationEnabled} + paginationPageSize={pagination.itemsPerPage || 100} + rowBuffer={20} + rowHeight={page.table.rowHeight || 40} + rowModelType="infinite" rowSelection="multiple" onBodyScrollEnd={handleScroll} onCellContextMenu={handleContextMenu} onColumnMoved={handleColumnChange} onColumnResized={debouncedColumnChange} - onGridReady={(params) => { - params.api.setDomLayout('autoHeight'); - params.api.sizeColumnsToFit(); - }} + onGridReady={onGridReady} onGridSizeChanged={handleGridSizeChange} + onPaginationChanged={onPaginationChanged} onRowDoubleClicked={handleRowDoubleClick} /> - {/* {page.display === ListDisplayType.TABLE_PAGINATED && ( )} - */} - + + ); }; diff --git a/src/renderer/features/playlists/components/playlist-detail-song-list-header.tsx b/src/renderer/features/playlists/components/playlist-detail-song-list-header.tsx new file mode 100644 index 00000000..a8e72fe2 --- /dev/null +++ b/src/renderer/features/playlists/components/playlist-detail-song-list-header.tsx @@ -0,0 +1,393 @@ +import { IDatasource } from '@ag-grid-community/core'; +import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; +import { Flex, Group, Stack } from '@mantine/core'; +import { useQueryClient } from '@tanstack/react-query'; +import { ChangeEvent, MutableRefObject, useCallback, MouseEvent } from 'react'; +import { RiArrowDownSLine, RiMoreFill, RiSortAsc, RiSortDesc } from 'react-icons/ri'; +import { useParams } from 'react-router'; +import styled from 'styled-components'; +import { api } from '/@/renderer/api'; +import { queryKeys } from '/@/renderer/api/query-keys'; +import { PlaylistSongListQuery, SongListSort, SortOrder } from '/@/renderer/api/types'; +import { + Button, + DropdownMenu, + MultiSelect, + PageHeader, + Slider, + SONG_TABLE_COLUMNS, + Switch, + Text, + TextTitle, +} from '/@/renderer/components'; +import { useContainerQuery } from '/@/renderer/hooks'; +import { + useCurrentServer, + usePlaylistDetailStore, + useSetPlaylistTablePagination, + useSetPlaylistDetailTable, + SongListFilter, + useSetPlaylistDetailFilters, + useSetPlaylistStore, +} from '/@/renderer/store'; +import { ListDisplayType, TableColumn } from '/@/renderer/types'; + +const FILTERS = { + jellyfin: [ + { defaultOrder: SortOrder.ASC, name: 'Album', value: SongListSort.ALBUM }, + { defaultOrder: SortOrder.ASC, name: 'Album Artist', value: SongListSort.ALBUM_ARTIST }, + { defaultOrder: SortOrder.ASC, name: 'Artist', value: SongListSort.ARTIST }, + { defaultOrder: SortOrder.ASC, name: 'Duration', value: SongListSort.DURATION }, + { defaultOrder: SortOrder.ASC, name: 'Most Played', value: SongListSort.PLAY_COUNT }, + { defaultOrder: SortOrder.ASC, name: 'Name', value: SongListSort.NAME }, + { defaultOrder: SortOrder.ASC, name: 'Random', value: SongListSort.RANDOM }, + { defaultOrder: SortOrder.ASC, name: 'Recently Added', value: SongListSort.RECENTLY_ADDED }, + { defaultOrder: SortOrder.ASC, name: 'Recently Played', value: SongListSort.RECENTLY_PLAYED }, + { defaultOrder: SortOrder.ASC, name: 'Release Date', value: SongListSort.RELEASE_DATE }, + ], + navidrome: [ + { defaultOrder: SortOrder.ASC, name: 'Album', value: SongListSort.ALBUM }, + { defaultOrder: SortOrder.ASC, name: 'Album Artist', value: SongListSort.ALBUM_ARTIST }, + { defaultOrder: SortOrder.ASC, name: 'Artist', value: SongListSort.ARTIST }, + { defaultOrder: SortOrder.DESC, name: 'BPM', value: SongListSort.BPM }, + { defaultOrder: SortOrder.ASC, name: 'Channels', value: SongListSort.CHANNELS }, + { defaultOrder: SortOrder.ASC, name: 'Comment', value: SongListSort.COMMENT }, + { defaultOrder: SortOrder.DESC, name: 'Duration', value: SongListSort.DURATION }, + { defaultOrder: SortOrder.DESC, name: 'Favorited', value: SongListSort.FAVORITED }, + { defaultOrder: SortOrder.ASC, name: 'Genre', value: SongListSort.GENRE }, + { defaultOrder: SortOrder.ASC, name: 'Id', value: SongListSort.ID }, + { defaultOrder: SortOrder.ASC, name: 'Name', value: SongListSort.NAME }, + { defaultOrder: SortOrder.DESC, name: 'Play Count', value: SongListSort.PLAY_COUNT }, + { defaultOrder: SortOrder.DESC, name: 'Rating', value: SongListSort.RATING }, + { defaultOrder: SortOrder.DESC, name: 'Recently Added', value: SongListSort.RECENTLY_ADDED }, + { defaultOrder: SortOrder.DESC, name: 'Recently Played', value: SongListSort.RECENTLY_PLAYED }, + { defaultOrder: SortOrder.DESC, name: 'Year', value: SongListSort.YEAR }, + ], +}; + +const ORDER = [ + { name: 'Ascending', value: SortOrder.ASC }, + { name: 'Descending', value: SortOrder.DESC }, +]; + +const HeaderItems = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; +`; + +interface PlaylistDetailHeaderProps { + tableRef: MutableRefObject; +} + +export const PlaylistDetailSongListHeader = ({ tableRef }: PlaylistDetailHeaderProps) => { + const { playlistId } = useParams() as { playlistId: string }; + + const queryClient = useQueryClient(); + const server = useCurrentServer(); + const setPage = useSetPlaylistStore(); + const setFilter = useSetPlaylistDetailFilters(); + const page = usePlaylistDetailStore(); + const filters: Partial = { + sortBy: page?.table.id[playlistId]?.filter?.sortBy || SongListSort.ID, + sortOrder: page?.table.id[playlistId]?.filter?.sortOrder || SortOrder.ASC, + }; + + const cq = useContainerQuery(); + + const setPagination = useSetPlaylistTablePagination(); + const setTable = useSetPlaylistDetailTable(); + + const sortByLabel = + (server?.type && + FILTERS[server.type as keyof typeof FILTERS].find((f) => f.value === filters.sortBy)?.name) || + 'Unknown'; + + const sortOrderLabel = ORDER.find((o) => o.value === filters.sortOrder)?.name || 'Unknown'; + + const handleItemSize = (e: number) => { + setTable({ rowHeight: e }); + }; + + const handleFilterChange = useCallback( + async (filters: SongListFilter) => { + const dataSource: IDatasource = { + getRows: async (params) => { + const limit = params.endRow - params.startRow; + const startIndex = params.startRow; + + const queryKey = queryKeys.playlists.songList(server?.id || '', playlistId, { + id: playlistId, + limit, + startIndex, + ...filters, + }); + + const songsRes = await queryClient.fetchQuery(queryKey, async ({ signal }) => + api.controller.getPlaylistSongList({ + query: { + id: playlistId, + limit, + startIndex, + ...filters, + }, + server, + signal, + }), + ); + + const songs = api.normalize.songList(songsRes, server); + params.successCallback(songs?.items || [], songsRes?.totalRecordCount || undefined); + }, + rowCount: undefined, + }; + tableRef.current?.api.setDatasource(dataSource); + tableRef.current?.api.purgeInfiniteCache(); + tableRef.current?.api.ensureIndexVisible(0, 'top'); + + if (page.display === ListDisplayType.TABLE_PAGINATED) { + setPagination({ currentPage: 0 }); + } + }, + [tableRef, page.display, server, playlistId, queryClient, setPagination], + ); + + const handleSetSortBy = useCallback( + (e: MouseEvent) => { + 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(playlistId, { + sortBy: e.currentTarget.value as SongListSort, + sortOrder: sortOrder || SortOrder.ASC, + }); + + handleFilterChange(updatedFilters); + }, + [handleFilterChange, playlistId, server?.type, setFilter], + ); + + const handleToggleSortOrder = useCallback(() => { + const newSortOrder = filters.sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC; + const updatedFilters = setFilter(playlistId, { sortOrder: newSortOrder }); + handleFilterChange(updatedFilters); + }, [filters.sortOrder, handleFilterChange, playlistId, setFilter]); + + const handleSetViewType = useCallback( + (e: MouseEvent) => { + if (!e.currentTarget?.value) return; + setPage({ detail: { ...page, display: e.currentTarget.value as ListDisplayType } }); + }, + [page, setPage], + ); + + const handleTableColumns = (values: TableColumn[]) => { + const existingColumns = page.table.columns; + + if (values.length === 0) { + return setTable({ + columns: [], + }); + } + + // If adding a column + if (values.length > existingColumns.length) { + const newColumn = { column: values[values.length - 1], width: 100 }; + + setTable({ columns: [...existingColumns, newColumn] }); + } else { + // If removing a column + const removed = existingColumns.filter((column) => !values.includes(column.column)); + const newColumns = existingColumns.filter((column) => !removed.includes(column)); + + setTable({ columns: newColumns }); + } + + return tableRef.current?.api.sizeColumnsToFit(); + }; + + const handleAutoFitColumns = (e: ChangeEvent) => { + setTable({ autoFit: e.currentTarget.checked }); + + if (e.currentTarget.checked) { + tableRef.current?.api.sizeColumnsToFit(); + } + }; + + return ( + + + + + + + + + Display type + + Table + + + Table (paginated) + + + Item size + + + + {(page.display === ListDisplayType.TABLE || + page.display === ListDisplayType.TABLE_PAGINATED) && ( + <> + Table Columns + + + column.column)} + width={300} + onChange={handleTableColumns} + /> + + Auto Fit Columns + + + + + + )} + + + + + + + + {FILTERS[server?.type as keyof typeof FILTERS].map((filter) => ( + + {filter.name} + + ))} + + + + + + + + + Play + Add to queue (next) + Add to queue (last) + Add to playlist + + + + + {/* */} + {/* + + + Playlist + + + + {detailQuery?.data?.name} + + + + + */} + {/* */} + + ); +}; diff --git a/src/renderer/features/playlists/routes/playlist-detail-route.tsx b/src/renderer/features/playlists/routes/playlist-detail-route.tsx index 0ec603f0..e520008e 100644 --- a/src/renderer/features/playlists/routes/playlist-detail-route.tsx +++ b/src/renderer/features/playlists/routes/playlist-detail-route.tsx @@ -1,76 +1,32 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; -import { Group } from '@mantine/core'; -import { useIntersection } from '@mantine/hooks'; import { useRef } from 'react'; import { useParams } from 'react-router'; -import { PageHeader, ScrollArea, TextTitle } from '/@/renderer/components'; -import { PlaylistDetailContent } from '/@/renderer/features/playlists/components/playlist-detail-content'; -import { PlaylistDetailHeader } from '/@/renderer/features/playlists/components/playlist-detail-header'; -import { usePlaylistDetail } from '/@/renderer/features/playlists/queries/playlist-detail-query'; -import { usePlaylistSongList } from '/@/renderer/features/playlists/queries/playlist-song-list-query'; -import { AnimatedPage, PlayButton } from '/@/renderer/features/shared'; -import { useFastAverageColor } from '/@/renderer/hooks'; +import { AnimatedPage } from '/@/renderer/features/shared'; const PlaylistDetailRoute = () => { - const tableRef = useRef(null); + // const tableRef = useRef(null); const { playlistId } = useParams() as { playlistId: string }; - const detailsQuery = usePlaylistDetail({ - id: playlistId, - }); + // const detailsQuery = usePlaylistDetail({ + // id: playlistId, + // }); - const playlistSongsQuery = usePlaylistSongList({ - id: playlistId, - limit: 50, - startIndex: 0, - }); + // const playlistSongsQuery = usePlaylistSongList({ + // id: playlistId, + // limit: 50, + // startIndex: 0, + // }); - const imageUrl = playlistSongsQuery.data?.items?.[0]?.imageUrl; - const background = useFastAverageColor(imageUrl); - const containerRef = useRef(); + // const imageUrl = playlistSongsQuery.data?.items?.[0]?.imageUrl; + // const background = useFastAverageColor(imageUrl); + // const containerRef = useRef(); - const { ref, entry } = useIntersection({ - root: containerRef.current, - threshold: 0.3, - }); + // const { ref, entry } = useIntersection({ + // root: containerRef.current, + // threshold: 0.3, + // }); - return ( - - - - - - {detailsQuery?.data?.name} - - - - - {background && ( - <> - - - - )} - - - ); + return Placeholder; }; export default PlaylistDetailRoute; diff --git a/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx b/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx new file mode 100644 index 00000000..95e9b859 --- /dev/null +++ b/src/renderer/features/playlists/routes/playlist-detail-song-list-route.tsx @@ -0,0 +1,23 @@ +import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; +import { useRef } from 'react'; +import { useParams } from 'react-router'; +import { VirtualGridContainer } from '/@/renderer/components'; +import { PlaylistDetailSongListContent } from '../components/playlist-detail-song-list-content'; +import { PlaylistDetailSongListHeader } from '../components/playlist-detail-song-list-header'; +import { AnimatedPage } from '/@/renderer/features/shared'; + +const PlaylistDetailSongListRoute = () => { + const tableRef = useRef(null); + const { playlistId } = useParams() as { playlistId: string }; + + return ( + + + + + + + ); +}; + +export default PlaylistDetailSongListRoute; diff --git a/src/renderer/features/sidebar/components/sidebar.tsx b/src/renderer/features/sidebar/components/sidebar.tsx index 43d3f79d..d5c20b7b 100644 --- a/src/renderer/features/sidebar/components/sidebar.tsx +++ b/src/renderer/features/sidebar/components/sidebar.tsx @@ -78,7 +78,7 @@ export const Sidebar = () => { const showImage = sidebar.image; const playlistsQuery = usePlaylistList({ - limit: 0, + limit: 100, sortBy: PlaylistListSort.NAME, sortOrder: SortOrder.ASC, startIndex: 0, diff --git a/src/renderer/router/app-router.tsx b/src/renderer/router/app-router.tsx index b5a23d07..889ae7ac 100644 --- a/src/renderer/router/app-router.tsx +++ b/src/renderer/router/app-router.tsx @@ -26,6 +26,10 @@ const PlaylistDetailRoute = lazy( () => import('/@/renderer/features/playlists/routes/playlist-detail-route'), ); +const PlaylistDetailSongListRoute = lazy( + () => import('/@/renderer/features/playlists/routes/playlist-detail-song-list-route'), +); + const PlaylistListRoute = lazy( () => import('/@/renderer/features/playlists/routes/playlist-list-route'), ); @@ -80,6 +84,10 @@ export const AppRouter = () => { element={} path={AppRoute.PLAYLISTS_DETAIL} /> + } + path={AppRoute.PLAYLISTS_DETAIL_SONGS} + /> } path={AppRoute.LIBRARY_ALBUMARTISTS} diff --git a/src/renderer/router/routes.ts b/src/renderer/router/routes.ts index 0913ae14..8fddd01e 100644 --- a/src/renderer/router/routes.ts +++ b/src/renderer/router/routes.ts @@ -16,6 +16,7 @@ export enum AppRoute { PLAYING = '/playing', PLAYLISTS = '/playlists', PLAYLISTS_DETAIL = '/playlists/:playlistId', + PLAYLISTS_DETAIL_SONGS = '/playlists/:playlistId/songs', SEARCH = '/search', SERVERS = '/servers', } diff --git a/src/renderer/store/playlist.store.ts b/src/renderer/store/playlist.store.ts index c2916319..e9dc99a7 100644 --- a/src/renderer/store/playlist.store.ts +++ b/src/renderer/store/playlist.store.ts @@ -4,6 +4,7 @@ import { devtools, persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; import { PlaylistListArgs, PlaylistListSort, SortOrder } from '/@/renderer/api/types'; import { DataTableProps } from '/@/renderer/store/settings.store'; +import { SongListFilter } from '/@/renderer/store/song.store'; import { ListDisplayType, TableColumn, TablePagination } from '/@/renderer/types'; type TableProps = { @@ -17,14 +18,33 @@ type ListProps = { table: TableProps; }; +type DetailPaginationProps = TablePagination & { + scrollOffset: number; +}; + +type DetailTableProps = DataTableProps & { + id: { + [key: string]: DetailPaginationProps & { filter: SongListFilter }; + }; +}; + +type DetailProps = { + display: ListDisplayType; + table: DetailTableProps; +}; + export type PlaylistListFilter = Omit; interface PlaylistState { + detail: DetailProps; list: ListProps; } export interface PlaylistSlice extends PlaylistState { actions: { + setDetailFilters: (id: string, data: Partial) => SongListFilter; + setDetailTable: (data: Partial) => void; + setDetailTablePagination: (id: string, data: Partial) => void; setFilters: (data: Partial) => PlaylistListFilter; setStore: (data: Partial) => void; setTable: (data: Partial) => void; @@ -37,6 +57,32 @@ export const usePlaylistStore = create()( devtools( immer((set, get) => ({ actions: { + setDetailFilters: (id, data) => { + set((state) => { + state.detail.table.id[id] = { + ...state.detail.table.id[id], + filter: { + ...state.detail.table.id[id].filter, + ...data, + }, + }; + }); + + return get().detail.table.id[id].filter; + }, + setDetailTable: (data) => { + set((state) => { + state.detail.table = { ...state.detail.table, ...data }; + }); + }, + setDetailTablePagination: (id, data) => { + set((state) => { + state.detail.table.id[id] = { + ...state.detail.table.id[id], + ...data, + }; + }); + }, setFilters: (data) => { set((state) => { state.list.filter = { ...state.list.filter, ...data }; @@ -58,6 +104,32 @@ export const usePlaylistStore = create()( }); }, }, + detail: { + display: ListDisplayType.TABLE, + table: { + autoFit: true, + columns: [ + { + column: TableColumn.ROW_INDEX, + width: 50, + }, + { + column: TableColumn.TITLE_COMBINED, + width: 500, + }, + { + column: TableColumn.DURATION, + width: 100, + }, + { + column: TableColumn.ALBUM, + width: 500, + }, + ], + id: {}, + rowHeight: 60, + }, + }, list: { display: ListDisplayType.TABLE, filter: { @@ -123,3 +195,17 @@ export const useSetPlaylistTablePagination = () => usePlaylistStore((state) => state.actions.setTablePagination); export const useSetPlaylistTable = () => usePlaylistStore((state) => state.actions.setTable); + +export const usePlaylistDetailStore = () => usePlaylistStore((state) => state.detail); + +export const usePlaylistDetailTablePagination = (id: string) => + usePlaylistStore((state) => state.detail.table.id[id]); + +export const useSetPlaylistDetailTablePagination = () => + usePlaylistStore((state) => state.actions.setDetailTablePagination); + +export const useSetPlaylistDetailTable = () => + usePlaylistStore((state) => state.actions.setDetailTable); + +export const useSetPlaylistDetailFilters = () => + usePlaylistStore((state) => state.actions.setDetailFilters);