diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 990c6cc47..c9ba70183 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -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" } } diff --git a/src/renderer/features/shared/components/filter-bar.module.css b/src/renderer/features/shared/components/filter-bar.module.css index 97c1d40a8..ded350238 100644 --- a/src/renderer/features/shared/components/filter-bar.module.css +++ b/src/renderer/features/shared/components/filter-bar.module.css @@ -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); } diff --git a/src/renderer/features/shared/components/folder-button.tsx b/src/renderer/features/shared/components/folder-button.tsx index e493b4110..2391a1f01 100644 --- a/src/renderer/features/shared/components/folder-button.tsx +++ b/src/renderer/features/shared/components/folder-button.tsx @@ -13,7 +13,7 @@ export const FolderButton = ({ isActive, ...props }: FolderButtonProps) => { { 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" /> @@ -192,9 +193,5 @@ export const GridConfig = ({ extraOptions, listKey }: GridConfigProps) => { ]; }, [list, t, grid, extraOptions, setList, listKey]); - return ( - <> - - - ); + return ; }; diff --git a/src/renderer/features/shared/components/list-filters.tsx b/src/renderer/features/shared/components/list-filters.tsx new file mode 100644 index 000000000..07dbecf26 --- /dev/null +++ b/src/renderer/features/shared/components/list-filters.tsx @@ -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 ( + <> + + + + + + ); +}; + +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, + }, +}; diff --git a/src/renderer/features/shared/components/list-music-folder-dropdown.tsx b/src/renderer/features/shared/components/list-music-folder-dropdown.tsx index f12190f06..6df701ea2 100644 --- a/src/renderer/features/shared/components/list-music-folder-dropdown.tsx +++ b/src/renderer/features/shared/components/list-music-folder-dropdown.tsx @@ -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}`; }; diff --git a/src/renderer/features/shared/components/list-sort-by-dropdown.tsx b/src/renderer/features/shared/components/list-sort-by-dropdown.tsx index b066f1287..d70622c78 100644 --- a/src/renderer/features/shared/components/list-sort-by-dropdown.tsx +++ b/src/renderer/features/shared/components/list-sort-by-dropdown.tsx @@ -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> = { - [LibraryItem.ALBUM]: ALBUM_LIST_FILTERS, +const SONG_LIST_FILTERS: Partial< + Record> +> = { + [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> = { + [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}`; }; diff --git a/src/renderer/features/shared/components/list-sort-order-toggle-button.tsx b/src/renderer/features/shared/components/list-sort-order-toggle-button.tsx index 210bbac70..19044af62 100644 --- a/src/renderer/features/shared/components/list-sort-order-toggle-button.tsx +++ b/src/renderer/features/shared/components/list-sort-order-toggle-button.tsx @@ -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}`; }; diff --git a/src/renderer/features/shared/components/table-config.tsx b/src/renderer/features/shared/components/table-config.tsx index 637a02510..994a1dbb3 100644 --- a/src/renderer/features/shared/components/table-config.tsx +++ b/src/renderer/features/shared/components/table-config.tsx @@ -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(() => { diff --git a/src/renderer/features/shared/hooks/use-music-folder-id-filter.ts b/src/renderer/features/shared/hooks/use-music-folder-id-filter.ts new file mode 100644 index 000000000..99f6d789f --- /dev/null +++ b/src/renderer/features/shared/hooks/use-music-folder-id-filter.ts @@ -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, + }; +}; diff --git a/src/renderer/features/shared/hooks/use-search-term-filter.ts b/src/renderer/features/shared/hooks/use-search-term-filter.ts new file mode 100644 index 000000000..9392a180e --- /dev/null +++ b/src/renderer/features/shared/hooks/use-search-term-filter.ts @@ -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, + }; +}; diff --git a/src/renderer/features/shared/hooks/use-sort-by-filter.ts b/src/renderer/features/shared/hooks/use-sort-by-filter.ts new file mode 100644 index 000000000..ae065bba8 --- /dev/null +++ b/src/renderer/features/shared/hooks/use-sort-by-filter.ts @@ -0,0 +1,15 @@ +import { parseAsString, useQueryState } from 'nuqs'; + +import { FILTER_KEYS } from '/@/renderer/features/shared/utils'; + +export const useSortByFilter = (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, + }; +}; diff --git a/src/renderer/features/shared/hooks/use-sort-order-filter.ts b/src/renderer/features/shared/hooks/use-sort-order-filter.ts new file mode 100644 index 000000000..5d67570ae --- /dev/null +++ b/src/renderer/features/shared/hooks/use-sort-order-filter.ts @@ -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, + }; +}; diff --git a/src/renderer/features/shared/utils.ts b/src/renderer/features/shared/utils.ts index 705283d67..1a3a1add1 100644 --- a/src/renderer/features/shared/utils.ts +++ b/src/renderer/features/shared/utils.ts @@ -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, };