diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts
index 2bbcbce09..d0bc9a0c0 100644
--- a/src/renderer/api/navidrome/navidrome-controller.ts
+++ b/src/renderer/api/navidrome/navidrome-controller.ts
@@ -299,6 +299,7 @@ export const NavidromeController: InternalControllerEndpoint = {
genre_id: genres,
library_id: getLibraryId(query.musicFolderId),
name: query.searchTerm,
+ year: query.maxYear || query.minYear,
...query._custom,
starred: query.favorite,
...excludeMissing(apiClientProps.server),
diff --git a/src/renderer/features/albums/components/album-list-content.tsx b/src/renderer/features/albums/components/album-list-content.tsx
index 79fdb3548..d79f5ab2d 100644
--- a/src/renderer/features/albums/components/album-list-content.tsx
+++ b/src/renderer/features/albums/components/album-list-content.tsx
@@ -2,9 +2,12 @@ import { lazy, Suspense, useMemo } from 'react';
import { useListContext } from '/@/renderer/context/list-context';
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
+import { ListFilters } from '/@/renderer/features/shared/components/list-filters';
+import { ListWithSidebarContainer } from '/@/renderer/features/shared/components/list-with-sidebar-container';
import { ItemListSettings, useCurrentServer, useListSettings } from '/@/renderer/store';
+import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
import { Spinner } from '/@/shared/components/spinner/spinner';
-import { AlbumListQuery } from '/@/shared/types/domain-types';
+import { AlbumListQuery, LibraryItem } from '/@/shared/types/domain-types';
import { ItemListKey, ListDisplayType, ListPaginationType } from '/@/shared/types/types';
const AlbumListInfiniteGrid = lazy(() =>
@@ -37,16 +40,23 @@ export const AlbumListContent = () => {
const { customFilters } = useListContext();
return (
- }>
-
-
+ <>
+
+
+
+
+
+ }>
+
+
+ >
);
};
@@ -77,6 +87,10 @@ export const AlbumListView = ({
};
}, [query, overrideQuery]);
+ console.log('query', query);
+ console.log('overrideQuery', overrideQuery);
+ console.log('mergedQuery', mergedQuery);
+
switch (display) {
case ListDisplayType.GRID: {
switch (pagination) {
diff --git a/src/renderer/features/albums/components/album-list-header-filters.tsx b/src/renderer/features/albums/components/album-list-header-filters.tsx
index f6aa8c053..6db35b049 100644
--- a/src/renderer/features/albums/components/album-list-header-filters.tsx
+++ b/src/renderer/features/albums/components/album-list-header-filters.tsx
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { ALBUM_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
-import { ListFilters } from '/@/renderer/features/shared/components/list-filters';
+import { ListFiltersModal } from '/@/renderer/features/shared/components/list-filters';
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
@@ -65,7 +65,7 @@ export const AlbumListHeaderFilters = ({ toggleGenreTarget }: { toggleGenreTarge
defaultSortOrder={SortOrder.ASC}
listKey={ItemListKey.ALBUM}
/>
-
+
diff --git a/src/renderer/features/albums/components/navidrome-album-filters.tsx b/src/renderer/features/albums/components/navidrome-album-filters.tsx
index 9e7c59e5c..9bb58cb60 100644
--- a/src/renderer/features/albums/components/navidrome-album-filters.tsx
+++ b/src/renderer/features/albums/components/navidrome-album-filters.tsx
@@ -1,6 +1,5 @@
import { useQuery } from '@tanstack/react-query';
-import debounce from 'lodash/debounce';
-import { ChangeEvent, useMemo } from 'react';
+import { ChangeEvent, memo, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
@@ -11,19 +10,17 @@ import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album
import { artistsQueries } from '/@/renderer/features/artists/api/artists-api';
import { useGenreList } from '/@/renderer/features/genres/api/genres-api';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
-import { useCurrentServer } from '/@/renderer/store';
-import { NDSongQueryFields } from '/@/shared/api/navidrome/navidrome-types';
-import { hasFeature } from '/@/shared/api/utils';
+import { useCurrentServer, useCurrentServerId } from '/@/renderer/store';
+import { titleCase } from '/@/renderer/utils';
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 { Spinner, 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 { YesNoSelect } from '/@/shared/components/yes-no-select/yes-no-select';
import { AlbumArtistListSort, LibraryItem, SortOrder } from '/@/shared/types/domain-types';
-import { ServerFeature } from '/@/shared/types/features-types';
interface NavidromeAlbumFiltersProps {
disableArtistFilter?: boolean;
@@ -38,7 +35,6 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
query,
setAlbumArtist,
setCompilation,
- setCustom,
setFavorite,
setGenreId,
setHasRating,
@@ -57,60 +53,73 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
}));
}, [genreListQuery.data]);
- const tagsQuery = useQuery(
- sharedQueries.tags({
- options: {
- gcTime: 1000 * 60 * 2,
- staleTime: 1000 * 60 * 1,
+ const yesNoUndefinedFilters = useMemo(
+ () => [
+ {
+ label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
+ onChange: (favorite?: boolean) => {
+ setFavorite(favorite ?? null);
+ },
+ value: query.favorite,
},
- query: {
- type: LibraryItem.ALBUM,
+ {
+ label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
+ onChange: (compilation?: boolean) => {
+ setCompilation(compilation ?? null);
+ },
+ value: query.compilation,
},
- serverId,
- }),
+ ],
+ [t, query.favorite, query.compilation, setFavorite, setCompilation],
);
- const yesNoUndefinedFilters = [
- {
- label: t('filter.isFavorited', { postProcess: 'sentenceCase' }),
- onChange: (favorite?: boolean) => {
- setFavorite(favorite ?? null);
+ const toggleFilters = useMemo(
+ () => [
+ {
+ label: t('filter.isRated', { postProcess: 'sentenceCase' }),
+ onChange: (e: ChangeEvent) => {
+ const hasRating = e.currentTarget.checked ? true : undefined;
+ setHasRating(hasRating ?? null);
+ },
+ value: query.hasRating,
},
- value: query.favorite,
- },
- {
- label: t('filter.isCompilation', { postProcess: 'sentenceCase' }),
- onChange: (compilation?: boolean) => {
- setCompilation(compilation ?? null);
+ {
+ label: t('filter.isRecentlyPlayed', { postProcess: 'sentenceCase' }),
+ onChange: (e: ChangeEvent) => {
+ const recentlyPlayed = e.currentTarget.checked ? true : undefined;
+ setRecentlyPlayed(recentlyPlayed ?? null);
+ },
+ value: query.recentlyPlayed,
},
- value: query.compilation,
- },
- ];
+ ],
+ [t, query.hasRating, query.recentlyPlayed, setHasRating, setRecentlyPlayed],
+ );
- const toggleFilters = [
- {
- label: t('filter.isRated', { postProcess: 'sentenceCase' }),
- onChange: (e: ChangeEvent) => {
- const hasRating = e.currentTarget.checked ? true : undefined;
- setHasRating(hasRating ?? null);
- },
- value: query.hasRating,
- },
- {
- label: t('filter.isRecentlyPlayed', { postProcess: 'sentenceCase' }),
- onChange: (e: ChangeEvent) => {
- const recentlyPlayed = e.currentTarget.checked ? true : undefined;
- setRecentlyPlayed(recentlyPlayed ?? null);
- },
- value: query.recentlyPlayed,
- },
- ];
+ const handleYearFilter = useMemo(
+ () => (e: number | string) => {
+ // Handle empty string, null, undefined, or invalid numbers as clearing
- const handleYearFilter = debounce((e: number | string) => {
- const year = e === '' ? undefined : (e as number);
- setMinYear(year ?? null);
- setMaxYear(year ?? null);
- }, 500);
+ if (e === '' || e === null || e === undefined) {
+ console.log('clearing year filters');
+ setMinYear(null);
+ setMaxYear(null);
+ return;
+ }
+
+ const year = typeof e === 'number' ? e : Number(e);
+ // If it's a valid number, set it; otherwise clear
+ if (!isNaN(year) && isFinite(year) && year > 0) {
+ console.log('setting year filters', year);
+ setMinYear(year);
+ setMaxYear(year);
+ } else {
+ console.log('clearing year filters', year);
+ setMinYear(null);
+ setMaxYear(null);
+ }
+ },
+ [setMinYear, setMaxYear],
+ );
const albumArtistListQuery = useQuery(
artistsQueries.albumArtistList({
@@ -136,26 +145,15 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
}));
}, [albumArtistListQuery.data?.items]);
- const handleTagFilter = debounce((tag: string, e: null | string) => {
- setCustom((prev) => ({
- ...prev,
- [tag]: e || undefined,
- }));
- }, 250);
-
- const hasBFR = hasFeature(server, ServerFeature.BFR);
-
return (
{yesNoUndefinedFilters.map((filter) => (
-
- {filter.label}
-
-
+
))}
{toggleFilters.map((filter) => (
@@ -164,67 +162,153 @@ export const NavidromeAlbumFilters = ({ disableArtistFilter }: NavidromeAlbumFil
))}
-
- handleYearFilter(e)}
- />
- (e ? setGenreId([e]) : undefined)}
- searchable
- />
-
- {hasBFR && (
-
- (e ? setGenreId(e) : undefined)}
- searchable
- />
-
- )}
-
- setAlbumArtist(e ? [e] : null)}
- rightSection={albumArtistListQuery.isFetching ? : undefined}
- searchable
- />
-
- {tagsQuery.data?.enumTags?.length &&
- tagsQuery.data.enumTags.length > 0 &&
- tagsQuery.data.enumTags.map((tag) => (
-
- i.value === tag.name)?.label ||
- tag.name
- }
- onChange={(value) => handleTagFilter(tag.name, value)}
- searchable
- width={150}
- />
-
- ))}
+ handleYearFilter(e.currentTarget.value)}
+ />
+ (e && e.length > 0 ? setGenreId(e) : setGenreId(null))}
+ searchable
+ />
+ setAlbumArtist(e ? [e] : null)}
+ rightSection={albumArtistListQuery.isFetching ? : undefined}
+ searchable
+ />
+
);
};
+
+interface TagFilterItemProps {
+ label: string;
+ onChange: (value: null | string) => void;
+ options: string[];
+ tagValue: string;
+ value: string | undefined;
+}
+
+const TagFilterItem = memo(
+ ({ label, onChange, options, tagValue, value }: TagFilterItemProps) => {
+ return (
+
+ );
+ },
+ (prevProps, nextProps) => {
+ // Only re-render if the specific tag's value or options change
+ // We don't compare onChange since it's a stable wrapper around handleTagFilter
+ // and handleTagFilter itself is memoized and stable
+ return (
+ prevProps.tagValue === nextProps.tagValue &&
+ prevProps.label === nextProps.label &&
+ prevProps.value === nextProps.value &&
+ prevProps.options === nextProps.options
+ );
+ },
+);
+
+TagFilterItem.displayName = 'TagFilterItem';
+
+const TagFilters = () => {
+ const { query, setCustom } = useAlbumListFilters();
+
+ const serverId = useCurrentServerId();
+
+ const tagsQuery = useQuery(
+ sharedQueries.tags({
+ options: {
+ gcTime: 1000 * 60 * 60,
+ staleTime: 1000 * 60 * 60,
+ },
+ query: {
+ type: LibraryItem.ALBUM,
+ },
+ serverId,
+ }),
+ );
+
+ const handleTagFilter = useMemo(
+ () => (tag: string, e: null | string) => {
+ setCustom((prev) => {
+ if (!prev) {
+ return e ? { [tag]: e } : null;
+ }
+
+ if (e === null) {
+ const rest = Object.fromEntries(
+ Object.entries(prev).filter(([key]) => key !== tag),
+ );
+
+ return Object.keys(rest).length === 0 ? null : rest;
+ }
+
+ return {
+ ...prev,
+ [tag]: e,
+ };
+ });
+ },
+ [setCustom],
+ );
+
+ const tags = useMemo(() => {
+ return (
+ tagsQuery.data?.enumTags?.map((tag) => ({
+ label: titleCase(tag.name),
+ options: tag.options,
+ value: tag.name,
+ })) || []
+ );
+ }, [tagsQuery.data?.enumTags]);
+
+ // Create stable onChange handlers for each tag using useMemo
+ const tagHandlers = useMemo(() => {
+ const handlers = new Map void>();
+ tags.forEach((tag) => {
+ handlers.set(tag.value, (value: null | string) => handleTagFilter(tag.value, value));
+ });
+ return handlers;
+ }, [tags, handleTagFilter]);
+
+ if (tagsQuery.isLoading) {
+ return ;
+ }
+
+ return (
+ <>
+ {tags.map((tag) => (
+
+ ))}
+ >
+ );
+};
diff --git a/src/renderer/features/albums/hooks/use-album-list-filters.ts b/src/renderer/features/albums/hooks/use-album-list-filters.ts
index 24bcd244b..1694247b2 100644
--- a/src/renderer/features/albums/hooks/use-album-list-filters.ts
+++ b/src/renderer/features/albums/hooks/use-album-list-filters.ts
@@ -6,7 +6,7 @@ import {
parseAsString,
useQueryState,
} from 'nuqs';
-import { useCallback } from 'react';
+import { useCallback, useMemo } from 'react';
import { useSearchTermFilter } from '/@/renderer/features/shared/hooks/use-search-term-filter';
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
@@ -86,20 +86,36 @@ export const useAlbumListFilters = () => {
setSortOrder,
]);
- const query = {
- [FILTER_KEYS.ALBUM._CUSTOM]: custom ?? undefined,
- [FILTER_KEYS.ALBUM.ARTIST_IDS]: albumArtist ?? undefined,
- [FILTER_KEYS.ALBUM.COMPILATION]: compilation ?? undefined,
- [FILTER_KEYS.ALBUM.FAVORITE]: favorite ?? undefined,
- [FILTER_KEYS.ALBUM.GENRE_ID]: genreId ?? undefined,
- [FILTER_KEYS.ALBUM.HAS_RATING]: hasRating ?? undefined,
- [FILTER_KEYS.ALBUM.MAX_YEAR]: maxYear ?? undefined,
- [FILTER_KEYS.ALBUM.MIN_YEAR]: minYear ?? undefined,
- [FILTER_KEYS.ALBUM.RECENTLY_PLAYED]: recentlyPlayed ?? undefined,
- [FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
- [FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
- [FILTER_KEYS.SHARED.SORT_ORDER]: sortOrder ?? undefined,
- };
+ const query = useMemo(
+ () => ({
+ [FILTER_KEYS.ALBUM._CUSTOM]: custom ?? undefined,
+ [FILTER_KEYS.ALBUM.ARTIST_IDS]: albumArtist ?? undefined,
+ [FILTER_KEYS.ALBUM.COMPILATION]: compilation ?? undefined,
+ [FILTER_KEYS.ALBUM.FAVORITE]: favorite ?? undefined,
+ [FILTER_KEYS.ALBUM.GENRE_ID]: genreId ?? undefined,
+ [FILTER_KEYS.ALBUM.HAS_RATING]: hasRating ?? undefined,
+ [FILTER_KEYS.ALBUM.MAX_YEAR]: maxYear ?? undefined,
+ [FILTER_KEYS.ALBUM.MIN_YEAR]: minYear ?? undefined,
+ [FILTER_KEYS.ALBUM.RECENTLY_PLAYED]: recentlyPlayed ?? undefined,
+ [FILTER_KEYS.SHARED.SEARCH_TERM]: searchTerm ?? undefined,
+ [FILTER_KEYS.SHARED.SORT_BY]: sortBy ?? undefined,
+ [FILTER_KEYS.SHARED.SORT_ORDER]: sortOrder ?? undefined,
+ }),
+ [
+ custom,
+ albumArtist,
+ compilation,
+ favorite,
+ genreId,
+ hasRating,
+ maxYear,
+ minYear,
+ recentlyPlayed,
+ searchTerm,
+ sortBy,
+ sortOrder,
+ ],
+ );
return {
clear,
diff --git a/src/renderer/features/albums/routes/album-list-route.tsx b/src/renderer/features/albums/routes/album-list-route.tsx
index 8b6a9d0b1..101a6b8b8 100644
--- a/src/renderer/features/albums/routes/album-list-route.tsx
+++ b/src/renderer/features/albums/routes/album-list-route.tsx
@@ -5,6 +5,7 @@ import { ListContext } from '/@/renderer/context/list-context';
import { AlbumListContent } from '/@/renderer/features/albums/components/album-list-content';
import { AlbumListHeader } from '/@/renderer/features/albums/components/album-list-header';
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
+import { ListWithSidebarContainer } from '/@/renderer/features/shared/components/list-with-sidebar-container';
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
import { AlbumListQuery } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
@@ -28,10 +29,19 @@ const AlbumListRoute = () => {
const [itemCount, setItemCount] = useState(undefined);
const customFilters: Partial = useMemo(() => {
- return {
- artistIds: albumArtistId ? [albumArtistId] : undefined,
- genreIds: genreId ? [genreId] : undefined,
- };
+ if (albumArtistId) {
+ return {
+ artistIds: [albumArtistId],
+ };
+ }
+
+ if (genreId) {
+ return {
+ genreIds: [genreId],
+ };
+ }
+
+ return {};
}, [albumArtistId, genreId]);
const providerValue = useMemo(() => {
@@ -48,7 +58,9 @@ const AlbumListRoute = () => {
-
+
+
+
);
diff --git a/src/renderer/features/genres/components/genre-list-header-filters.tsx b/src/renderer/features/genres/components/genre-list-header-filters.tsx
index 28e7592c2..dd0b36198 100644
--- a/src/renderer/features/genres/components/genre-list-header-filters.tsx
+++ b/src/renderer/features/genres/components/genre-list-header-filters.tsx
@@ -1,6 +1,6 @@
import { GENRE_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
-import { ListFilters } from '/@/renderer/features/shared/components/list-filters';
+import { ListFiltersModal } from '/@/renderer/features/shared/components/list-filters';
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
@@ -24,7 +24,7 @@ export const GenreListHeaderFilters = () => {
defaultSortOrder={SortOrder.ASC}
listKey={ItemListKey.GENRE}
/>
-
+
diff --git a/src/renderer/features/playlists/components/playlist-list-header-filters.tsx b/src/renderer/features/playlists/components/playlist-list-header-filters.tsx
index d6a12d9b8..f907ef630 100644
--- a/src/renderer/features/playlists/components/playlist-list-header-filters.tsx
+++ b/src/renderer/features/playlists/components/playlist-list-header-filters.tsx
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { PLAYLIST_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
import { CreatePlaylistForm } from '/@/renderer/features/playlists/components/create-playlist-form';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
-import { ListFilters } from '/@/renderer/features/shared/components/list-filters';
+import { ListFiltersModal } from '/@/renderer/features/shared/components/list-filters';
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
@@ -39,7 +39,7 @@ export const PlaylistListHeaderFilters = () => {
defaultSortOrder={SortOrder.ASC}
listKey={ItemListKey.PLAYLIST}
/>
-
+
diff --git a/src/renderer/features/shared/components/list-filters.tsx b/src/renderer/features/shared/components/list-filters.tsx
index 534ef7694..4821c1d8d 100644
--- a/src/renderer/features/shared/components/list-filters.tsx
+++ b/src/renderer/features/shared/components/list-filters.tsx
@@ -17,7 +17,7 @@ interface ListFiltersProps {
itemType: LibraryItem;
}
-export const ListFilters = ({ isActive, itemType }: ListFiltersProps) => {
+export const ListFiltersModal = ({ isActive, itemType }: ListFiltersProps) => {
const { t } = useTranslation();
const server = useCurrentServer();
@@ -41,6 +41,14 @@ export const ListFilters = ({ isActive, itemType }: ListFiltersProps) => {
);
};
+export const ListFilters = ({ itemType }: ListFiltersProps) => {
+ const server = useCurrentServer();
+ const serverType = server.type;
+ const FilterComponent = FILTERS[serverType][itemType];
+
+ return ;
+};
+
const FILTERS = {
[ServerType.JELLYFIN]: {
[LibraryItem.ALBUM]: JellyfinAlbumFilters,
diff --git a/src/renderer/features/shared/components/list-with-sidebar-container.module.css b/src/renderer/features/shared/components/list-with-sidebar-container.module.css
new file mode 100644
index 000000000..d4d88bb16
--- /dev/null
+++ b/src/renderer/features/shared/components/list-with-sidebar-container.module.css
@@ -0,0 +1,31 @@
+.container {
+ position: relative;
+ display: flex;
+ flex-direction: row;
+ width: 100%;
+ height: 100%;
+ container-type: inline-size;
+ overflow: hidden;
+}
+
+.sidebar-container {
+ position: relative;
+ flex-shrink: 0;
+ width: 300px;
+ min-width: 300px;
+ max-width: 300px;
+ height: 100%;
+ overflow: hidden;
+ border-right: 1px solid var(--theme-colors-border);
+}
+
+.content-container {
+ position: relative;
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ width: 100%;
+ min-width: 0;
+ height: 100%;
+ overflow: hidden;
+}
diff --git a/src/renderer/features/shared/components/list-with-sidebar-container.tsx b/src/renderer/features/shared/components/list-with-sidebar-container.tsx
new file mode 100644
index 000000000..a5f42dded
--- /dev/null
+++ b/src/renderer/features/shared/components/list-with-sidebar-container.tsx
@@ -0,0 +1,100 @@
+import { motion } from 'motion/react';
+import { createContext, ReactNode, useContext, useMemo, useRef } from 'react';
+
+import styles from './list-with-sidebar-container.module.css';
+
+import { useContainerQuery } from '/@/renderer/hooks';
+import { animationProps } from '/@/shared/components/animations/animation-props';
+import { Portal } from '/@/shared/components/portal/portal';
+
+interface ListWithSidebarContainerContextValue {
+ showSidebar: boolean;
+ sidebarRef: React.RefObject;
+}
+
+const ListWithSidebarContainerContext = createContext(
+ null,
+);
+
+interface ListWithSidebarContainerProps {
+ children: ReactNode;
+ sidebarBreakpoint?: number;
+}
+
+interface SidebarPortalProps {
+ children: ReactNode;
+}
+
+interface SidebarProps {
+ children: ReactNode;
+}
+
+function Sidebar({ children }: SidebarProps) {
+ const context = useContext(ListWithSidebarContainerContext);
+
+ if (!context) {
+ throw new Error('Sidebar must be used within ResponsiveAnimatedPage');
+ }
+
+ if (!context.showSidebar || !context.sidebarRef?.current) {
+ return null;
+ }
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+function SidebarPortal({ children }: SidebarPortalProps) {
+ const context = useContext(ListWithSidebarContainerContext);
+
+ if (!context) {
+ throw new Error('SidebarPortal must be used within ResponsiveAnimatedPage');
+ }
+
+ if (!context.showSidebar || !context.sidebarRef?.current) {
+ return null;
+ }
+
+ return {children};
+}
+
+export const ListWithSidebarContainer = ({
+ children,
+ sidebarBreakpoint,
+}: ListWithSidebarContainerProps) => {
+ const sidebarRef = useRef(null);
+ const { isLg, ref: containerQueryRef } = useContainerQuery({
+ lg: sidebarBreakpoint,
+ });
+
+ const showSidebar = isLg;
+
+ const contextValue = useMemo(
+ () => ({
+ showSidebar,
+ sidebarRef,
+ }),
+ [showSidebar],
+ );
+
+ return (
+
+
+
+ );
+};
+
+ListWithSidebarContainer.Sidebar = Sidebar;
+ListWithSidebarContainer.SidebarPortal = SidebarPortal;
diff --git a/src/renderer/features/shared/utils.ts b/src/renderer/features/shared/utils.ts
index 15eeccbd1..3056ef62d 100644
--- a/src/renderer/features/shared/utils.ts
+++ b/src/renderer/features/shared/utils.ts
@@ -40,7 +40,7 @@ enum AlbumFilterKeys {
ARTIST_IDS = 'artistIds',
COMPILATION = 'compilation',
FAVORITE = 'favorite',
- GENRE_ID = 'genreId',
+ GENRE_ID = 'genreIds',
HAS_RATING = 'hasRating',
MAX_YEAR = 'maxYear',
MIN_YEAR = 'minYear',
diff --git a/src/renderer/features/songs/components/song-list-header-filters.tsx b/src/renderer/features/songs/components/song-list-header-filters.tsx
index d5214ee57..6b7e707cf 100644
--- a/src/renderer/features/songs/components/song-list-header-filters.tsx
+++ b/src/renderer/features/songs/components/song-list-header-filters.tsx
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { SONG_TABLE_COLUMNS } from '/@/renderer/components/item-list/item-table-list/default-columns';
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
-import { ListFilters } from '/@/renderer/features/shared/components/list-filters';
+import { ListFiltersModal } from '/@/renderer/features/shared/components/list-filters';
import { ListRefreshButton } from '/@/renderer/features/shared/components/list-refresh-button';
import { ListSortByDropdown } from '/@/renderer/features/shared/components/list-sort-by-dropdown';
import { ListSortOrderToggleButton } from '/@/renderer/features/shared/components/list-sort-order-toggle-button';
@@ -65,7 +65,7 @@ export const SongListHeaderFilters = ({ toggleGenreTarget }: { toggleGenreTarget
defaultSortOrder={SortOrder.ASC}
listKey={ItemListKey.SONG}
/>
-
+
diff --git a/src/shared/components/select/select.tsx b/src/shared/components/select/select.tsx
index 06973fcc2..de012025d 100644
--- a/src/shared/components/select/select.tsx
+++ b/src/shared/components/select/select.tsx
@@ -31,7 +31,8 @@ export const Select = ({
section: styles.section,
...classNames,
}}
- clearable={false}
+ clearable={clearable}
+ spellCheck={false}
style={{ maxWidth, width }}
variant={variant}
withCheckIcon={false}