update shared filter components

This commit is contained in:
jeffvli
2025-10-13 18:25:29 -07:00
parent f6a7af2b12
commit cd578db53a
14 changed files with 359 additions and 56 deletions
+25 -4
View File
@@ -127,7 +127,8 @@
"year": "year",
"yes": "yes",
"explicit": "explicit",
"clean": "clean"
"clean": "clean",
"tableColumns": "table columns"
},
"entity": {
"album_one": "album",
@@ -826,13 +827,33 @@
"config": {
"general": {
"autoFitColumns": "auto fit columns",
"autosize": "autosize",
"moveUp": "move up",
"moveDown": "move down",
"pinToLeft": "pin to left",
"pinToRight": "pin to right",
"alignLeft": "align left",
"alignCenter": "align center",
"alignRight": "align right",
"followCurrentSong": "follow current song",
"displayType": "display type",
"gap": "$t(common.gap)",
"itemGap": "item gap (px)",
"itemSize": "item size (px)",
"itemsPerRow": "items per row",
"size": "$t(common.size)",
"tableColumns": "table columns"
"size_default": "default",
"size_compact": "compact",
"size_large": "large",
"tableColumns": "table columns",
"pagination": "pagination",
"pagination_itemsPerPage": "items per page",
"pagination_infinite": "infinite",
"pagination_paginate": "paginated",
"alternateRowColors": "alternate row colors",
"horizontalBorders": "horizontal borders",
"rowHoverHighlight": "row hover highlight",
"verticalBorders": "vertical borders"
},
"label": {
"actions": "$t(common.action_other)",
@@ -849,6 +870,8 @@
"duration": "$t(common.duration)",
"favorite": "$t(common.favorite)",
"genre": "$t(entity.genre_one)",
"genreBadge": "$t(entity.genre_one) (badges)",
"image": "image",
"lastPlayed": "last played",
"note": "$t(common.note)",
"owner": "$t(common.owner)",
@@ -865,10 +888,8 @@
"year": "$t(common.year)"
},
"view": {
"card": "card",
"grid": "grid",
"list": "list",
"poster": "poster",
"table": "table"
}
}
@@ -1,12 +1,5 @@
.filter-bar {
z-index: 1;
padding: var(--theme-spacing-md) var(--theme-spacing-sm);
@mixin dark {
box-shadow: 0 5px 15px rgb(0 0 0 / 65%);
}
@mixin light {
box-shadow: 0 2px 0 var(--theme-colors-border);
}
padding: var(--theme-spacing-sm);
border-bottom: 1px solid var(--theme-colors-border);
}
@@ -13,7 +13,7 @@ export const FolderButton = ({ isActive, ...props }: FolderButtonProps) => {
<ActionIcon
icon="folder"
iconProps={{
fill: isActive ? 'primary' : undefined,
color: isActive ? 'primary' : undefined,
size: 'lg',
...props.iconProps,
}}
@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { ListConfigTable } from '/@/renderer/features/shared/components/list-config-menu';
import {
DataGridProps,
DataListProps,
ItemListSettings,
useSettingsStore,
useSettingsStoreActions,
} from '/@/renderer/store';
@@ -28,8 +28,8 @@ type GridConfigProps = {
export const GridConfig = ({ extraOptions, listKey }: GridConfigProps) => {
const { t } = useTranslation();
const list = useSettingsStore((state) => state.lists[listKey]) as DataListProps;
const grid = useSettingsStore((state) => state.lists[listKey].grid) as DataGridProps;
const list = useSettingsStore((state) => state.lists[listKey]) as ItemListSettings;
const grid = list.grid as DataGridProps;
const { setList } = useSettingsStoreActions();
const options = useMemo(() => {
@@ -182,6 +182,7 @@ export const GridConfig = ({ extraOptions, listKey }: GridConfigProps) => {
grid: { itemsPerRowEnabled: e.target.checked },
})
}
pr="md"
size="xs"
/>
</Group>
@@ -192,9 +193,5 @@ export const GridConfig = ({ extraOptions, listKey }: GridConfigProps) => {
];
}, [list, t, grid, extraOptions, setList, listKey]);
return (
<>
<ListConfigTable options={options} />
</>
);
return <ListConfigTable options={options} />;
};
@@ -0,0 +1,53 @@
import { useTranslation } from 'react-i18next';
import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters';
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
import { SubsonicAlbumFilters } from '/@/renderer/features/albums/components/subsonic-album-filters';
import { FilterButton } from '/@/renderer/features/shared/components/filter-button';
import { JellyfinSongFilters } from '/@/renderer/features/songs/components/jellyfin-song-filters';
import { NavidromeSongFilters } from '/@/renderer/features/songs/components/navidrome-song-filters';
import { SubsonicSongFilters } from '/@/renderer/features/songs/components/subsonic-song-filter';
import { useCurrentServer } from '/@/renderer/store';
import { Modal } from '/@/shared/components/modal/modal';
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
import { LibraryItem, ServerType } from '/@/shared/types/domain-types';
interface ListFiltersProps {
isActive?: boolean;
itemType: LibraryItem;
}
export const ListFilters = ({ isActive, itemType }: ListFiltersProps) => {
const { t } = useTranslation();
const server = useCurrentServer();
const serverType = server.type;
const FilterComponent = FILTERS[serverType][itemType];
const [isOpen, handlers] = useDisclosure(false);
return (
<>
<FilterButton isActive={isActive} onClick={handlers.toggle} />
<Modal handlers={handlers} opened={isOpen} title={t('common.filters')}>
<FilterComponent />
</Modal>
</>
);
};
const FILTERS = {
[ServerType.JELLYFIN]: {
[LibraryItem.ALBUM]: JellyfinAlbumFilters,
[LibraryItem.SONG]: JellyfinSongFilters,
},
[ServerType.NAVIDROME]: {
[LibraryItem.ALBUM]: NavidromeAlbumFilters,
[LibraryItem.SONG]: NavidromeSongFilters,
},
[ServerType.SUBSONIC]: {
[LibraryItem.ALBUM]: SubsonicAlbumFilters,
[LibraryItem.SONG]: SubsonicSongFilters,
},
};
@@ -1,12 +1,13 @@
import { useQuery } from '@tanstack/react-query';
import { parseAsString, useQueryState } from 'nuqs';
import { sharedQueries } from '/@/renderer/features/shared/api/shared-api';
import { FolderButton } from '/@/renderer/features/shared/components/folder-button';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { useCurrentServer } from '/@/renderer/store';
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
import { ItemListKey } from '/@/shared/types/types';
import { useMusicFolderIdFilter } from '/@/renderer/features/shared/hooks/use-music-folder-id-filter';
interface ListMusicFolderDropdownProps {
listKey: ItemListKey;
@@ -20,13 +21,10 @@ export const ListMusicFolderDropdown = ({ listKey }: ListMusicFolderDropdownProp
const [persisted, setPersisted] = useLocalStorage({
defaultValue: '',
key: getPersistenceKey(listKey),
key: getPersistenceKey(server.id, listKey),
});
const [musicFolderId, setMusicFolderId] = useQueryState(
getPersistenceKey(listKey),
parseAsString.withDefault(persisted || ''),
);
const { musicFolderId, setMusicFolderId } = useMusicFolderIdFilter(persisted);
const handleSetMusicFolder = (e: string) => {
if (e === musicFolderId) {
@@ -60,6 +58,6 @@ export const ListMusicFolderDropdown = ({ listKey }: ListMusicFolderDropdownProp
);
};
const getPersistenceKey = (listKey: ItemListKey) => {
return `list-${listKey}-musicFolder`;
const getPersistenceKey = (serverId: string, listKey: ItemListKey) => {
return `${serverId}-list-${listKey}-${FILTER_KEYS.SHARED.MUSIC_FOLDER_ID}`;
};
@@ -1,13 +1,18 @@
import { parseAsString, useQueryState } from 'nuqs';
import i18n from '/@/i18n/i18n';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { useCurrentServer } from '/@/renderer/store';
import { Button } from '/@/shared/components/button/button';
import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
import { AlbumListSort, LibraryItem, ServerType, SortOrder } from '/@/shared/types/domain-types';
import {
AlbumListSort,
LibraryItem,
ServerType,
SongListSort,
SortOrder,
} from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
import { useSortByFilter } from '/@/renderer/features/shared/hooks/use-sort-by-filter';
interface ListSortByDropdownProps {
defaultSortByValue: string;
@@ -28,13 +33,10 @@ export const ListSortByDropdown = ({
const [persisted, setPersisted] = useLocalStorage({
defaultValue: defaultSortByValue,
key: getPersistenceKey(listKey),
key: getPersistenceKey(server.id, listKey),
});
const [sortBy, setSortBy] = useQueryState(
FILTER_KEYS.SORT_BY,
parseAsString.withDefault(persisted || defaultSortByValue),
);
const { sortBy, setSortBy } = useSortByFilter(persisted || defaultSortByValue);
const sortByLabel =
(itemType && FILTERS[itemType][server.type].find((f) => f.value === sortBy)?.name) || '—';
@@ -217,10 +219,157 @@ const ALBUM_LIST_FILTERS: Partial<
],
};
const FILTERS: Partial<Record<LibraryItem, any>> = {
[LibraryItem.ALBUM]: ALBUM_LIST_FILTERS,
const SONG_LIST_FILTERS: Partial<
Record<ServerType, Array<{ defaultOrder: SortOrder; name: string; value: string }>>
> = {
[ServerType.JELLYFIN]: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.album', { postProcess: 'titleCase' }),
value: SongListSort.ALBUM,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
value: SongListSort.ALBUM_ARTIST,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
value: SongListSort.ARTIST,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
value: SongListSort.DURATION,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.playCount', { postProcess: 'titleCase' }),
value: SongListSort.PLAY_COUNT,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: SongListSort.NAME,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: SongListSort.RANDOM,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: SongListSort.RECENTLY_ADDED,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
value: SongListSort.RECENTLY_PLAYED,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.releaseDate', { postProcess: 'titleCase' }),
value: SongListSort.RELEASE_DATE,
},
],
[ServerType.NAVIDROME]: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.album', { postProcess: 'titleCase' }),
value: SongListSort.ALBUM,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
value: SongListSort.ALBUM_ARTIST,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
value: SongListSort.ARTIST,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.bpm', { postProcess: 'titleCase' }),
value: SongListSort.BPM,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('common.channel', { count: 2, postProcess: 'titleCase' }),
value: SongListSort.CHANNELS,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.comment', { postProcess: 'titleCase' }),
value: SongListSort.COMMENT,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
value: SongListSort.DURATION,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.isFavorited', { postProcess: 'titleCase' }),
value: SongListSort.FAVORITED,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.genre', { postProcess: 'titleCase' }),
value: SongListSort.GENRE,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: SongListSort.NAME,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.playCount', { postProcess: 'titleCase' }),
value: SongListSort.PLAY_COUNT,
},
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.random', { postProcess: 'titleCase' }),
value: SongListSort.RANDOM,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
value: SongListSort.RATING,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
value: SongListSort.RECENTLY_ADDED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
value: SongListSort.RECENTLY_PLAYED,
},
{
defaultOrder: SortOrder.DESC,
name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
value: SongListSort.YEAR,
},
],
[ServerType.SUBSONIC]: [
{
defaultOrder: SortOrder.ASC,
name: i18n.t('filter.name', { postProcess: 'titleCase' }),
value: SongListSort.NAME,
},
],
};
const getPersistenceKey = (listKey: ItemListKey) => {
return `item_list_${listKey}-${FILTER_KEYS.SORT_BY}`;
const FILTERS: Partial<Record<LibraryItem, any>> = {
[LibraryItem.ALBUM]: ALBUM_LIST_FILTERS,
[LibraryItem.SONG]: SONG_LIST_FILTERS,
};
const getPersistenceKey = (serverId: string, listKey: ItemListKey) => {
return `${serverId}-list-${listKey}-${FILTER_KEYS.SHARED.SORT_BY}`;
};
@@ -1,7 +1,7 @@
import { parseAsString, useQueryState } from 'nuqs';
import { OrderToggleButton } from '/@/renderer/features/shared/components/order-toggle-button';
import { useSortOrderFilter } from '/@/renderer/features/shared/hooks/use-sort-order-filter';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { useCurrentServer } from '/@/renderer/store';
import { useLocalStorage } from '/@/shared/hooks/use-local-storage';
import { SortOrder } from '/@/shared/types/domain-types';
import { ItemListKey } from '/@/shared/types/types';
@@ -11,15 +11,14 @@ interface ListSortOrderToggleButtonProps {
}
export const ListSortOrderToggleButton = ({ listKey }: ListSortOrderToggleButtonProps) => {
const server = useCurrentServer();
const [persisted, setPersisted] = useLocalStorage({
defaultValue: SortOrder.ASC,
key: getPersistenceKey(listKey),
key: getPersistenceKey(server.id, listKey),
});
const [sortOrder, setSortOrder] = useQueryState(
FILTER_KEYS.SORT_ORDER,
parseAsString.withDefault(persisted || SortOrder.ASC),
);
const { sortOrder, setSortOrder } = useSortOrderFilter(persisted || SortOrder.ASC);
const handleToggleSortOrder = () => {
const newSortOrder = sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
@@ -32,6 +31,6 @@ export const ListSortOrderToggleButton = ({ listKey }: ListSortOrderToggleButton
);
};
const getPersistenceKey = (listKey: ItemListKey) => {
return `item_list_${listKey}-${FILTER_KEYS.SORT_ORDER}`;
const getPersistenceKey = (serverId: string, listKey: ItemListKey) => {
return `${serverId}-list-${listKey}-${FILTER_KEYS.SHARED.SORT_ORDER}`;
};
@@ -12,7 +12,7 @@ import {
ListConfigBooleanControl,
ListConfigTable,
} from '/@/renderer/features/shared/components/list-config-menu';
import { DataListProps, useSettingsStore, useSettingsStoreActions } from '/@/renderer/store';
import { ItemListSettings, useSettingsStore, useSettingsStoreActions } from '/@/renderer/store';
import { ActionIcon, ActionIconGroup } from '/@/shared/components/action-icon/action-icon';
import { Badge } from '/@/shared/components/badge/badge';
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
@@ -40,7 +40,7 @@ interface TableConfigProps {
export const TableConfig = ({ extraOptions, listKey, tableColumnsData }: TableConfigProps) => {
const { t } = useTranslation();
const list = useSettingsStore((state) => state.lists[listKey]) as DataListProps;
const list = useSettingsStore((state) => state.lists[listKey]) as ItemListSettings;
const { setList } = useSettingsStoreActions();
const options = useMemo(() => {
@@ -0,0 +1,15 @@
import { parseAsString, useQueryState } from 'nuqs';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
export const useMusicFolderIdFilter = (defaultValue?: string) => {
const [musicFolderId, setMusicFolderId] = useQueryState(
FILTER_KEYS.SHARED.MUSIC_FOLDER_ID,
defaultValue ? parseAsString.withDefault(defaultValue) : parseAsString,
);
return {
[FILTER_KEYS.SHARED.MUSIC_FOLDER_ID]: musicFolderId ?? undefined,
setMusicFolderId,
};
};
@@ -0,0 +1,19 @@
import { useDebouncedValue } from '@mantine/hooks';
import { parseAsString, useQueryState } from 'nuqs';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
export const useSearchTermFilter = (defaultValue?: string) => {
const [searchTerm, setSearchTerm] = useQueryState(
FILTER_KEYS.SHARED.SEARCH_TERM,
defaultValue ? parseAsString.withDefault(defaultValue) : parseAsString,
);
const [debouncedSearchTerm] = useDebouncedValue(searchTerm, 300);
return {
[FILTER_KEYS.SHARED.SEARCH_TERM]: debouncedSearchTerm ?? undefined,
rawSearchTerm: searchTerm ?? undefined,
setSearchTerm,
};
};
@@ -0,0 +1,15 @@
import { parseAsString, useQueryState } from 'nuqs';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
export const useSortByFilter = <TSortBy>(defaultValue?: string) => {
const [sortBy, setSortBy] = useQueryState(
FILTER_KEYS.SHARED.SORT_BY,
defaultValue ? parseAsString.withDefault(defaultValue) : parseAsString,
);
return {
[FILTER_KEYS.SHARED.SORT_BY]: (sortBy as TSortBy) ?? undefined,
setSortBy,
};
};
@@ -0,0 +1,16 @@
import { parseAsString, useQueryState } from 'nuqs';
import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
import { SortOrder } from '/@/shared/types/domain-types';
export const useSortOrderFilter = (defaultValue?: string) => {
const [sortOrder, setSortOrder] = useQueryState(
FILTER_KEYS.SHARED.SORT_ORDER,
defaultValue ? parseAsString.withDefault(defaultValue) : parseAsString,
);
return {
[FILTER_KEYS.SHARED.SORT_ORDER]: (sortOrder as SortOrder) ?? undefined,
setSortOrder,
};
};
+31 -3
View File
@@ -1,3 +1,5 @@
import z from 'zod';
import i18n from '/@/i18n/i18n';
import { Play } from '/@/shared/types/types';
@@ -20,9 +22,35 @@ export const PLAY_TYPES = [
},
];
export const FILTER_KEYS = {
export const customFiltersSchema = z.record(z.string(), z.any());
enum AlbumFilterKeys {
_CUSTOM = '_custom',
ARTIST_IDS = 'artistIds',
COMPILATION = 'compilation',
FAVORITE = 'favorite',
GENRE_ID = 'genreId',
GENRES = 'genres',
HAS_RATING = 'hasRating',
MAX_YEAR = 'maxYear',
MIN_YEAR = 'minYear',
RECENTLY_PLAYED = 'recentlyPlayed',
}
enum SharedFilterKeys {
MUSIC_FOLDER_ID = 'musicFolderId',
SEARCH_TERM = 'searchTerm',
SORT_BY = 'sortBy',
SORT_ORDER = 'sortOrder',
}
const PaginationFilterKeys = {
CURRENT_PAGE: 'currentPage',
SCROLL_OFFSET: 'scrollOffset',
SORT_BY: 'sortBy',
SORT_ORDER: 'sortOrder',
};
export const FILTER_KEYS = {
ALBUM: AlbumFilterKeys,
PAGINATION: PaginationFilterKeys,
SHARED: SharedFilterKeys,
};