Redesign sidebar / header and other misc. improvements (#24)

* Remove 1920px max width

* Fix position of list controls menu

* Match size and color of search input

* Adjust library header sizing

* Move app menu to sidebar

* Increase row buffer on play queue list

* Fix query builder styles

* Fix playerbar slider track bg

* Adjust titlebar styles

* Fix invalid modal prop

* Various adjustments to detail pages

* Fix sidebar height calculation

* Fix list null indicators, add filter indicator

* Adjust playqueue styles

* Fix jellyfin releaseYear normalization

* Suppress browser context menu on ag-grid

* Add radius to drawer queue -- normalize layout

* Add modal styles to provider theme

* Fix playlist song list pagination

* Add disc number to albums with more than one disc

* Fix query builder boolean values

* Adjust input placeholder color

* Properly handle rating/favorite from context menu on table

* Conform dropdown menu styles to context menu

* Increase sort type select width

* Fix drawer queue radius

* Change primary color

* Prevent volume wheel from invalid values

* Add icons to query builder dropdowns

* Update notification styles

* Update scrollbar thumb styles

* Remove "add to playlist" on smart playlists

* Fix "add to playlist" from context menu
This commit is contained in:
Jeff 2023-02-07 22:47:23 -08:00 committed by GitHub
parent d2c0d4c11f
commit 9f2e873366
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 1427 additions and 1101 deletions

View file

@ -3,28 +3,28 @@ import {
Button,
getColumnDefs,
GridCarousel,
Text,
TextTitle,
useFixedTableHeader,
VirtualTable,
} from '/@/renderer/components';
import { ColDef, RowDoubleClickedEvent } from '@ag-grid-community/core';
import { ColDef, RowDoubleClickedEvent, RowHeightParams, RowNode } from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Box, Group, Stack } from '@mantine/core';
import { useSetState } from '@mantine/hooks';
import { RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
import { RiDiscFill, RiHeartFill, RiHeartLine, RiMoreFill } from 'react-icons/ri';
import { generatePath, useParams } from 'react-router';
import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query';
import { useSongListStore } from '/@/renderer/store';
import { Link } from 'react-router-dom';
import styled from 'styled-components';
import { AppRoute } from '/@/renderer/router/routes';
import { useContainerQuery } from '/@/renderer/hooks';
import { usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import { PersistedTableColumn, usePlayButtonBehavior } from '/@/renderer/store/settings.store';
import {
useHandleGeneralContextMenu,
useHandleTableContextMenu,
} from '/@/renderer/features/context-menu';
import { Play } from '/@/renderer/types';
import { Play, ServerType, TableColumn } from '/@/renderer/types';
import {
ALBUM_CONTEXT_MENU_ITEMS,
SONG_CONTEXT_MENU_ITEMS,
@ -34,11 +34,14 @@ import { useAlbumList } from '/@/renderer/features/albums/queries/album-list-que
import { AlbumListSort, LibraryItem, QueueSong, SortOrder } from '/@/renderer/api/types';
import { usePlayQueueAdd } from '/@/renderer/features/player';
const isFullWidthRow = (node: RowNode) => {
return node.id?.includes('disc-');
};
const ContentContainer = styled.div`
position: relative;
display: flex;
flex-direction: column;
max-width: 1920px;
padding: 1rem 2rem 5rem;
overflow: hidden;
@ -61,13 +64,82 @@ export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => {
const cq = useContainerQuery();
const handlePlayQueueAdd = usePlayQueueAdd();
const page = useSongListStore();
// TODO: Make this customizable
const columnDefs: ColDef[] = useMemo(() => {
const userRatingColumn =
detailQuery?.data?.serverType !== ServerType.JELLYFIN
? [
{
column: TableColumn.USER_RATING,
width: 0,
},
]
: [];
const columnDefs: ColDef[] = useMemo(
() =>
getColumnDefs(page.table.columns).filter((c) => c.colId !== 'album' && c.colId !== 'artist'),
[page.table.columns],
);
const cols: PersistedTableColumn[] = [
{
column: TableColumn.TRACK_NUMBER,
width: 0,
},
{
column: TableColumn.TITLE_COMBINED,
width: 0,
},
{
column: TableColumn.DURATION,
width: 0,
},
{
column: TableColumn.BIT_RATE,
width: 0,
},
{
column: TableColumn.PLAY_COUNT,
width: 0,
},
{
column: TableColumn.LAST_PLAYED,
width: 0,
},
...userRatingColumn,
{
column: TableColumn.USER_FAVORITE,
width: 0,
},
];
return getColumnDefs(cols).filter((c) => c.colId !== 'album' && c.colId !== 'artist');
}, [detailQuery?.data?.serverType]);
const getRowHeight = useCallback((params: RowHeightParams) => {
if (isFullWidthRow(params.node)) {
return 45;
}
return 60;
}, []);
const songsRowData = useMemo(() => {
if (!detailQuery.data?.songs) {
return [];
}
const uniqueDiscNumbers = new Set(detailQuery.data?.songs.map((s) => s.discNumber));
if (uniqueDiscNumbers.size === 1) {
return detailQuery.data?.songs;
}
const rowData: (QueueSong | { id: string; name: string })[] = [];
for (const discNumber of uniqueDiscNumbers.values()) {
const songsByDiscNumber = detailQuery.data?.songs.filter((s) => s.discNumber === discNumber);
rowData.push({ id: `disc-${discNumber}`, name: `DISC ${discNumber}` });
rowData.push(...songsByDiscNumber);
}
return rowData;
}, [detailQuery.data?.songs]);
const [pagination, setPagination] = useSetState({
artist: 0,
@ -261,9 +333,29 @@ export const AlbumDetailContent = ({ tableRef }: AlbumDetailContentProps) => {
suppressRowDrag
columnDefs={columnDefs}
enableCellChangeFlash={false}
fullWidthCellRenderer={(data: any) => {
if (!data.data) return null;
return (
<Group
align="center"
h="100%"
spacing="sm"
>
<RiDiscFill />
<Text>{data.data.name}</Text>
</Group>
);
}}
getRowHeight={getRowHeight}
getRowId={(data) => data.data.id}
rowData={detailQuery.data?.songs}
rowHeight={60}
isFullWidthRow={(data) => {
return isFullWidthRow(data.rowNode) || false;
}}
isRowSelectable={(data) => {
if (isFullWidthRow(data.data)) return false;
return true;
}}
rowData={songsRowData}
rowSelection="multiple"
onCellContextMenu={handleContextMenu}
onRowDoubleClicked={handleRowDoubleClick}

View file

@ -114,7 +114,7 @@ export const AlbumListContent = ({
);
const albums = api.normalize.albumList(albumsRes, server);
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || undefined);
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || 0);
},
rowCount: undefined,
};
@ -341,7 +341,7 @@ export const AlbumListContent = ({
itemGap={20}
itemSize={150 + page.grid?.size}
itemType={LibraryItem.ALBUM}
loading={!itemCount}
loading={itemCount === undefined || itemCount === null}
minimumBatchSize={40}
route={{
route: AppRoute.LIBRARY_ALBUMS_DETAIL,
@ -366,7 +366,7 @@ export const AlbumListContent = ({
blockLoadDebounceMillis={200}
columnDefs={columnDefs}
getRowId={(data) => data.data.id}
infiniteInitialRowCount={itemCount || 1}
infiniteInitialRowCount={itemCount || 100}
pagination={isPaginationEnabled}
paginationAutoPageSize={isPaginationEnabled}
paginationPageSize={page.table.pagination.itemsPerPage || 100}

View file

@ -1,4 +1,4 @@
import { MutableRefObject, useCallback, MouseEvent, ChangeEvent } from 'react';
import { MutableRefObject, useCallback, MouseEvent, ChangeEvent, useMemo } from 'react';
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';
@ -8,13 +8,13 @@ import {
RiSortAsc,
RiSortDesc,
RiFolder2Line,
RiFilter3Line,
RiMoreFill,
RiAddBoxFill,
RiPlayFill,
RiAddCircleFill,
RiRefreshLine,
RiSettings3Fill,
RiFilterFill,
} from 'react-icons/ri';
import { api } from '/@/renderer/api';
import { queryKeys } from '/@/renderer/api/query-keys';
@ -189,7 +189,7 @@ export const AlbumListHeaderFilters = ({
);
const albums = api.normalize.albumList(albumsRes, server);
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || undefined);
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || 0);
},
rowCount: undefined,
};
@ -371,6 +371,20 @@ export const AlbumListHeaderFilters = ({
}
};
const isFilterApplied = useMemo(() => {
const isNavidromeFilterApplied =
server?.type === ServerType.NAVIDROME &&
page.filter.ndParams &&
Object.values(page.filter.ndParams).some((value) => value !== undefined);
const isJellyfinFilterApplied =
server?.type === ServerType.JELLYFIN &&
page.filter.jfParams &&
Object.values(page.filter.jfParams).some((value) => value !== undefined);
return isNavidromeFilterApplied || isJellyfinFilterApplied;
}, [page.filter.jfParams, page.filter.ndParams, server?.type]);
return (
<Flex justify="space-between">
<Group
@ -447,15 +461,6 @@ export const AlbumListHeaderFilters = ({
</DropdownMenu.Dropdown>
</DropdownMenu>
)}
<Button
compact
fw={600}
size="md"
variant="subtle"
onClick={handleOpenFiltersModal}
>
{cq.isSm ? 'Filters' : <RiFilter3Line size="1.3rem" />}
</Button>
<DropdownMenu position="bottom-start">
<DropdownMenu.Target>
<Button
@ -495,12 +500,26 @@ export const AlbumListHeaderFilters = ({
</DropdownMenu.Dropdown>
</DropdownMenu>
</Group>
<Group>
<DropdownMenu position="bottom-start">
<Group
noWrap
spacing="sm"
>
<Button
compact
size="md"
sx={{ svg: { fill: isFilterApplied ? 'var(--primary-color) !important' : undefined } }}
tooltip={{ label: 'Filters' }}
variant="subtle"
onClick={handleOpenFiltersModal}
>
<RiFilterFill size="1.3rem" />
</Button>
<DropdownMenu position="bottom-end">
<DropdownMenu.Target>
<Button
compact
size="md"
tooltip={{ label: 'Configure' }}
variant="subtle"
>
<RiSettings3Fill size="1.3rem" />

View file

@ -129,7 +129,7 @@ export const AlbumListHeader = ({
);
const albums = api.normalize.albumList(albumsRes, server);
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || undefined);
params.successCallback(albums?.items || [], albumsRes?.totalRecordCount || 0);
},
rowCount: undefined,
};
@ -209,13 +209,11 @@ export const AlbumListHeader = ({
<PageHeader backgroundColor="var(--titlebar-bg)">
<Flex
justify="space-between"
py="1rem"
w="100%"
>
<LibraryHeaderBar>
<Group noWrap>
<LibraryHeaderBar.PlayButton onClick={() => handlePlay(playButtonBehavior)} />
<LibraryHeaderBar.Title>{title || 'Albums'}</LibraryHeaderBar.Title>
</Group>
<LibraryHeaderBar.PlayButton onClick={() => handlePlay(playButtonBehavior)} />
<LibraryHeaderBar.Title>{title || 'Albums'}</LibraryHeaderBar.Title>
<Paper
fw="600"
px="1rem"

View file

@ -4,7 +4,6 @@ import { MultiSelect, NumberInput, SpinnerIcon, Switch, Text } from '/@/renderer
import { AlbumListFilter, useAlbumListStore, useSetAlbumFilters } from '/@/renderer/store';
import debounce from 'lodash/debounce';
import { useGenreList } from '/@/renderer/features/genres';
import { useDebouncedValue } from '@mantine/hooks';
import { AlbumArtistListSort, SortOrder } from '/@/renderer/api/types';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
@ -48,30 +47,30 @@ export const JellyfinAlbumFilters = ({
},
];
const handleMinYearFilter = debounce((e: number | undefined) => {
if (e && (e < 1700 || e > 2300)) return;
const handleMinYearFilter = debounce((e: number | string) => {
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
const updatedFilters = setFilters({
jfParams: {
...filter.jfParams,
minYear: e,
minYear: e === '' ? undefined : (e as number),
},
});
handleFilterChange(updatedFilters);
}, 500);
const handleMaxYearFilter = debounce((e: number | undefined) => {
if (e && (e < 1700 || e > 2300)) return;
const handleMaxYearFilter = debounce((e: number | string) => {
if (typeof e === 'number' && (e < 1700 || e > 2300)) return;
const updatedFilters = setFilters({
jfParams: {
...filter.jfParams,
maxYear: e,
maxYear: e === '' ? undefined : (e as number),
},
});
handleFilterChange(updatedFilters);
}, 500);
const handleGenresFilter = debounce((e: string[] | undefined) => {
const genreFilterString = e?.join(',');
const genreFilterString = e?.length ? e.join(',') : undefined;
const updatedFilters = setFilters({
jfParams: {
...filter.jfParams,
@ -82,18 +81,16 @@ export const JellyfinAlbumFilters = ({
}, 250);
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
const [debouncedSearchTerm] = useDebouncedValue(albumArtistSearchTerm, 200);
const albumArtistListQuery = useAlbumArtistList(
{
limit: 300,
searchTerm: debouncedSearchTerm,
sortBy: AlbumArtistListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
},
{
enabled: debouncedSearchTerm ? debouncedSearchTerm !== '' : false,
cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
);
@ -106,6 +103,17 @@ export const JellyfinAlbumFilters = ({
}));
}, [albumArtistListQuery?.data?.items]);
const handleAlbumArtistFilter = (e: string[] | null) => {
const albumArtistFilterString = e?.length ? e.join(',') : undefined;
const updatedFilters = setFilters({
jfParams: {
...filter.jfParams,
albumArtistIds: albumArtistFilterString,
},
});
handleFilterChange(updatedFilters);
};
return (
<Stack p="0.8rem">
{toggleFilters.map((filter) => (
@ -124,22 +132,22 @@ export const JellyfinAlbumFilters = ({
<Divider my="0.5rem" />
<Group grow>
<NumberInput
defaultValue={filter.jfParams?.minYear}
hideControls={false}
label="From year"
max={2300}
min={1700}
required={!!filter.jfParams?.maxYear}
value={filter.jfParams?.minYear}
onChange={handleMinYearFilter}
onChange={(e) => handleMinYearFilter(e)}
/>
<NumberInput
defaultValue={filter.jfParams?.maxYear}
hideControls={false}
label="To year"
max={2300}
min={1700}
required={!!filter.jfParams?.minYear}
value={filter.jfParams?.maxYear}
onChange={handleMaxYearFilter}
onChange={(e) => handleMaxYearFilter(e)}
/>
</Group>
<Group grow>
@ -158,12 +166,14 @@ export const JellyfinAlbumFilters = ({
clearable
searchable
data={selectableAlbumArtists}
defaultValue={filter.jfParams?.albumArtistIds?.split(',')}
disabled={disableArtistFilter}
label="Artist"
limit={300}
placeholder="Type to search for an artist"
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
searchValue={albumArtistSearchTerm}
onChange={handleAlbumArtistFilter}
onSearchChange={setAlbumArtistSearchTerm}
/>
</Group>

View file

@ -2,7 +2,6 @@ import { ChangeEvent, useMemo, useState } from 'react';
import { Divider, Group, Stack } from '@mantine/core';
import { NumberInput, Switch, Text, Select, SpinnerIcon } from '/@/renderer/components';
import { AlbumListFilter, useAlbumListStore, useSetAlbumFilters } from '/@/renderer/store';
import { useDebouncedValue } from '@mantine/hooks';
import debounce from 'lodash/debounce';
import { useGenreList } from '/@/renderer/features/genres';
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
@ -86,29 +85,28 @@ export const NavidromeAlbumFilters = ({
},
];
const handleYearFilter = debounce((e: number | undefined) => {
const handleYearFilter = debounce((e: number | string) => {
const updatedFilters = setFilters({
ndParams: {
...filter.ndParams,
year: e,
year: e === '' ? undefined : (e as number),
},
});
handleFilterChange(updatedFilters);
}, 500);
const [albumArtistSearchTerm, setAlbumArtistSearchTerm] = useState<string>('');
const [debouncedSearchTerm] = useDebouncedValue(albumArtistSearchTerm, 200);
const albumArtistListQuery = useAlbumArtistList(
{
limit: 300,
searchTerm: debouncedSearchTerm,
// searchTerm: debouncedSearchTerm,
sortBy: AlbumArtistListSort.NAME,
sortOrder: SortOrder.ASC,
startIndex: 0,
},
{
enabled: debouncedSearchTerm ? debouncedSearchTerm !== '' : false,
cacheTime: 1000 * 60 * 2,
staleTime: 1000 * 60 * 1,
},
);
@ -121,6 +119,16 @@ export const NavidromeAlbumFilters = ({
}));
}, [albumArtistListQuery?.data?.items]);
const handleAlbumArtistFilter = (e: string | null) => {
const updatedFilters = setFilters({
ndParams: {
...filter.ndParams,
artist_id: e || undefined,
},
});
handleFilterChange(updatedFilters);
};
return (
<Stack p="0.8rem">
{toggleFilters.map((filter) => (
@ -138,12 +146,12 @@ export const NavidromeAlbumFilters = ({
<Divider my="0.5rem" />
<Group grow>
<NumberInput
defaultValue={filter.ndParams?.year}
hideControls={false}
label="Year"
max={5000}
min={0}
value={filter.ndParams?.year}
onChange={handleYearFilter}
onChange={(e) => handleYearFilter(e)}
/>
<Select
clearable
@ -159,12 +167,14 @@ export const NavidromeAlbumFilters = ({
clearable
searchable
data={selectableAlbumArtists}
defaultValue={filter.ndParams?.artist_id}
disabled={disableArtistFilter}
label="Artist"
limit={300}
placeholder="Type to search for an artist"
rightSection={albumArtistListQuery.isFetching ? <SpinnerIcon /> : undefined}
searchValue={albumArtistSearchTerm}
onChange={handleAlbumArtistFilter}
onSearchChange={setAlbumArtistSearchTerm}
/>
</Group>