Add initial queue/album list routes

This commit is contained in:
jeffvli
2022-10-28 13:11:29 -07:00
parent c6d80831f8
commit 4fb963d689
8 changed files with 402 additions and 4 deletions
+3
View File
@@ -0,0 +1,3 @@
export * from './queries/use-album-detail';
export * from './queries/use-album-list';
export * from './routes/album-list-route';
@@ -0,0 +1,21 @@
import { useQuery } from '@tanstack/react-query';
import { api } from '@/renderer/api';
import { queryKeys } from '@/renderer/api/query-keys';
import { QueryOptions } from '@/renderer/lib/react-query';
import { useAuthStore } from '@/renderer/store';
import { AlbumDetailResponse } from 'renderer/api/types';
export const useAlbumDetail = (
query: { albumId: string },
options: QueryOptions<AlbumDetailResponse>
) => {
const serverId = useAuthStore((state) => state.currentServer?.id) || '';
return useQuery({
enabled: !!serverId,
queryFn: ({ signal }) =>
api.albums.getAlbumDetail({ albumId: query.albumId, serverId }, signal),
queryKey: queryKeys.albums.detail(query.albumId),
...options,
});
};
@@ -0,0 +1,33 @@
import { useInfiniteQuery, useQuery } from '@tanstack/react-query';
import { api } from '@/renderer/api';
import { AlbumListParams } from '@/renderer/api/albums.api';
import { queryKeys } from '@/renderer/api/query-keys';
import { useAuthStore } from '@/renderer/store';
import { AlbumListResponse } from 'renderer/api/types';
export const useAlbumList = (params: AlbumListParams) => {
const serverId = useAuthStore((state) => state.currentServer?.id) || '';
return useQuery({
enabled: !!serverId,
queryFn: () => api.albums.getAlbumList({ serverId }, params),
queryKey: queryKeys.albums.list(serverId, params),
});
};
export const useAlbumListInfinite = (params: AlbumListParams) => {
const serverId = useAuthStore((state) => state.currentServer?.id) || '';
return useInfiniteQuery({
enabled: !!serverId,
getNextPageParam: (lastPage: AlbumListResponse) => {
return !!lastPage.pagination.nextPage;
},
getPreviousPageParam: (firstPage: AlbumListResponse) => {
return !!firstPage.pagination.prevPage;
},
queryFn: ({ pageParam }) =>
api.albums.getAlbumList({ serverId }, { ...(pageParam || params) }),
queryKey: queryKeys.albums.list(serverId, params),
});
};
@@ -0,0 +1,213 @@
/* eslint-disable no-plusplus */
import { useState, useCallback, useMemo } from 'react';
import { Group, Checkbox } from '@mantine/core';
import { useSetState } from '@mantine/hooks';
import { useQueryClient } from '@tanstack/react-query';
import { RiArrowDownSLine } from 'react-icons/ri';
import AutoSizer from 'react-virtualized-auto-sizer';
import { api } from '@/renderer/api';
import { AlbumSort } from '@/renderer/api/albums.api';
import { queryKeys } from '@/renderer/api/query-keys';
import { SortOrder } from '@/renderer/api/types';
import {
Button,
DropdownMenu,
Text,
VirtualGridAutoSizerContainer,
VirtualGridContainer,
VirtualInfiniteGrid,
} from '@/renderer/components';
import { useAlbumList } from '@/renderer/features/albums/queries/use-album-list';
import { useServerList } from '@/renderer/features/servers';
import { AnimatedPage, useServerCredential } from '@/renderer/features/shared';
import { AppRoute } from '@/renderer/router/routes';
import { useAuthStore } from '@/renderer/store';
import { Font } from '@/renderer/styles';
import { LibraryItem } from '@/renderer/types';
import {
ViewType,
ViewTypeButton,
} from '../../library/components/ViewTypeButton';
const FILTERS = [
{ name: 'Date added', value: AlbumSort.DATE_ADDED },
{
name: 'Date added (remote)',
value: AlbumSort.DATE_ADDED_REMOTE,
},
{ name: 'Date released', value: AlbumSort.DATE_RELEASED },
{ name: 'Favorites', value: AlbumSort.FAVORITE },
{ name: 'Random', value: AlbumSort.RANDOM },
{ name: 'Rating', value: AlbumSort.RATING },
{ name: 'Title', value: AlbumSort.NAME },
{ name: 'Year', value: AlbumSort.DATE_RELEASED_YEAR },
];
const SORT = [
{ name: 'Ascending', value: SortOrder.ASC },
{ name: 'Descending', value: SortOrder.DESC },
];
export const AlbumListRoute = () => {
const queryClient = useQueryClient();
const { serverToken, isImageTokenRequired } = useServerCredential();
const serverId = useAuthStore((state) => state.currentServer?.id) || '';
const { data: servers } = useServerList({ enabled: true });
const [viewType, setViewType] = useState(ViewType.Grid);
const [filters, setFilters] = useSetState({
orderBy: SortOrder.ASC,
serverFolderId: [] as string[],
sortBy: AlbumSort.NAME,
});
const serverFolders = useMemo(() => {
const server = servers?.data.find((server) => server.id === serverId);
return server?.serverFolders;
}, [serverId, servers]);
const { data: albums } = useAlbumList({
orderBy: filters.orderBy,
serverFolderId: filters.serverFolderId,
skip: 0,
sortBy: filters.sortBy,
take: 0,
});
const fetch = useCallback(
async ({ skip, take }) => {
const albums = await queryClient.fetchQuery(
queryKeys.albums.list(serverId, { skip, take, ...filters }),
async () =>
api.albums.getAlbumList({ serverId }, { skip, take, ...filters })
);
// * Adds server token
if (isImageTokenRequired) {
const t = albums.data.map((album) => {
return {
...album,
imageUrl: album?.imageUrl + serverToken!,
};
});
return { ...albums, data: t };
}
return albums;
},
[filters, isImageTokenRequired, queryClient, serverId, serverToken]
);
return (
<AnimatedPage>
<VirtualGridContainer>
<Group m={10} position="apart">
<Group>
<Text font={Font.POPPINS} size="lg">
Albums
</Text>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button compact variant="subtle">
<Group>
{FILTERS.find((f) => f.value === filters.sortBy)?.name}{' '}
<RiArrowDownSLine size={15} />
</Group>
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
{FILTERS.map((filter) => (
<DropdownMenu.Item
key={`filter-${filter.value}`}
onClick={() => setFilters({ sortBy: filter.value })}
>
{filter.name}
</DropdownMenu.Item>
))}
</DropdownMenu.Dropdown>
</DropdownMenu>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button compact variant="subtle">
<Group>
{SORT.find((s) => s.value === filters.orderBy)?.name}{' '}
<RiArrowDownSLine size={15} />
</Group>
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
{SORT.map((sort) => (
<DropdownMenu.Item
key={`sort-${sort.value}`}
onClick={() => setFilters({ orderBy: sort.value })}
>
{sort.name}
</DropdownMenu.Item>
))}
</DropdownMenu.Dropdown>
</DropdownMenu>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button compact variant="subtle">
<Group>
Folders <RiArrowDownSLine size={15} />
</Group>
</Button>
</DropdownMenu.Target>
<DropdownMenu.Dropdown>
<Checkbox.Group
orientation="vertical"
value={filters.serverFolderId}
onChange={(e) => setFilters({ serverFolderId: e })}
>
{serverFolders?.map((folder) => (
<Checkbox
key={folder.id}
label={folder.name}
value={folder.id}
/>
))}
</Checkbox.Group>
</DropdownMenu.Dropdown>
</DropdownMenu>
</Group>
<ViewTypeButton
handler={setViewType}
menuProps={{ position: 'bottom-end' }}
type={viewType}
/>
</Group>
<VirtualGridAutoSizerContainer>
<AutoSizer>
{({ height, width }) => (
<VirtualInfiniteGrid
cardRows={[
{
align: 'center',
prop: 'name',
route: {
prop: 'id',
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
},
},
{
align: 'center',
prop: 'releaseYear',
},
]}
fetchFn={fetch}
height={height}
itemCount={albums?.pagination.totalEntries || 0}
itemGap={20}
itemSize={200}
itemType={LibraryItem.ALBUM}
minimumBatchSize={100}
width={width}
/>
)}
</AutoSizer>
</VirtualGridAutoSizerContainer>
</VirtualGridContainer>
</AnimatedPage>
);
};
@@ -0,0 +1 @@
export * from './routes/now-playing-route';
@@ -0,0 +1,123 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { ColDef, RowClassRules } from 'ag-grid-community';
import {
VirtualGridAutoSizerContainer,
VirtualGridContainer,
} from '@/renderer/components';
import { VirtualTable } from '@/renderer/components/virtual-table';
import { mpvPlayer } from '@/renderer/features/player/utils/mpvPlayer';
import { AnimatedPage } from '@/renderer/features/shared';
import { usePlayerStore } from '@/renderer/store';
const selector = (state: any) => state.queue.default;
export const NowPlayingRoute = () => {
const gridRef = useRef<any>(null);
const queue = usePlayerStore(selector);
const currentPlayerIndex = usePlayerStore((state) => state.current.index);
const current = usePlayerStore((state) => state.getQueueData().current);
const previous = usePlayerStore((state) => state.queue.previousNode);
const setCurrentIndex = usePlayerStore((state) => state.setCurrentIndex);
const [columnDefs] = useState<ColDef[]>([
{
field: 'index',
headerName: '-',
initialWidth: 50,
rowDrag: true,
suppressSizeToFit: true,
},
{
headerName: '#',
initialWidth: 50,
suppressSizeToFit: true,
valueGetter: 'node.rowIndex + 1',
},
{ field: 'name' },
{
field: 'duration',
initialWidth: 100,
suppressSizeToFit: true,
},
{ field: 'album.id', initialWidth: 100 },
]);
const defaultColumnDefs: ColDef = useMemo(() => {
return {
lockPinned: true,
lockVisible: true,
resizable: true,
};
}, []);
const rowClassRules = useMemo<RowClassRules>(() => {
return {
'current-song': (params) => {
return params.rowIndex === currentPlayerIndex;
},
};
}, [currentPlayerIndex]);
useEffect(() => {
const { api, columnApi } = gridRef.current;
if (api == null || columnApi == null) {
return;
}
const currentNode = api.getRowNode(current?.uniqueId);
const previousNode = api.getRowNode(previous?.uniqueId);
const rowNodes = [currentNode, previousNode];
if (rowNodes) {
api.redrawRows({ rowNodes });
api.ensureNodeVisible(currentNode, 'middle');
}
}, [current, previous]);
const handlePlayByRowClick = (e: any) => {
const playerData = setCurrentIndex(e.rowIndex);
mpvPlayer.setQueue(playerData);
};
return (
<AnimatedPage>
<VirtualGridContainer>
<VirtualGridAutoSizerContainer>
<VirtualTable
ref={gridRef}
rowDragEntireRow
rowDragManaged
rowDragMultiRow
suppressMoveWhenRowDragging
suppressScrollOnNewData
animateRows={false}
columnDefs={columnDefs}
defaultColDef={defaultColumnDefs}
enableCellChangeFlash={false}
getRowId={(data) => {
return data.data.uniqueId;
}}
rowBuffer={30}
rowClassRules={rowClassRules}
rowData={queue}
rowSelection="multiple"
onCellClicked={(e) => console.log('clicked', e)}
onCellContextMenu={(e) => console.log(e)}
onCellDoubleClicked={handlePlayByRowClick}
onDragStarted={(e) => {
console.log('ddrag move', e);
}}
onGridSizeChanged={() => {
console.log('size');
gridRef.current.api.sizeColumnsToFit();
}}
onRowDragEnd={(e) => {
console.log('dragend', e);
}}
/>
</VirtualGridAutoSizerContainer>
</VirtualGridContainer>
</AnimatedPage>
);
};
+6 -4
View File
@@ -1,11 +1,12 @@
/* eslint-disable sort-keys-fix/sort-keys-fix */
import { Routes, Route } from 'react-router-dom';
import { AlbumListRoute } from '@/renderer/features/albums/routes/album-list-route';
import { AlbumListRoute } from '@/renderer/features/albums';
import { LoginRoute } from '@/renderer/features/auth';
import { DashboardRoute } from '@/renderer/features/dashboard';
import { NowPlayingRoute } from '@/renderer/features/now-playing';
import { AuthLayout, DefaultLayout } from '@/renderer/layouts';
import { AuthOutlet } from '@/renderer/router/auth-outlet';
import { PrivateOutlet } from '@/renderer/router/private-outlet';
import { LoginRoute } from '../features/auth';
import { DashboardRoute } from '../features/dashboard';
import { AuthLayout, DefaultLayout } from '../layouts';
import { AppRoute } from './routes';
export const AppRouter = () => {
@@ -22,6 +23,7 @@ export const AppRouter = () => {
>
<Route element={<DefaultLayout />}>
<Route element={<DashboardRoute />} path={AppRoute.HOME} />
<Route element={<NowPlayingRoute />} path={AppRoute.NOW_PLAYING} />
<Route element={<AlbumListRoute />} path={AppRoute.LIBRARY_ALBUMS} />
<Route element={<></>} path={AppRoute.LIBRARY_ARTISTS} />
</Route>
+2
View File
@@ -12,6 +12,7 @@ export enum AppRoute {
LIBRARY_FOLDERS = '/library/folders',
LIBRARY_SONGS = '/library/songs',
LOGIN = '/login',
NOW_PLAYING = '/now-playing',
PLAYING = '/playing',
PLAYLISTS = '/playlists',
PLAYLISTS_DETAIL = '/playlists/:playlistId',
@@ -21,6 +22,7 @@ export enum AppRoute {
type TArgs =
| { path: AppRoute.HOME }
| { path: AppRoute.NOW_PLAYING }
| { path: AppRoute.EXPLORE }
| { path: AppRoute.LOGIN }
| { path: AppRoute.PLAYING }