mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 10:23:33 +00:00
Migrate to Mantine v8 and Design Changes (#961)
* mantine v8 migration * various design changes and improvements
This commit is contained in:
parent
bea55d48a8
commit
c1330d92b2
473 changed files with 12469 additions and 11607 deletions
|
|
@ -0,0 +1,12 @@
|
|||
.content-container {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.detail-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--theme-spacing-lg);
|
||||
padding: 1rem 2rem 5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
@ -1,20 +1,16 @@
|
|||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import { RowDoubleClickedEvent, RowHeightParams, RowNode } from '@ag-grid-community/core';
|
||||
import { Box, Group, Stack } from '@mantine/core';
|
||||
import { useSetState } from '@mantine/hooks';
|
||||
import { MutableRefObject, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { FaLastfmSquare } from 'react-icons/fa';
|
||||
import { RiHeartFill, RiHeartLine, RiMoreFill, RiSettings2Fill } from 'react-icons/ri';
|
||||
import { SiMusicbrainz } from 'react-icons/si';
|
||||
import { generatePath, useParams } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import styles from './album-detail-content.module.css';
|
||||
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { Button, Popover, Spoiler } from '/@/renderer/components';
|
||||
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel';
|
||||
import { MemoizedSwiperGridCarousel } from '/@/renderer/components/grid-carousel/grid-carousel';
|
||||
import {
|
||||
getColumnDefs,
|
||||
TableConfigDropdown,
|
||||
|
|
@ -47,6 +43,12 @@ import {
|
|||
useTableSettings,
|
||||
} from '/@/renderer/store/settings.store';
|
||||
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Popover } from '/@/shared/components/popover/popover';
|
||||
import { Spoiler } from '/@/shared/components/spoiler/spoiler';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import {
|
||||
AlbumListQuery,
|
||||
AlbumListSort,
|
||||
|
|
@ -60,19 +62,6 @@ const isFullWidthRow = (node: RowNode) => {
|
|||
return node.id?.startsWith('disc-');
|
||||
};
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
`;
|
||||
|
||||
const DetailContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
padding: 1rem 2rem 5rem;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
interface AlbumDetailContentProps {
|
||||
background?: string;
|
||||
tableRef: MutableRefObject<AgGridReactType | null>;
|
||||
|
|
@ -330,72 +319,73 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
|||
const mbzId = detailQuery?.data?.mbzId;
|
||||
|
||||
return (
|
||||
<ContentContainer>
|
||||
<LibraryBackgroundOverlay $backgroundColor={background} />
|
||||
<DetailContainer>
|
||||
<Box component="section">
|
||||
<div
|
||||
className={styles.contentContainer}
|
||||
ref={cq.ref}
|
||||
>
|
||||
<LibraryBackgroundOverlay backgroundColor={background} />
|
||||
<div className={styles.detailContainer}>
|
||||
<section>
|
||||
<Group
|
||||
position="apart"
|
||||
spacing="sm"
|
||||
gap="sm"
|
||||
justify="space-between"
|
||||
>
|
||||
<Group>
|
||||
<PlayButton onClick={() => handlePlay(playButtonBehavior)} />
|
||||
<Button
|
||||
compact
|
||||
loading={
|
||||
createFavoriteMutation.isLoading ||
|
||||
deleteFavoriteMutation.isLoading
|
||||
}
|
||||
onClick={handleFavorite}
|
||||
variant="subtle"
|
||||
>
|
||||
{detailQuery?.data?.userFavorite ? (
|
||||
<RiHeartFill
|
||||
color="red"
|
||||
size={20}
|
||||
/>
|
||||
) : (
|
||||
<RiHeartLine size={20} />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
compact
|
||||
onClick={(e) => {
|
||||
if (!detailQuery?.data) return;
|
||||
handleGeneralContextMenu(e, [detailQuery.data!]);
|
||||
}}
|
||||
variant="subtle"
|
||||
>
|
||||
<RiMoreFill size={20} />
|
||||
</Button>
|
||||
<Group gap="xs">
|
||||
<ActionIcon
|
||||
icon="favorite"
|
||||
iconProps={{
|
||||
fill: detailQuery?.data?.userFavorite
|
||||
? 'primary'
|
||||
: undefined,
|
||||
}}
|
||||
loading={
|
||||
createFavoriteMutation.isLoading ||
|
||||
deleteFavoriteMutation.isLoading
|
||||
}
|
||||
onClick={handleFavorite}
|
||||
size="lg"
|
||||
variant="transparent"
|
||||
/>
|
||||
<ActionIcon
|
||||
icon="ellipsisHorizontal"
|
||||
onClick={(e) => {
|
||||
if (!detailQuery?.data) return;
|
||||
handleGeneralContextMenu(e, [detailQuery.data!]);
|
||||
}}
|
||||
size="lg"
|
||||
variant="transparent"
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<Popover position="bottom-end">
|
||||
<Popover.Target>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
<RiSettings2Fill size={20} />
|
||||
</Button>
|
||||
<ActionIcon
|
||||
icon="settings"
|
||||
onClick={(e) => {
|
||||
if (!detailQuery?.data) return;
|
||||
handleGeneralContextMenu(e, [detailQuery.data!]);
|
||||
}}
|
||||
size="lg"
|
||||
variant="transparent"
|
||||
/>
|
||||
</Popover.Target>
|
||||
<Popover.Dropdown>
|
||||
<TableConfigDropdown type="albumDetail" />
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
</Group>
|
||||
</Box>
|
||||
</section>
|
||||
{showGenres && (
|
||||
<Box component="section">
|
||||
<Group spacing="sm">
|
||||
<section>
|
||||
<Group gap="sm">
|
||||
{detailQuery?.data?.genres?.map((genre) => (
|
||||
<Button
|
||||
compact
|
||||
component={Link}
|
||||
key={`genre-${genre.id}`}
|
||||
radius={0}
|
||||
size="md"
|
||||
radius="md"
|
||||
size="compact-md"
|
||||
to={generatePath(genreRoute, {
|
||||
genreId: genre.id,
|
||||
})}
|
||||
|
|
@ -405,35 +395,38 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
|||
</Button>
|
||||
))}
|
||||
</Group>
|
||||
</Box>
|
||||
</section>
|
||||
)}
|
||||
{externalLinks && (lastFM || musicBrainz) ? (
|
||||
<Box component="section">
|
||||
<Group spacing="sm">
|
||||
{lastFM && (
|
||||
<Button
|
||||
compact
|
||||
component="a"
|
||||
href={`https://www.last.fm/music/${encodeURIComponent(
|
||||
detailQuery?.data?.albumArtist || '',
|
||||
)}/${encodeURIComponent(detailQuery.data?.name || '')}`}
|
||||
radius="md"
|
||||
rel="noopener noreferrer"
|
||||
size="md"
|
||||
target="_blank"
|
||||
tooltip={{
|
||||
label: t('action.openIn.lastfm'),
|
||||
}}
|
||||
variant="subtle"
|
||||
>
|
||||
<FaLastfmSquare size={25} />
|
||||
</Button>
|
||||
)}
|
||||
{musicBrainz && mbzId ? (
|
||||
<Button
|
||||
compact
|
||||
<section>
|
||||
<Group gap="sm">
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href={`https://www.last.fm/music/${encodeURIComponent(
|
||||
detailQuery?.data?.albumArtist || '',
|
||||
)}/${encodeURIComponent(detailQuery.data?.name || '')}`}
|
||||
icon="brandLastfm"
|
||||
iconProps={{
|
||||
fill: 'default',
|
||||
size: 'xl',
|
||||
}}
|
||||
radius="md"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
tooltip={{
|
||||
label: t('action.openIn.lastfm'),
|
||||
}}
|
||||
variant="subtle"
|
||||
/>
|
||||
{mbzId ? (
|
||||
<ActionIcon
|
||||
component="a"
|
||||
href={`https://musicbrainz.org/release/${mbzId}`}
|
||||
icon="brandMusicBrainz"
|
||||
iconProps={{
|
||||
fill: 'default',
|
||||
size: 'xl',
|
||||
}}
|
||||
radius="md"
|
||||
rel="noopener noreferrer"
|
||||
size="md"
|
||||
|
|
@ -442,19 +435,17 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
|||
label: t('action.openIn.musicbrainz'),
|
||||
}}
|
||||
variant="subtle"
|
||||
>
|
||||
<SiMusicbrainz size={25} />
|
||||
</Button>
|
||||
/>
|
||||
) : null}
|
||||
</Group>
|
||||
</Box>
|
||||
</section>
|
||||
) : null}
|
||||
{comment && (
|
||||
<Box component="section">
|
||||
<section>
|
||||
<Spoiler maxHeight={75}>{replaceURLWithHTMLLinks(comment)}</Spoiler>
|
||||
</Box>
|
||||
</section>
|
||||
)}
|
||||
<Box style={{ minHeight: '300px' }}>
|
||||
<div style={{ minHeight: '300px' }}>
|
||||
<VirtualTable
|
||||
autoFitColumns={tableConfig.autoFit}
|
||||
autoHeight
|
||||
|
|
@ -491,11 +482,11 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
|||
suppressLoadingOverlay
|
||||
suppressRowDrag
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
<Stack
|
||||
gap="lg"
|
||||
mt="3rem"
|
||||
ref={cq.ref}
|
||||
spacing="lg"
|
||||
>
|
||||
{cq.height || cq.width ? (
|
||||
<>
|
||||
|
|
@ -547,7 +538,7 @@ export const AlbumDetailContent = ({ background, tableRef }: AlbumDetailContentP
|
|||
</>
|
||||
) : null}
|
||||
</Stack>
|
||||
</DetailContainer>
|
||||
</ContentContainer>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
import { Group, Stack } from '@mantine/core';
|
||||
import { forwardRef, Fragment, Ref, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { generatePath, useParams } from 'react-router';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { Rating, Text } from '/@/renderer/components';
|
||||
import { useAlbumDetail } from '/@/renderer/features/albums/queries/album-detail-query';
|
||||
import { LibraryHeader, useSetRating } from '/@/renderer/features/shared';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
|
|
@ -14,6 +12,10 @@ import { queryClient } from '/@/renderer/lib/react-query';
|
|||
import { AppRoute } from '/@/renderer/router/routes';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { formatDateAbsoluteUTC, formatDurationString } from '/@/renderer/utils';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Rating } from '/@/shared/components/rating/rating';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { AlbumDetailResponse, LibraryItem, ServerType } from '/@/shared/types/domain-types';
|
||||
|
||||
interface AlbumDetailHeaderProps {
|
||||
|
|
@ -129,17 +131,17 @@ export const AlbumDetailHeader = forwardRef(
|
|||
title={detailQuery?.data?.name || ''}
|
||||
{...background}
|
||||
>
|
||||
<Stack spacing="sm">
|
||||
<Group spacing="sm">
|
||||
<Stack gap="sm">
|
||||
<Group gap="sm">
|
||||
{metadataItems.map((item, index) => (
|
||||
<Fragment key={`item-${item.id}-${index}`}>
|
||||
{index > 0 && <Text $noSelect>•</Text>}
|
||||
{index > 0 && <Text isNoSelect>•</Text>}
|
||||
<Text>{item.value}</Text>
|
||||
</Fragment>
|
||||
))}
|
||||
{showRating && (
|
||||
<>
|
||||
<Text $noSelect>•</Text>
|
||||
<Text isNoSelect>•</Text>
|
||||
<Rating
|
||||
onChange={handleUpdateRating}
|
||||
readOnly={
|
||||
|
|
@ -152,9 +154,9 @@ export const AlbumDetailHeader = forwardRef(
|
|||
)}
|
||||
</Group>
|
||||
<Group
|
||||
gap="md"
|
||||
mah="4rem"
|
||||
spacing="md"
|
||||
sx={{
|
||||
style={{
|
||||
overflow: 'hidden',
|
||||
WebkitBoxOrient: 'vertical',
|
||||
WebkitLineClamp: 2,
|
||||
|
|
@ -162,9 +164,9 @@ export const AlbumDetailHeader = forwardRef(
|
|||
>
|
||||
{detailQuery?.data?.albumArtists.map((artist) => (
|
||||
<Text
|
||||
$link
|
||||
component={Link}
|
||||
fw={600}
|
||||
isLink
|
||||
key={`artist-${artist.id}`}
|
||||
to={generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
||||
albumArtistId: artist.id,
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/li
|
|||
|
||||
import { lazy, MutableRefObject, Suspense } from 'react';
|
||||
|
||||
import { Spinner } from '/@/renderer/components';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
import { useListStoreByKey } from '/@/renderer/store';
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
import { ListDisplayType } from '/@/shared/types/types';
|
||||
|
||||
const AlbumListGridView = lazy(() =>
|
||||
|
|
@ -32,7 +32,7 @@ export const AlbumListContent = ({ gridRef, itemCount, tableRef }: AlbumListCont
|
|||
|
||||
return (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
{display === ListDisplayType.CARD || display === ListDisplayType.POSTER ? (
|
||||
{display === ListDisplayType.CARD || display === ListDisplayType.GRID ? (
|
||||
<AlbumListGridView
|
||||
gridRef={gridRef}
|
||||
itemCount={itemCount}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { ListOnScrollProps } from 'react-window';
|
|||
|
||||
import { controller } from '/@/renderer/api/controller';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { ALBUM_CARD_ROWS } from '/@/renderer/components';
|
||||
import { ALBUM_CARD_ROWS } from '/@/renderer/components/card/card-rows';
|
||||
import {
|
||||
VirtualGridAutoSizerContainer,
|
||||
VirtualInfiniteGrid,
|
||||
|
|
|
|||
|
|
@ -1,24 +1,13 @@
|
|||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import { Divider, Flex, Group, Stack } from '@mantine/core';
|
||||
import { openModal } from '@mantine/modals';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { ChangeEvent, MouseEvent, MutableRefObject, useCallback, useMemo } from 'react';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { MouseEvent, MutableRefObject, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
RiAddBoxFill,
|
||||
RiAddCircleFill,
|
||||
RiFilterFill,
|
||||
RiFolder2Fill,
|
||||
RiMoreFill,
|
||||
RiPlayFill,
|
||||
RiRefreshLine,
|
||||
RiSettings3Fill,
|
||||
} from 'react-icons/ri';
|
||||
|
||||
import i18n from '/@/i18n/i18n';
|
||||
import { queryKeys } from '/@/renderer/api/query-keys';
|
||||
import { Button, DropdownMenu, MultiSelect, Slider, Switch, Text } from '/@/renderer/components';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/virtual-table';
|
||||
import { useListContext } from '/@/renderer/context/list-context';
|
||||
|
|
@ -26,6 +15,11 @@ import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jel
|
|||
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
|
||||
import { SubsonicAlbumFilters } from '/@/renderer/features/albums/components/subsonic-album-filters';
|
||||
import { OrderToggleButton, useMusicFolders } from '/@/renderer/features/shared';
|
||||
import { FilterButton } from '/@/renderer/features/shared/components/filter-button';
|
||||
import { FolderButton } from '/@/renderer/features/shared/components/folder-button';
|
||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||
import { MoreButton } from '/@/renderer/features/shared/components/more-button';
|
||||
import { RefreshButton } from '/@/renderer/features/shared/components/refresh-button';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { useListFilterRefresh } from '/@/renderer/hooks/use-list-filter-refresh';
|
||||
import {
|
||||
|
|
@ -34,6 +28,12 @@ import {
|
|||
useListStoreActions,
|
||||
useListStoreByKey,
|
||||
} from '/@/renderer/store';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import {
|
||||
AlbumListQuery,
|
||||
AlbumListSort,
|
||||
|
|
@ -228,7 +228,7 @@ export const AlbumListHeaderFilters = ({
|
|||
?.name) ||
|
||||
'Unknown';
|
||||
|
||||
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.POSTER;
|
||||
const isGrid = display === ListDisplayType.CARD || display === ListDisplayType.GRID;
|
||||
|
||||
const onFilterChange = useCallback(
|
||||
(filter: AlbumListFilter) => {
|
||||
|
|
@ -355,19 +355,20 @@ export const AlbumListHeaderFilters = ({
|
|||
}
|
||||
};
|
||||
|
||||
const debouncedHandleItemSize = debounce(handleItemSize, 20);
|
||||
|
||||
const handleItemGap = (e: number) => {
|
||||
setGrid({ data: { itemGap: e }, key: pageKey });
|
||||
};
|
||||
|
||||
const handleSetViewType = useCallback(
|
||||
(e: MouseEvent<HTMLButtonElement>) => {
|
||||
if (!e.currentTarget?.value) return;
|
||||
setDisplayType({ data: e.currentTarget.value as ListDisplayType, key: pageKey });
|
||||
(displayType: ListDisplayType) => {
|
||||
setDisplayType({ data: displayType, key: pageKey });
|
||||
},
|
||||
[pageKey, setDisplayType],
|
||||
);
|
||||
|
||||
const handleTableColumns = (values: TableColumn[]) => {
|
||||
const handleTableColumns = (values: string[]) => {
|
||||
const existingColumns = table.columns;
|
||||
|
||||
if (values.length === 0) {
|
||||
|
|
@ -379,7 +380,7 @@ export const AlbumListHeaderFilters = ({
|
|||
|
||||
// If adding a column
|
||||
if (values.length > existingColumns.length) {
|
||||
const newColumn = { column: values[values.length - 1], width: 100 };
|
||||
const newColumn = { column: values[values.length - 1] as TableColumn, width: 100 };
|
||||
|
||||
setTable({ data: { columns: [...existingColumns, newColumn] }, key: pageKey });
|
||||
} else {
|
||||
|
|
@ -393,10 +394,10 @@ export const AlbumListHeaderFilters = ({
|
|||
return tableRef.current?.api.sizeColumnsToFit();
|
||||
};
|
||||
|
||||
const handleAutoFitColumns = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setTable({ data: { autoFit: e.currentTarget.checked }, key: pageKey });
|
||||
const handleAutoFitColumns = (autoFitColumns: boolean) => {
|
||||
setTable({ data: { autoFit: autoFitColumns }, key: pageKey });
|
||||
|
||||
if (e.currentTarget.checked) {
|
||||
if (autoFitColumns) {
|
||||
tableRef.current?.api.sizeColumnsToFit();
|
||||
}
|
||||
};
|
||||
|
|
@ -439,25 +440,18 @@ export const AlbumListHeaderFilters = ({
|
|||
return (
|
||||
<Flex justify="space-between">
|
||||
<Group
|
||||
gap="sm"
|
||||
ref={cq.ref}
|
||||
spacing="sm"
|
||||
w="100%"
|
||||
>
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
fw={600}
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
{sortByLabel}
|
||||
</Button>
|
||||
<Button variant="subtle">{sortByLabel}</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{FILTERS[server?.type as keyof typeof FILTERS].map((f) => (
|
||||
<DropdownMenu.Item
|
||||
$isActive={f.value === filter.sortBy}
|
||||
isSelected={f.value === filter.sortBy}
|
||||
key={`filter-${f.name}`}
|
||||
onClick={handleSetSortBy}
|
||||
value={f.value}
|
||||
|
|
@ -477,26 +471,12 @@ export const AlbumListHeaderFilters = ({
|
|||
<Divider orientation="vertical" />
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
fw={600}
|
||||
size="md"
|
||||
sx={{
|
||||
svg: {
|
||||
fill: isFolderFilterApplied
|
||||
? 'var(--primary-color) !important'
|
||||
: undefined,
|
||||
},
|
||||
}}
|
||||
variant="subtle"
|
||||
>
|
||||
<RiFolder2Fill size="1.3rem" />
|
||||
</Button>
|
||||
<FolderButton isActive={!!isFolderFilterApplied} />
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
{musicFoldersQuery.data?.items.map((folder) => (
|
||||
<DropdownMenu.Item
|
||||
$isActive={filter.musicFolderId === folder.id}
|
||||
isSelected={filter.musicFolderId === folder.id}
|
||||
key={`musicFolder-${folder.id}`}
|
||||
onClick={handleSetMusicFolder}
|
||||
value={folder.id}
|
||||
|
|
@ -508,66 +488,37 @@ export const AlbumListHeaderFilters = ({
|
|||
</DropdownMenu>
|
||||
</>
|
||||
)}
|
||||
<Divider orientation="vertical" />
|
||||
<Button
|
||||
compact
|
||||
<FilterButton
|
||||
isActive={!!isFilterApplied}
|
||||
onClick={handleOpenFiltersModal}
|
||||
size="md"
|
||||
sx={{
|
||||
svg: {
|
||||
fill: isFilterApplied ? 'var(--primary-color) !important' : undefined,
|
||||
},
|
||||
}}
|
||||
tooltip={{
|
||||
label: t('common.filters', { count: 2, postProcess: 'sentenceCase' }),
|
||||
}}
|
||||
variant="subtle"
|
||||
>
|
||||
<RiFilterFill size="1.3rem" />
|
||||
</Button>
|
||||
<Divider orientation="vertical" />
|
||||
<Button
|
||||
compact
|
||||
onClick={handleRefresh}
|
||||
size="md"
|
||||
tooltip={{ label: t('common.refresh', { postProcess: 'sentenceCase' }) }}
|
||||
variant="subtle"
|
||||
>
|
||||
<RiRefreshLine size="1.3rem" />
|
||||
</Button>
|
||||
<Divider orientation="vertical" />
|
||||
/>
|
||||
<RefreshButton onClick={handleRefresh} />
|
||||
<DropdownMenu position="bottom-start">
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
variant="subtle"
|
||||
>
|
||||
<RiMoreFill size={15} />
|
||||
</Button>
|
||||
<MoreButton />
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiPlayFill />}
|
||||
leftSection={<Icon icon="mediaPlay" />}
|
||||
onClick={() => handlePlay?.({ playType: Play.NOW })}
|
||||
>
|
||||
{t('player.play', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiAddBoxFill />}
|
||||
leftSection={<Icon icon="mediaPlayLast" />}
|
||||
onClick={() => handlePlay?.({ playType: Play.LAST })}
|
||||
>
|
||||
{t('player.addLast', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
icon={<RiAddCircleFill />}
|
||||
leftSection={<Icon icon="mediaPlayNext" />}
|
||||
onClick={() => handlePlay?.({ playType: Play.NEXT })}
|
||||
>
|
||||
{t('player.addNext', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Item
|
||||
icon={<RiRefreshLine />}
|
||||
leftSection={<Icon icon="refresh" />}
|
||||
onClick={handleRefresh}
|
||||
>
|
||||
{t('common.refresh', { postProcess: 'sentenceCase' })}
|
||||
|
|
@ -576,118 +527,23 @@ export const AlbumListHeaderFilters = ({
|
|||
</DropdownMenu>
|
||||
</Group>
|
||||
<Group
|
||||
noWrap
|
||||
spacing="sm"
|
||||
gap="sm"
|
||||
wrap="nowrap"
|
||||
>
|
||||
<DropdownMenu
|
||||
position="bottom-end"
|
||||
width={425}
|
||||
>
|
||||
<DropdownMenu.Target>
|
||||
<Button
|
||||
compact
|
||||
size="md"
|
||||
tooltip={{
|
||||
label: t('common.configure', { postProcess: 'sentenceCase' }),
|
||||
}}
|
||||
variant="subtle"
|
||||
>
|
||||
<RiSettings3Fill size="1.3rem" />
|
||||
</Button>
|
||||
</DropdownMenu.Target>
|
||||
<DropdownMenu.Dropdown>
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.general.displayType', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.CARD}
|
||||
onClick={handleSetViewType}
|
||||
value={ListDisplayType.CARD}
|
||||
>
|
||||
{t('table.config.view.card', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.POSTER}
|
||||
onClick={handleSetViewType}
|
||||
value={ListDisplayType.POSTER}
|
||||
>
|
||||
{t('table.config.view.poster', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.TABLE}
|
||||
onClick={handleSetViewType}
|
||||
value={ListDisplayType.TABLE}
|
||||
>
|
||||
{t('table.config.view.table', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Item>
|
||||
{/* <DropdownMenu.Item
|
||||
$isActive={display === ListDisplayType.TABLE_PAGINATED}
|
||||
value={ListDisplayType.TABLE_PAGINATED}
|
||||
onClick={handleSetViewType}
|
||||
>
|
||||
Table (paginated)
|
||||
</DropdownMenu.Item> */}
|
||||
<DropdownMenu.Divider />
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.general.itemSize', { postProcess: 'sentenceCase' })}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
<Slider
|
||||
defaultValue={isGrid ? grid?.itemSize || 0 : table.rowHeight}
|
||||
max={isGrid ? 300 : 100}
|
||||
min={isGrid ? 100 : 25}
|
||||
onChangeEnd={handleItemSize}
|
||||
/>
|
||||
</DropdownMenu.Item>
|
||||
{isGrid && (
|
||||
<>
|
||||
<DropdownMenu.Label>
|
||||
{t('table.config.general.itemGap', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
</DropdownMenu.Label>
|
||||
<DropdownMenu.Item closeMenuOnClick={false}>
|
||||
<Slider
|
||||
defaultValue={grid?.itemGap || 0}
|
||||
max={30}
|
||||
min={0}
|
||||
onChangeEnd={handleItemGap}
|
||||
/>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
{(display === ListDisplayType.TABLE ||
|
||||
display === ListDisplayType.TABLE_PAGINATED) && (
|
||||
<>
|
||||
<DropdownMenu.Label>Table Columns</DropdownMenu.Label>
|
||||
<DropdownMenu.Item
|
||||
closeMenuOnClick={false}
|
||||
component="div"
|
||||
sx={{ cursor: 'default' }}
|
||||
>
|
||||
<Stack>
|
||||
<MultiSelect
|
||||
clearable
|
||||
data={ALBUM_TABLE_COLUMNS}
|
||||
defaultValue={table?.columns.map(
|
||||
(column) => column.column,
|
||||
)}
|
||||
onChange={handleTableColumns}
|
||||
width={300}
|
||||
/>
|
||||
<Group position="apart">
|
||||
<Text>Auto Fit Columns</Text>
|
||||
<Switch
|
||||
defaultChecked={table.autoFit}
|
||||
onChange={handleAutoFitColumns}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenu.Dropdown>
|
||||
</DropdownMenu>
|
||||
<ListConfigMenu
|
||||
autoFitColumns={table.autoFit}
|
||||
disabledViewTypes={[ListDisplayType.LIST]}
|
||||
displayType={display}
|
||||
itemGap={grid?.itemGap || 0}
|
||||
itemSize={isGrid ? grid?.itemSize || 0 : table.rowHeight}
|
||||
onChangeAutoFitColumns={handleAutoFitColumns}
|
||||
onChangeDisplayType={handleSetViewType}
|
||||
onChangeItemGap={handleItemGap}
|
||||
onChangeItemSize={debouncedHandleItemSize}
|
||||
onChangeTableColumns={handleTableColumns}
|
||||
tableColumns={table?.columns.map((column) => column.column)}
|
||||
tableColumnsData={ALBUM_TABLE_COLUMNS}
|
||||
/>
|
||||
</Group>
|
||||
</Flex>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,18 +1,21 @@
|
|||
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
|
||||
|
||||
import { Flex, Group, Stack } from '@mantine/core';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { type ChangeEvent, type MutableRefObject, useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { PageHeader, SearchInput } from '/@/renderer/components';
|
||||
import { PageHeader } from '/@/renderer/components/page-header/page-header';
|
||||
import { VirtualInfiniteGridRef } from '/@/renderer/components/virtual-grid';
|
||||
import { AlbumListHeaderFilters } from '/@/renderer/features/albums/components/album-list-header-filters';
|
||||
import { FilterBar, LibraryHeaderBar } from '/@/renderer/features/shared';
|
||||
import { SearchInput } from '/@/renderer/features/shared/components/search-input';
|
||||
import { useContainerQuery } from '/@/renderer/hooks';
|
||||
import { useDisplayRefresh } from '/@/renderer/hooks/use-display-refresh';
|
||||
import { AlbumListFilter, useCurrentServer, usePlayButtonBehavior } from '/@/renderer/store';
|
||||
import { titleCase } from '/@/renderer/utils';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { AlbumListQuery, LibraryItem } from '/@/shared/types/domain-types';
|
||||
|
||||
interface AlbumListHeaderProps {
|
||||
|
|
@ -59,10 +62,10 @@ export const AlbumListHeader = ({
|
|||
|
||||
return (
|
||||
<Stack
|
||||
gap={0}
|
||||
ref={cq.ref}
|
||||
spacing={0}
|
||||
>
|
||||
<PageHeader backgroundColor="var(--titlebar-bg)">
|
||||
<PageHeader backgroundColor="var(--theme-colors-background)">
|
||||
<Flex
|
||||
justify="space-between"
|
||||
w="100%"
|
||||
|
|
@ -85,7 +88,6 @@ export const AlbumListHeader = ({
|
|||
<SearchInput
|
||||
defaultValue={filter.searchTerm}
|
||||
onChange={handleSearch}
|
||||
openedWidth={cq.isMd ? 250 : cq.isSm ? 200 : 150}
|
||||
/>
|
||||
</Group>
|
||||
</Flex>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
import { Divider, Group, Stack } from '@mantine/core';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { ChangeEvent, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { NumberInput, SpinnerIcon, Switch, Text } from '/@/renderer/components';
|
||||
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
|
||||
import { useGenreList } from '/@/renderer/features/genres';
|
||||
import { useTagList } from '/@/renderer/features/tag/queries/use-tag-list';
|
||||
import { AlbumListFilter, useListFilterByKey, useListStoreActions } from '/@/renderer/store';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import {
|
||||
AlbumArtistListSort,
|
||||
AlbumListQuery,
|
||||
|
|
@ -186,8 +191,8 @@ export const JellyfinAlbumFilters = ({
|
|||
<Stack p="0.8rem">
|
||||
{toggleFilters.map((filter) => (
|
||||
<Group
|
||||
justify="space-between"
|
||||
key={`nd-filter-${filter.label}`}
|
||||
position="apart"
|
||||
>
|
||||
<Text>{filter.label}</Text>
|
||||
<Switch
|
||||
|
|
|
|||
|
|
@ -1,14 +1,19 @@
|
|||
import { Divider, Group, Stack } from '@mantine/core';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { ChangeEvent, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { NumberInput, SpinnerIcon, Switch, Text } from '/@/renderer/components';
|
||||
import { SelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
|
||||
import { useAlbumArtistList } from '/@/renderer/features/artists/queries/album-artist-list-query';
|
||||
import { useGenreList } from '/@/renderer/features/genres';
|
||||
import { useTagList } from '/@/renderer/features/tag/queries/use-tag-list';
|
||||
import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||
import { SpinnerIcon } from '/@/shared/components/spinner/spinner';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import {
|
||||
AlbumArtistListSort,
|
||||
AlbumListQuery,
|
||||
|
|
@ -233,8 +238,8 @@ export const NavidromeAlbumFilters = ({
|
|||
<Stack p="0.8rem">
|
||||
{toggleFilters.map((filter) => (
|
||||
<Group
|
||||
justify="space-between"
|
||||
key={`nd-filter-${filter.label}`}
|
||||
position="apart"
|
||||
>
|
||||
<Text>{filter.label}</Text>
|
||||
<Switch
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
import { Divider, Group, Stack } from '@mantine/core';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { ChangeEvent, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { NumberInput, Select, Switch, Text } from '/@/renderer/components';
|
||||
import { useGenreList } from '/@/renderer/features/genres';
|
||||
import { AlbumListFilter, useListStoreActions, useListStoreByKey } from '/@/renderer/store';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { NumberInput } from '/@/shared/components/number-input/number-input';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { Switch } from '/@/shared/components/switch/switch';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import {
|
||||
AlbumListQuery,
|
||||
GenreListSort,
|
||||
|
|
@ -100,8 +105,8 @@ export const SubsonicAlbumFilters = ({
|
|||
<Stack p="0.8rem">
|
||||
{toggleFilters.map((filter) => (
|
||||
<Group
|
||||
justify="space-between"
|
||||
key={`nd-filter-${filter.label}`}
|
||||
position="apart"
|
||||
>
|
||||
<Text>{filter.label}</Text>
|
||||
<Switch
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue