lint all files

This commit is contained in:
jeffvli
2025-11-29 17:38:19 -08:00
parent 96e9d54f4e
commit 108ba53be2
30 changed files with 202 additions and 545 deletions
+2 -2
View File
@@ -27,8 +27,6 @@ export default tseslint.config(
rules: { rules: {
...eslintPluginReactHooks.configs.recommended.rules, ...eslintPluginReactHooks.configs.recommended.rules,
...eslintPluginReactRefresh.configs.vite.rules, ...eslintPluginReactRefresh.configs.vite.rules,
'react-hooks/set-state-in-effect': 'off',
'react-hooks/refs': 'off',
'@typescript-eslint/explicit-function-return-type': 'off', '@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-duplicate-enum-values': 'off', '@typescript-eslint/no-duplicate-enum-values': 'off',
'@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/no-explicit-any': 'off',
@@ -45,6 +43,8 @@ export default tseslint.config(
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'no-use-before-define': 'off', 'no-use-before-define': 'off',
quotes: ['error', 'single'], quotes: ['error', 'single'],
'react-hooks/refs': 'off',
'react-hooks/set-state-in-effect': 'off',
'react-refresh/only-export-components': 'off', 'react-refresh/only-export-components': 'off',
'react/display-name': 'off', 'react/display-name': 'off',
semi: ['error', 'always'], semi: ['error', 'always'],
+5 -5
View File
@@ -3,13 +3,13 @@ module.exports = {
'postcss-preset-mantine': {}, 'postcss-preset-mantine': {},
'postcss-simple-vars': { 'postcss-simple-vars': {
variables: { variables: {
'mantine-breakpoint-xs': '36em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-xl': '88em',
'mantine-breakpoint-2xl': '120em', 'mantine-breakpoint-2xl': '120em',
'mantine-breakpoint-3xl': '160em', 'mantine-breakpoint-3xl': '160em',
'mantine-breakpoint-lg': '75em',
'mantine-breakpoint-md': '62em',
'mantine-breakpoint-sm': '48em',
'mantine-breakpoint-xl': '88em',
'mantine-breakpoint-xs': '36em',
}, },
}, },
}, },
@@ -1,18 +0,0 @@
.item-detail-container {
display: flex;
flex-direction: column !important;
width: 100%;
height: 100%;
padding: 0 var(--theme-spacing-md);
}
.list-expanded-container {
width: 100%;
height: 100%;
}
.item-list {
width: 100%;
height: 100%;
padding-bottom: var(--theme-spacing-sm);
}
@@ -1,288 +0,0 @@
import throttle from 'lodash/throttle';
import { AnimatePresence, motion, Variants } from 'motion/react';
import { useOverlayScrollbars } from 'overlayscrollbars-react';
import {
MouseEvent,
Ref,
UIEvent,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import { List, ListImperativeAPI, RowComponentProps, useListRef } from 'react-window-v2';
import styles from './item-detail-list.module.css';
import { ItemDetail } from '/@/renderer/components/item-detail/item-detail';
import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item';
import {
useItemListState,
useItemListStateSubscription,
} from '/@/renderer/components/item-list/helpers/item-list-state';
import { useElementSize } from '/@/shared/hooks/use-element-size';
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
import { LibraryItem } from '/@/shared/types/domain-types';
export interface ItemDetailListProps {
data: unknown[];
enableExpansion?: boolean;
enableSelection?: boolean;
initialTopMostItemIndex?:
| number
| {
align: 'center' | 'end' | 'start';
behavior: 'auto' | 'smooth';
index: number;
offset?: number;
};
itemType: LibraryItem;
onEndReached?: (index: number) => void;
onItemClick?: (item: unknown, index: number) => void;
onItemContextMenu?: (item: unknown, index: number) => void;
onItemDoubleClick?: (item: unknown, index: number) => void;
onRangeChanged?: (range: { endIndex: number; startIndex: number }) => void;
onScroll?: (e: UIEvent<HTMLDivElement>) => void;
onScrollEnd?: () => void;
onStartReached?: (index: number) => void;
ref: Ref<ListImperativeAPI>;
totalItemCount?: number;
}
const expandedAnimationVariants: Variants = {
hidden: {
height: 0,
minHeight: 0,
},
show: {
minHeight: '300px',
transition: {
duration: 0.3,
ease: 'easeInOut',
},
},
};
export const ItemDetailList = ({
data,
enableExpansion = false,
enableSelection = false,
initialTopMostItemIndex = 0,
itemType,
onEndReached,
onItemClick,
onItemContextMenu,
onItemDoubleClick,
onRangeChanged,
onScroll,
onScrollEnd,
onStartReached,
totalItemCount = 0,
}: ItemDetailListProps) => {
const itemDetailRef = useListRef(null);
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
const { ref: containerRef, width: containerWidth } = useElementSize();
const mergedContainerRef = useMergedRef(containerRef, scrollContainerRef);
const internalState = useItemListState();
const [initialize] = useOverlayScrollbars({
defer: true,
events: {
initialized(osInstance) {
const { viewport } = osInstance.elements();
viewport.style.overflowX = `var(--os-viewport-overflow-x)`;
viewport.style.overflowY = `var(--os-viewport-overflow-y)`;
},
},
options: {
overflow: { x: 'hidden', y: 'scroll' },
paddingAbsolute: true,
scrollbars: {
autoHide: 'leave',
autoHideDelay: 500,
pointers: ['mouse', 'pen', 'touch'],
theme: 'feishin-os-scrollbar',
visibility: 'visible',
},
},
});
useEffect(() => {
const { current: root } = scrollContainerRef;
if (root) {
initialize({
elements: { viewport: root.firstElementChild as HTMLElement },
target: root,
});
}
}, [itemDetailRef, initialize]);
const hasExpanded = useItemListStateSubscription(internalState, (state) =>
state ? state.expanded.size > 0 : false,
);
const handleExpand = useCallback(
(_e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => {
if (item && typeof item === 'object' && 'id' in item && 'serverId' in item) {
internalState.toggleExpanded({
_itemType: itemType,
_serverId: item.serverId as string,
id: item.id as string,
});
}
},
[internalState],
);
const handleScroll = useCallback(
(e: UIEvent<HTMLDivElement>) => {
onScroll?.(e);
},
[onScroll],
);
const [tableMeta, setTableMeta] = useState<null | {
itemHeight: number;
}>(null);
// Throttled function to update table meta
const throttledSetTableMeta = useMemo(() => {
return throttle((width: number) => {
const isSm = width >= 600;
const isMd = width >= 768;
const isLg = width >= 1200;
const isXl = width >= 1500;
const is2xl = width >= 1920;
const is3xl = width >= 2560;
let itemHeight = 160;
if (is3xl) {
itemHeight = 160;
} else if (is2xl) {
itemHeight = 160;
} else if (isXl) {
itemHeight = 160;
} else if (isLg) {
itemHeight = 160;
} else if (isMd) {
itemHeight = 160;
} else if (isSm) {
itemHeight = 160;
} else {
itemHeight = 160;
}
if (itemHeight === 0) {
return;
}
setTableMeta({
itemHeight,
});
}, 200);
}, []);
useLayoutEffect(() => {
throttledSetTableMeta(containerWidth);
}, [containerWidth, data.length, itemType, throttledSetTableMeta]);
const handleOnRowsRendered = useCallback(
(visibleRows: { startIndex: number; stopIndex: number }) => {
onRangeChanged?.({
endIndex: visibleRows.stopIndex * (tableMeta?.columnCount || 0),
startIndex: visibleRows.startIndex * (tableMeta?.columnCount || 0),
});
if (onStartReached || onEndReached) {
const totalRows = Math.ceil(totalItemCount / (tableMeta?.columnCount || 0));
const startRow = visibleRows.startIndex;
const endRow = visibleRows.stopIndex;
if (startRow === 0) {
onStartReached?.(startRow);
}
if (endRow >= totalRows) {
onEndReached?.(endRow);
}
}
},
[onEndReached, onRangeChanged, onStartReached, totalItemCount, tableMeta?.columnCount],
);
return (
<motion.div
animate={{
height: '100%',
opacity: 1,
transition: {
duration: 1,
ease: 'backInOut',
},
}}
className={styles.itemDetailContainer}
data-overlayscrollbars-initialize=""
initial={{ opacity: 0 }}
ref={mergedContainerRef}
>
<List
listRef={itemDetailRef}
onRowsRendered={handleOnRowsRendered}
onScroll={handleScroll}
rowComponent={RowComponent}
rowCount={data.length}
rowHeight={tableMeta?.itemHeight || 0}
rowProps={{
data,
handleExpand,
itemHeight: tableMeta?.itemHeight || 0,
itemType,
}}
/>
<AnimatePresence>
{hasExpanded && (
<motion.div
animate="show"
className={styles.listExpandedContainer}
exit="hidden"
initial="hidden"
style={{ height: '500px' }}
variants={expandedAnimationVariants}
>
<ExpandedListItem internalState={internalState} itemType={itemType} />
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
};
function RowComponent({
data,
handleExpand,
index,
itemHeight,
itemType,
style,
}: RowComponentProps<{
data: any[];
handleExpand: (e: MouseEvent<HTMLDivElement>, item: unknown, itemType: LibraryItem) => void;
itemHeight: number;
itemType: LibraryItem;
}>) {
return (
<div className={styles.itemList} style={style}>
<ItemDetail
data={data[index]}
itemHeight={itemHeight}
itemType={itemType}
onClick={(e, item, itemType) => handleExpand(e, item, itemType)}
withControls
/>
</div>
);
}
@@ -18,20 +18,17 @@ import { ItemListKey } from '/@/shared/types/types';
interface AlbumListInfiniteGridProps extends ItemListGridComponentProps<AlbumListQuery> {} interface AlbumListInfiniteGridProps extends ItemListGridComponentProps<AlbumListQuery> {}
export const AlbumListInfiniteGrid = forwardRef<any, AlbumListInfiniteGridProps>( export const AlbumListInfiniteGrid = forwardRef<any, AlbumListInfiniteGridProps>(
( ({
{ gap = 'md',
gap = 'md', itemsPerPage = 100,
itemsPerPage = 100, itemsPerRow,
itemsPerRow, query = {
query = { sortBy: AlbumListSort.NAME,
sortBy: AlbumListSort.NAME, sortOrder: SortOrder.ASC,
sortOrder: SortOrder.ASC,
},
saveScrollOffset = true,
serverId,
}, },
ref, saveScrollOffset = true,
) => { serverId,
}) => {
const listCountQuery = albumQueries.listCount({ const listCountQuery = albumQueries.listCount({
query: { ...query }, query: { ...query },
serverId: serverId, serverId: serverId,
@@ -62,14 +62,16 @@ export const SubsonicAlbumFilters = ({
}), }),
); );
const selectableAlbumArtists = useMemo(() => { const items = albumArtistListQuery?.data?.items;
if (!albumArtistListQuery?.data?.items) return [];
return albumArtistListQuery?.data?.items?.map((artist) => ({ const selectableAlbumArtists = useMemo(() => {
if (!items) return [];
return items.map((artist) => ({
label: artist.name, label: artist.name,
value: artist.id, value: artist.id,
})); }));
}, [albumArtistListQuery?.data?.items]); }, [items]);
const handleAlbumArtistFilter = (e: null | string[]) => { const handleAlbumArtistFilter = (e: null | string[]) => {
setArtistIds(e ?? null); setArtistIds(e ?? null);
@@ -1,4 +1,3 @@
import { useCallback } from 'react';
import { import {
parseAsArrayOf, parseAsArrayOf,
parseAsBoolean, parseAsBoolean,
@@ -7,6 +6,7 @@ import {
parseAsString, parseAsString,
useQueryState, useQueryState,
} from 'nuqs'; } from 'nuqs';
import { useCallback } from 'react';
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter'; import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter'; import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
@@ -16,9 +16,12 @@ import { AlbumListSort, SortOrder } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey } from '/@/shared/types/types';
export const useAlbumListFilters = () => { export const useAlbumListFilters = () => {
const { sortBy, setSortBy } = useSortByFilter<AlbumListSort>(AlbumListSort.NAME, ItemListKey.ALBUM); const { setSortBy, sortBy } = useSortByFilter<AlbumListSort>(
AlbumListSort.NAME,
ItemListKey.ALBUM,
);
const { sortOrder, setSortOrder } = useSortOrderFilter(SortOrder.ASC, ItemListKey.ALBUM); const { setSortOrder, sortOrder } = useSortOrderFilter(SortOrder.ASC, ItemListKey.ALBUM);
const { searchTerm, setSearchTerm } = useSearchTermFilter(''); const { searchTerm, setSearchTerm } = useSearchTermFilter('');
@@ -15,7 +15,7 @@ import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-e
import { DefaultPlayButton } from '/@/renderer/features/shared/components/play-button'; import { DefaultPlayButton } from '/@/renderer/features/shared/components/play-button';
import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation'; import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation';
import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation'; import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation';
import { useContainerQuery, useFastAverageColor } from '/@/renderer/hooks'; import { useFastAverageColor } from '/@/renderer/hooks';
import { queryClient } from '/@/renderer/lib/react-query'; import { queryClient } from '/@/renderer/lib/react-query';
import { AppRoute } from '/@/renderer/router/routes'; import { AppRoute } from '/@/renderer/router/routes';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
@@ -33,7 +33,6 @@ import { Text } from '/@/shared/components/text/text';
import { LibraryItem, SongDetailResponse } from '/@/shared/types/domain-types'; import { LibraryItem, SongDetailResponse } from '/@/shared/types/domain-types';
const DummyAlbumDetailRoute = () => { const DummyAlbumDetailRoute = () => {
const { ref, ...cq } = useContainerQuery();
const { t } = useTranslation(); const { t } = useTranslation();
const { albumId } = useParams() as { albumId: string }; const { albumId } = useParams() as { albumId: string };
@@ -117,7 +116,7 @@ const DummyAlbumDetailRoute = () => {
return ( return (
<AnimatedPage key={`dummy-album-detail-${albumId}`}> <AnimatedPage key={`dummy-album-detail-${albumId}`}>
<LibraryContainer> <LibraryContainer>
<Stack ref={ref}> <Stack>
<LibraryHeader <LibraryHeader
imageUrl={detailQuery?.data?.imageUrl} imageUrl={detailQuery?.data?.imageUrl}
item={{ route: AppRoute.LIBRARY_SONGS, type: LibraryItem.SONG }} item={{ route: AppRoute.LIBRARY_SONGS, type: LibraryItem.SONG }}
@@ -182,7 +181,7 @@ const DummyAlbumDetailRoute = () => {
/> />
<ActionIcon <ActionIcon
icon="ellipsisHorizontal" icon="ellipsisHorizontal"
onClick={(e) => { onClick={() => {
if (!detailQuery?.data) return; if (!detailQuery?.data) return;
}} }}
variant="subtle" variant="subtle"
@@ -199,7 +198,7 @@ const DummyAlbumDetailRoute = () => {
key={`genre-${genre.id}`} key={`genre-${genre.id}`}
radius={0} radius={0}
size="compact-md" size="compact-md"
to={generatePath(AppRoute.LIBRARY_GENRES_SONGS, { to={generatePath(AppRoute.LIBRARY_GENRES_DETAIL, {
genreId: genre.id, genreId: genre.id,
})} })}
variant="outline" variant="outline"
@@ -15,102 +15,100 @@ import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { LibraryItem, ServerType } from '/@/shared/types/domain-types'; import { LibraryItem, ServerType } from '/@/shared/types/domain-types';
export const AlbumArtistDetailHeader = forwardRef( export const AlbumArtistDetailHeader = forwardRef((_props, ref: Ref<HTMLDivElement>) => {
(_props, ref: Ref<HTMLDivElement>) => { const { albumArtistId, artistId } = useParams() as {
const { albumArtistId, artistId } = useParams() as { albumArtistId?: string;
albumArtistId?: string; artistId?: string;
artistId?: string; };
}; const routeId = (artistId || albumArtistId) as string;
const routeId = (artistId || albumArtistId) as string; const server = useCurrentServer();
const server = useCurrentServer(); const { t } = useTranslation();
const { t } = useTranslation(); const detailQuery = useQuery(
const detailQuery = useQuery( artistsQueries.albumArtistDetail({
artistsQueries.albumArtistDetail({ query: { id: routeId },
query: { id: routeId }, serverId: server?.id,
serverId: server?.id, }),
}), );
);
const albumCount = detailQuery?.data?.albumCount; const albumCount = detailQuery?.data?.albumCount;
const songCount = detailQuery?.data?.songCount; const songCount = detailQuery?.data?.songCount;
const duration = detailQuery?.data?.duration; const duration = detailQuery?.data?.duration;
const durationEnabled = duration !== null && duration !== undefined; const durationEnabled = duration !== null && duration !== undefined;
const metadataItems = [ const metadataItems = [
{ {
enabled: albumCount !== null && albumCount !== undefined, enabled: albumCount !== null && albumCount !== undefined,
id: 'albumCount', id: 'albumCount',
secondary: false, secondary: false,
value: t('entity.albumWithCount', { count: albumCount || 0 }), value: t('entity.albumWithCount', { count: albumCount || 0 }),
}, },
{ {
enabled: songCount !== null && songCount !== undefined, enabled: songCount !== null && songCount !== undefined,
id: 'songCount', id: 'songCount',
secondary: false, secondary: false,
value: t('entity.trackWithCount', { count: songCount || 0 }), value: t('entity.trackWithCount', { count: songCount || 0 }),
}, },
{ {
enabled: durationEnabled, enabled: durationEnabled,
id: 'duration', id: 'duration',
secondary: true, secondary: true,
value: durationEnabled && formatDurationString(duration), value: durationEnabled && formatDurationString(duration),
}, },
]; ];
const { setRating } = usePlayer(); const { setRating } = usePlayer();
const handleUpdateRating = (rating: number) => { const handleUpdateRating = (rating: number) => {
if (!detailQuery?.data) return; if (!detailQuery?.data) return;
if (detailQuery.data.userRating === rating) {
return setRating(
detailQuery.data._serverId,
[detailQuery.data.id],
LibraryItem.ALBUM_ARTIST,
0,
);
}
if (detailQuery.data.userRating === rating) {
return setRating( return setRating(
detailQuery.data._serverId, detailQuery.data._serverId,
[detailQuery.data.id], [detailQuery.data.id],
LibraryItem.ALBUM_ARTIST, LibraryItem.ALBUM_ARTIST,
rating, 0,
); );
}; }
const showRating = detailQuery?.data?._serverType === ServerType.NAVIDROME; return setRating(
detailQuery.data._serverId,
return ( [detailQuery.data.id],
<LibraryHeader LibraryItem.ALBUM_ARTIST,
imageUrl={detailQuery?.data?.imageUrl} rating,
item={{ route: AppRoute.LIBRARY_ALBUM_ARTISTS, type: LibraryItem.ALBUM_ARTIST }}
ref={ref}
title={detailQuery?.data?.name || ''}
>
<Stack>
<Group>
{metadataItems
.filter((i) => i.enabled)
.map((item, index) => (
<Fragment key={`item-${item.id}-${index}`}>
{index > 0 && <Text isNoSelect></Text>}
<Text isMuted={item.secondary}>{item.value}</Text>
</Fragment>
))}
{showRating && (
<>
<Text isNoSelect></Text>
<Rating
onChange={handleUpdateRating}
readOnly={detailQuery?.isFetching}
value={detailQuery?.data?.userRating || 0}
/>
</>
)}
</Group>
</Stack>
</LibraryHeader>
); );
}, };
);
const showRating = detailQuery?.data?._serverType === ServerType.NAVIDROME;
return (
<LibraryHeader
imageUrl={detailQuery?.data?.imageUrl}
item={{ route: AppRoute.LIBRARY_ALBUM_ARTISTS, type: LibraryItem.ALBUM_ARTIST }}
ref={ref}
title={detailQuery?.data?.name || ''}
>
<Stack>
<Group>
{metadataItems
.filter((i) => i.enabled)
.map((item, index) => (
<Fragment key={`item-${item.id}-${index}`}>
{index > 0 && <Text isNoSelect></Text>}
<Text isMuted={item.secondary}>{item.value}</Text>
</Fragment>
))}
{showRating && (
<>
<Text isNoSelect></Text>
<Rating
onChange={handleUpdateRating}
readOnly={detailQuery?.isFetching}
value={detailQuery?.data?.userRating || 0}
/>
</>
)}
</Group>
</Stack>
</LibraryHeader>
);
});
@@ -3,7 +3,6 @@ import { ListConfigMenu } from '/@/renderer/features/shared/components/list-conf
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button'; import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown'; import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button'; import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
import { useContainerQuery } from '/@/renderer/hooks';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Flex } from '/@/shared/components/flex/flex'; import { Flex } from '/@/shared/components/flex/flex';
import { Group } from '/@/shared/components/group/group'; import { Group } from '/@/shared/components/group/group';
@@ -11,11 +10,9 @@ import { AlbumArtistListSort, LibraryItem, SortOrder } from '/@/shared/types/dom
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey } from '/@/shared/types/types';
export const AlbumArtistListHeaderFilters = () => { export const AlbumArtistListHeaderFilters = () => {
const { ref, ...cq } = useContainerQuery();
return ( return (
<Flex justify="space-between"> <Flex justify="space-between">
<Group gap="sm" ref={ref} w="100%"> <Group gap="sm" w="100%">
<ListSortByDropdown <ListSortByDropdown
defaultSortByValue={AlbumArtistListSort.NAME} defaultSortByValue={AlbumArtistListSort.NAME}
itemType={LibraryItem.ALBUM_ARTIST} itemType={LibraryItem.ALBUM_ARTIST}
@@ -20,20 +20,17 @@ interface AlbumArtistListInfiniteGridProps
extends ItemListGridComponentProps<AlbumArtistListQuery> {} extends ItemListGridComponentProps<AlbumArtistListQuery> {}
export const AlbumArtistListInfiniteGrid = forwardRef<any, AlbumArtistListInfiniteGridProps>( export const AlbumArtistListInfiniteGrid = forwardRef<any, AlbumArtistListInfiniteGridProps>(
( ({
{ gap = 'md',
gap = 'md', itemsPerPage = 100,
itemsPerPage = 100, itemsPerRow,
itemsPerRow, query = {
query = { sortBy: AlbumArtistListSort.NAME,
sortBy: AlbumArtistListSort.NAME, sortOrder: SortOrder.ASC,
sortOrder: SortOrder.ASC,
},
saveScrollOffset = true,
serverId,
}, },
ref, saveScrollOffset = true,
) => { serverId,
}) => {
const listCountQuery = artistsQueries.albumArtistListCount({ const listCountQuery = artistsQueries.albumArtistListCount({
query: { ...query }, query: { ...query },
serverId: serverId, serverId: serverId,
@@ -8,7 +8,6 @@ import { ListSelectFilter } from '/@/renderer/features/shared/components/list-se
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown'; import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button'; import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils'; import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { useContainerQuery } from '/@/renderer/hooks';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { Divider } from '/@/shared/components/divider/divider'; import { Divider } from '/@/shared/components/divider/divider';
import { Flex } from '/@/shared/components/flex/flex'; import { Flex } from '/@/shared/components/flex/flex';
@@ -17,14 +16,13 @@ import { ArtistListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-t
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey } from '/@/shared/types/types';
export const ArtistListHeaderFilters = () => { export const ArtistListHeaderFilters = () => {
const { ref, ...cq } = useContainerQuery();
const server = useCurrentServer(); const server = useCurrentServer();
const rolesQuery = useQuery(sharedQueries.roles({ query: {}, serverId: server.id })); const rolesQuery = useQuery(sharedQueries.roles({ query: {}, serverId: server.id }));
return ( return (
<Flex justify="space-between"> <Flex justify="space-between">
<Group gap="sm" ref={ref} w="100%"> <Group gap="sm" w="100%">
<ListSortByDropdown <ListSortByDropdown
defaultSortByValue={ArtistListSort.NAME} defaultSortByValue={ArtistListSort.NAME}
itemType={LibraryItem.ARTIST} itemType={LibraryItem.ARTIST}
@@ -19,20 +19,17 @@ import { ItemListKey } from '/@/shared/types/types';
interface ArtistListInfiniteGridProps extends ItemListGridComponentProps<ArtistListQuery> {} interface ArtistListInfiniteGridProps extends ItemListGridComponentProps<ArtistListQuery> {}
export const ArtistListInfiniteGrid = forwardRef<any, ArtistListInfiniteGridProps>( export const ArtistListInfiniteGrid = forwardRef<any, ArtistListInfiniteGridProps>(
( ({
{ gap = 'md',
gap = 'md', itemsPerPage = 100,
itemsPerPage = 100, itemsPerRow,
itemsPerRow, query = {
query = { sortBy: ArtistListSort.NAME,
sortBy: ArtistListSort.NAME, sortOrder: SortOrder.ASC,
sortOrder: SortOrder.ASC,
},
saveScrollOffset = true,
serverId,
}, },
ref, saveScrollOffset = true,
) => { serverId,
}) => {
const listCountQuery = artistsQueries.artistListCount({ const listCountQuery = artistsQueries.artistListCount({
query: { ...query }, query: { ...query },
serverId: serverId, serverId: serverId,
@@ -1,8 +1,7 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import { useMemo, useRef } from 'react'; import { useMemo } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { ItemListHandle } from '/@/renderer/components/item-list/types';
import { ListContext } from '/@/renderer/context/list-context'; import { ListContext } from '/@/renderer/context/list-context';
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api'; import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { AlbumArtistDetailTopSongsListHeader } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-header'; import { AlbumArtistDetailTopSongsListHeader } from '/@/renderer/features/artists/components/album-artist-detail-top-songs-list-header';
@@ -13,7 +12,6 @@ import { useCurrentServer } from '/@/renderer/store/auth.store';
import { LibraryItem } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
const AlbumArtistDetailTopSongsListRoute = () => { const AlbumArtistDetailTopSongsListRoute = () => {
const tableRef = useRef<ItemListHandle | null>(null);
const { albumArtistId, artistId } = useParams() as { const { albumArtistId, artistId } = useParams() as {
albumArtistId?: string; albumArtistId?: string;
artistId?: string; artistId?: string;
@@ -52,21 +52,23 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
const { recentPlaylistId } = useRecentPlaylists(serverId); const { recentPlaylistId } = useRecentPlaylists(serverId);
const fuse = useMemo(() => { const playlists = playlistsQuery.data?.items;
if (!playlistsQuery.data?.items) return null;
return new Fuse(playlistsQuery.data.items, { const fuse = useMemo(() => {
if (!playlists) return null;
return new Fuse(playlists, {
fieldNormWeight: 1, fieldNormWeight: 1,
ignoreLocation: true, ignoreLocation: true,
keys: ['name'], keys: ['name'],
threshold: 0.3, threshold: 0.3,
}); });
}, [playlistsQuery.data?.items]); }, [playlists]);
const recentPlaylist = useMemo(() => { const recentPlaylist = useMemo(() => {
if (!playlistsQuery.data?.items || !recentPlaylistId) return null; if (!playlists || !recentPlaylistId) return null;
const playlist = playlistsQuery.data.items.find((p) => p.id === recentPlaylistId); const playlist = playlists.find((p) => p.id === recentPlaylistId);
if (!playlist) return null; if (!playlist) return null;
if (searchTerm && fuse) { if (searchTerm && fuse) {
@@ -76,22 +78,22 @@ export const AddToPlaylistAction = ({ items, itemType }: AddToPlaylistActionProp
} }
return playlist; return playlist;
}, [playlistsQuery.data?.items, recentPlaylistId, searchTerm, fuse]); }, [playlists, recentPlaylistId, searchTerm, fuse]);
const filteredPlaylists = useMemo(() => { const filteredPlaylists = useMemo(() => {
if (!playlistsQuery.data?.items) return []; if (!playlists) return [];
if (!searchTerm || !fuse) { if (!searchTerm || !fuse) {
// Exclude recent playlist from the list if it exists // Exclude recent playlist from the list if it exists
return recentPlaylistId return recentPlaylistId
? playlistsQuery.data.items.filter((p) => p.id !== recentPlaylistId) ? playlists.filter((p) => p.id !== recentPlaylistId)
: playlistsQuery.data.items; : playlists;
} }
const results = fuse.search(searchTerm); const results = fuse.search(searchTerm);
const filtered = results.map((result) => result.item); const filtered = results.map((result) => result.item);
// Exclude recent playlist from the filtered results if it exists // Exclude recent playlist from the filtered results if it exists
return recentPlaylistId ? filtered.filter((p) => p.id !== recentPlaylistId) : filtered; return recentPlaylistId ? filtered.filter((p) => p.id !== recentPlaylistId) : filtered;
}, [playlistsQuery.data?.items, searchTerm, fuse, recentPlaylistId]); }, [playlists, searchTerm, fuse, recentPlaylistId]);
const getSongsByAlbum = useCallback( const getSongsByAlbum = useCallback(
async (albumId: string) => { async (albumId: string) => {
@@ -16,10 +16,9 @@ import { LibraryItem, QueueSong } from '/@/shared/types/domain-types';
interface QueueContextMenuProps { interface QueueContextMenuProps {
items: QueueSong[]; items: QueueSong[];
type: LibraryItem.QUEUE_SONG;
} }
export const QueueContextMenu = ({ items, type }: QueueContextMenuProps) => { export const QueueContextMenu = ({ items }: QueueContextMenuProps) => {
const { ids } = useMemo(() => { const { ids } = useMemo(() => {
const ids = items.map((item) => item.id); const ids = items.map((item) => item.id);
return { ids }; return { ids };
@@ -27,9 +26,7 @@ export const QueueContextMenu = ({ items, type }: QueueContextMenuProps) => {
return ( return (
<ContextMenu.Content <ContextMenu.Content
bottomStickyContent={ bottomStickyContent={<ContextMenuPreview items={items} itemType={LibraryItem.SONG} />}
<ContextMenuPreview items={items} itemType={LibraryItem.SONG} />
}
> >
<RemoveFromQueueAction items={items} /> <RemoveFromQueueAction items={items} />
<ContextMenu.Divider /> <ContextMenu.Divider />
@@ -19,20 +19,17 @@ import { ItemListKey } from '/@/shared/types/types';
interface GenreListInfiniteGridProps extends ItemListGridComponentProps<GenreListQuery> {} interface GenreListInfiniteGridProps extends ItemListGridComponentProps<GenreListQuery> {}
export const GenreListInfiniteGrid = forwardRef<any, GenreListInfiniteGridProps>( export const GenreListInfiniteGrid = forwardRef<any, GenreListInfiniteGridProps>(
( ({
{ gap = 'md',
gap = 'md', itemsPerPage = 100,
itemsPerPage = 100, itemsPerRow,
itemsPerRow, query = {
query = { sortBy: GenreListSort.NAME,
sortBy: GenreListSort.NAME, sortOrder: SortOrder.ASC,
sortOrder: SortOrder.ASC,
},
saveScrollOffset = true,
serverId,
}, },
ref, saveScrollOffset = true,
) => { serverId,
}) => {
const listCountQuery = genresQueries.listCount({ const listCountQuery = genresQueries.listCount({
query: { ...query }, query: { ...query },
serverId: serverId, serverId: serverId,
@@ -3,8 +3,8 @@ import { useTranslation } from 'react-i18next';
import { openLyricSearchModal } from '/@/renderer/features/lyrics/components/lyrics-search-form'; import { openLyricSearchModal } from '/@/renderer/features/lyrics/components/lyrics-search-form';
import { import {
usePlayerSong,
useLyricsSettings, useLyricsSettings,
usePlayerSong,
useSettingsStore, useSettingsStore,
useSettingsStoreActions, useSettingsStoreActions,
} from '/@/renderer/store'; } from '/@/renderer/store';
@@ -44,9 +44,9 @@
.header { .header {
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
gap: 0.5rem;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
gap: 0.5rem;
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
} }
@@ -80,10 +80,10 @@
box-shadow: 0 8px 24px rgb(0 0 0 / 30%); box-shadow: 0 8px 24px rgb(0 0 0 / 30%);
} }
.imageNativeAspectRatio { .image-native-aspect-ratio {
aspect-ratio: auto;
height: auto; height: auto;
max-height: 100%; max-height: 100%;
aspect-ratio: auto;
} }
.album-image { .album-image {
@@ -1,11 +1,10 @@
import formatDuration from 'format-duration'; import formatDuration from 'format-duration';
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import styles from './playerbar-slider.module.css'; import { CustomPlayerbarSlider } from './playerbar-slider';
import { usePlayer } from '/@/renderer/features/player/context/player-context'; import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { usePlayerTimestamp } from '/@/renderer/store'; import { usePlayerTimestamp } from '/@/renderer/store';
import { CustomPlayerbarSlider } from './playerbar-slider';
interface PlayerbarSeekSliderProps { interface PlayerbarSeekSliderProps {
max: number; max: number;
@@ -19,20 +19,17 @@ import { ItemListKey } from '/@/shared/types/types';
interface PlaylistListInfiniteGridProps extends ItemListGridComponentProps<PlaylistListQuery> {} interface PlaylistListInfiniteGridProps extends ItemListGridComponentProps<PlaylistListQuery> {}
export const PlaylistListInfiniteGrid = forwardRef<any, PlaylistListInfiniteGridProps>( export const PlaylistListInfiniteGrid = forwardRef<any, PlaylistListInfiniteGridProps>(
( ({
{ gap = 'md',
gap = 'md', itemsPerPage = 100,
itemsPerPage = 100, itemsPerRow,
itemsPerRow, query = {
query = { sortBy: PlaylistListSort.NAME,
sortBy: PlaylistListSort.NAME, sortOrder: SortOrder.ASC,
sortOrder: SortOrder.ASC,
},
saveScrollOffset = true,
serverId,
}, },
ref, saveScrollOffset = true,
) => { serverId,
}) => {
const listCountQuery = playlistsQueries.listCount({ const listCountQuery = playlistsQueries.listCount({
query: { ...query }, query: { ...query },
serverId: serverId, serverId: serverId,
@@ -1,8 +1,4 @@
import { import { parseAsJson, useQueryState } from 'nuqs';
parseAsJson,
parseAsString,
useQueryState,
} from 'nuqs';
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter'; import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter'; import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
@@ -17,16 +13,13 @@ export const usePlaylistListFilters = () => {
const { searchTerm, setSearchTerm } = useSearchTermFilter(''); const { searchTerm, setSearchTerm } = useSearchTermFilter('');
const [custom, setCustom] = useQueryState( const [custom, setCustom] = useQueryState('playlistCustom', parseAsJson(customFiltersSchema));
'playlistCustom',
parseAsJson(customFiltersSchema),
);
const query = { const query = {
_custom: custom ?? undefined,
searchTerm: searchTerm ?? undefined, searchTerm: searchTerm ?? undefined,
sortBy: sortByFilter[FILTER_KEYS.SHARED.SORT_BY] ?? undefined, sortBy: sortByFilter[FILTER_KEYS.SHARED.SORT_BY] ?? undefined,
sortOrder: sortOrderFilter[FILTER_KEYS.SHARED.SORT_ORDER] ?? undefined, sortOrder: sortOrderFilter[FILTER_KEYS.SHARED.SORT_ORDER] ?? undefined,
_custom: custom ?? undefined,
}; };
return { return {
@@ -3,7 +3,6 @@ import { Fragment, useCallback, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { generatePath, useNavigate } from 'react-router'; import { generatePath, useNavigate } from 'react-router';
import { usePlayer } from '/@/renderer/features/player/context/player-context';
import { searchQueries } from '/@/renderer/features/search/api/search-api'; import { searchQueries } from '/@/renderer/features/search/api/search-api';
import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command'; import { Command, CommandPalettePages } from '/@/renderer/features/search/components/command';
import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable'; import { CommandItemSelectable } from '/@/renderer/features/search/components/command-item-selectable';
@@ -40,10 +40,10 @@ export const LibraryCommandItem = ({
(e: SyntheticEvent, id: string, playType: Play) => { (e: SyntheticEvent, id: string, playType: Play) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
if (!server?.id) return; if (!server.id) return;
addToQueueByFetch(server.id, [id], itemType, playType); addToQueueByFetch(server.id, [id], itemType, playType);
}, },
[addToQueueByFetch, itemType, server?.id], [addToQueueByFetch, itemType, server],
); );
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
@@ -5,7 +5,6 @@ import { PageHeader } from '/@/renderer/components/page-header/page-header';
import { useSettingSearchContext } from '/@/renderer/features/settings/context/search-context'; import { useSettingSearchContext } from '/@/renderer/features/settings/context/search-context';
import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar'; import { LibraryHeaderBar } from '/@/renderer/features/shared/components/library-header-bar';
import { SearchInput } from '/@/renderer/features/shared/components/search-input'; import { SearchInput } from '/@/renderer/features/shared/components/search-input';
import { useContainerQuery } from '/@/renderer/hooks';
import { useSettingsStoreActions } from '/@/renderer/store/settings.store'; import { useSettingsStoreActions } from '/@/renderer/store/settings.store';
import { Button } from '/@/shared/components/button/button'; import { Button } from '/@/shared/components/button/button';
import { Flex } from '/@/shared/components/flex/flex'; import { Flex } from '/@/shared/components/flex/flex';
@@ -22,7 +21,6 @@ export const SettingsHeader = ({ setSearch }: SettingsHeaderProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { reset } = useSettingsStoreActions(); const { reset } = useSettingsStoreActions();
const search = useSettingSearchContext(); const search = useSettingSearchContext();
const { ref, ...cq } = useContainerQuery();
const handleResetToDefault = () => { const handleResetToDefault = () => {
reset(); reset();
@@ -41,7 +39,7 @@ export const SettingsHeader = ({ setSearch }: SettingsHeaderProps) => {
}; };
return ( return (
<Flex ref={ref}> <Flex>
<PageHeader> <PageHeader>
<LibraryHeaderBar ignoreMaxWidth> <LibraryHeaderBar ignoreMaxWidth>
<Flex align="center" justify="space-between" w="100%"> <Flex align="center" justify="space-between" w="100%">
@@ -1,7 +1,7 @@
import { parseAsString, useQueryState } from 'nuqs'; import { parseAsString, useQueryState } from 'nuqs';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { useListFilterPersistence } from '/@/renderer/features/shared/hooks/use-list-filter-persistence'; import { useListFilterPersistence } from '/@/renderer/features/shared/hooks/use-list-filter-persistence';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { useCurrentServer } from '/@/renderer/store'; import { useCurrentServer } from '/@/renderer/store';
import { ItemListKey } from '/@/shared/types/types'; import { ItemListKey } from '/@/shared/types/types';
@@ -4,7 +4,7 @@ import { useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data'; import { MultiSelectWithInvalidData } from '/@/renderer/components/select-with-invalid-data';
import { genresQueries, useGenreList } from '/@/renderer/features/genres/api/genres-api'; import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api'; import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters'; import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
import { SongListFilter, useCurrentServer } from '/@/renderer/store'; import { SongListFilter, useCurrentServer } from '/@/renderer/store';
@@ -14,7 +14,7 @@ import { NumberInput } from '/@/shared/components/number-input/number-input';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select'; import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
import { GenreListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-types'; import { LibraryItem } from '/@/shared/types/domain-types';
interface JellyfinSongFiltersProps { interface JellyfinSongFiltersProps {
customFilters?: Partial<SongListFilter>; customFilters?: Partial<SongListFilter>;
@@ -7,7 +7,7 @@ import {
MultiSelectWithInvalidData, MultiSelectWithInvalidData,
SelectWithInvalidData, SelectWithInvalidData,
} from '/@/renderer/components/select-with-invalid-data'; } from '/@/renderer/components/select-with-invalid-data';
import { genresQueries, useGenreList } from '/@/renderer/features/genres/api/genres-api'; import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api'; import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { import {
SongListFilter, SongListFilter,
@@ -23,7 +23,7 @@ import { NumberInput } from '/@/shared/components/number-input/number-input';
import { Stack } from '/@/shared/components/stack/stack'; import { Stack } from '/@/shared/components/stack/stack';
import { Text } from '/@/shared/components/text/text'; import { Text } from '/@/shared/components/text/text';
import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select'; import { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
import { GenreListSort, LibraryItem, SongListQuery, SortOrder } from '/@/shared/types/domain-types'; import { LibraryItem, SongListQuery } from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types'; import { ServerFeature } from '/@/shared/types/features-types';
interface NavidromeSongFiltersProps { interface NavidromeSongFiltersProps {
@@ -14,20 +14,17 @@ import { ItemListKey } from '/@/shared/types/types';
interface SongListInfiniteGridProps extends ItemListGridComponentProps<SongListQuery> {} interface SongListInfiniteGridProps extends ItemListGridComponentProps<SongListQuery> {}
export const SongListInfiniteGrid = forwardRef<any, SongListInfiniteGridProps>( export const SongListInfiniteGrid = forwardRef<any, SongListInfiniteGridProps>(
( ({
{ gap = 'md',
gap = 'md', itemsPerPage = 100,
itemsPerPage = 100, itemsPerRow,
itemsPerRow, query = {
query = { sortBy: SongListSort.NAME,
sortBy: SongListSort.NAME, sortOrder: SortOrder.ASC,
sortOrder: SortOrder.ASC,
},
saveScrollOffset = true,
serverId,
}, },
ref, saveScrollOffset = true,
) => { serverId,
}) => {
const listCountQuery = songsQueries.listCount({ const listCountQuery = songsQueries.listCount({
query: { ...query }, query: { ...query },
serverId: serverId, serverId: serverId,
@@ -26,7 +26,6 @@
max-width: 100%; max-width: 100%;
padding: var(--theme-spacing-sm) var(--theme-spacing-md); padding: var(--theme-spacing-sm) var(--theme-spacing-md);
font-size: var(--theme-font-size-sm); font-size: var(--theme-font-size-sm);
word-wrap: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
white-space: normal; white-space: normal;
cursor: default; cursor: default;
@@ -43,7 +42,6 @@
& > *:not(.left-icon, .right-icon) { & > *:not(.left-icon, .right-icon) {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
word-wrap: break-word;
overflow-wrap: break-word; overflow-wrap: break-word;
} }