diff --git a/src/renderer/features/shared/components/list-music-folder-dropdown.tsx b/src/renderer/features/shared/components/list-music-folder-dropdown.tsx
new file mode 100644
index 000000000..b0bb5bd14
--- /dev/null
+++ b/src/renderer/features/shared/components/list-music-folder-dropdown.tsx
@@ -0,0 +1,65 @@
+import { useLocalStorage } from '@mantine/hooks';
+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 { useCurrentServer } from '/@/renderer/store';
+import { DropdownMenu } from '/@/shared/components/dropdown-menu/dropdown-menu';
+import { ItemListKey } from '/@/shared/types/types';
+
+interface ListMusicFolderDropdownProps {
+ listKey: ItemListKey;
+}
+
+export const ListMusicFolderDropdown = ({ listKey }: ListMusicFolderDropdownProps) => {
+ const server = useCurrentServer();
+ const { data: musicFolders } = useQuery(
+ sharedQueries.musicFolders({ query: null, serverId: server.id }),
+ );
+
+ const [persisted, setPersisted] = useLocalStorage({
+ defaultValue: '',
+ key: getPersistenceKey(listKey),
+ });
+
+ const [musicFolderId, setMusicFolderId] = useQueryState(
+ getPersistenceKey(listKey),
+ parseAsString.withDefault(persisted || ''),
+ );
+
+ const handleSetMusicFolder = (e: string) => {
+ if (e === musicFolderId) {
+ setMusicFolderId('');
+ setPersisted('');
+ return;
+ }
+
+ setMusicFolderId(e);
+ setPersisted(e);
+ };
+
+ return (
+
+
+
+
+
+ {musicFolders?.items.map((folder) => (
+ handleSetMusicFolder(folder.id)}
+ value={folder.id}
+ >
+ {folder.name}
+
+ ))}
+
+
+ );
+};
+
+const getPersistenceKey = (listKey: ItemListKey) => {
+ return `list-${listKey}-musicFolder`;
+};
diff --git a/src/renderer/features/shared/components/list-refresh-button.tsx b/src/renderer/features/shared/components/list-refresh-button.tsx
new file mode 100644
index 000000000..e7d656b99
--- /dev/null
+++ b/src/renderer/features/shared/components/list-refresh-button.tsx
@@ -0,0 +1,17 @@
+import { useCallback } from 'react';
+
+import { eventEmitter } from '/@/renderer/events/event-emitter';
+import { RefreshButton } from '/@/renderer/features/shared/components/refresh-button';
+import { ItemListKey } from '/@/shared/types/types';
+
+interface ListRefreshButtonProps {
+ listKey: ItemListKey;
+}
+
+export const ListRefreshButton = ({ listKey }: ListRefreshButtonProps) => {
+ const handleRefresh = useCallback(() => {
+ eventEmitter.emit('ITEM_LIST_REFRESH', { key: listKey });
+ }, [listKey]);
+
+ return ;
+};
diff --git a/src/renderer/features/shared/components/list-sort-by-dropdown.tsx b/src/renderer/features/shared/components/list-sort-by-dropdown.tsx
new file mode 100644
index 000000000..d9196084f
--- /dev/null
+++ b/src/renderer/features/shared/components/list-sort-by-dropdown.tsx
@@ -0,0 +1,226 @@
+import { useLocalStorage } from '@mantine/hooks';
+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 { AlbumListSort, LibraryItem, ServerType, SortOrder } from '/@/shared/types/domain-types';
+import { ItemListKey } from '/@/shared/types/types';
+
+interface ListSortByDropdownProps {
+ defaultSortByValue: string;
+ itemType: LibraryItem;
+ listKey: ItemListKey;
+ onChange?: (value: string) => void;
+ target?: React.ReactNode;
+}
+
+export const ListSortByDropdown = ({
+ defaultSortByValue,
+ itemType,
+ listKey,
+ onChange,
+ target,
+}: ListSortByDropdownProps) => {
+ const server = useCurrentServer();
+
+ const [persisted, setPersisted] = useLocalStorage({
+ defaultValue: defaultSortByValue,
+ key: getPersistenceKey(listKey),
+ });
+
+ const [sortBy, setSortBy] = useQueryState(
+ FILTER_KEYS.SORT_BY,
+ parseAsString.withDefault(persisted || defaultSortByValue),
+ );
+
+ const sortByLabel =
+ (itemType && FILTERS[itemType][server.type].find((f) => f.value === sortBy)?.name) || '—';
+
+ const handleSortByChange = (sortBy: string) => {
+ setSortBy(sortBy);
+ setPersisted(sortBy);
+ onChange?.(sortBy);
+ };
+
+ return (
+
+
+ {target ? target : }
+
+
+ {FILTERS[itemType][server.type].map((f) => (
+ handleSortByChange(f.value)}
+ value={f.value}
+ >
+ {f.name}
+
+ ))}
+
+
+ );
+};
+
+const ALBUM_LIST_FILTERS: Partial<
+ Record>
+> = {
+ [ServerType.JELLYFIN]: [
+ {
+ defaultOrder: SortOrder.ASC,
+ name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
+ value: AlbumListSort.ALBUM_ARTIST,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.communityRating', { postProcess: 'titleCase' }),
+ value: AlbumListSort.COMMUNITY_RATING,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.criticRating', { postProcess: 'titleCase' }),
+ value: AlbumListSort.CRITIC_RATING,
+ },
+ {
+ defaultOrder: SortOrder.ASC,
+ name: i18n.t('filter.name', { postProcess: 'titleCase' }),
+ value: AlbumListSort.NAME,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.playCount', { postProcess: 'titleCase' }),
+ value: AlbumListSort.PLAY_COUNT,
+ },
+ {
+ defaultOrder: SortOrder.ASC,
+ name: i18n.t('filter.random', { postProcess: 'titleCase' }),
+ value: AlbumListSort.RANDOM,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
+ value: AlbumListSort.RECENTLY_ADDED,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.releaseDate', { postProcess: 'titleCase' }),
+ value: AlbumListSort.RELEASE_DATE,
+ },
+ ],
+ [ServerType.NAVIDROME]: [
+ {
+ defaultOrder: SortOrder.ASC,
+ name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
+ value: AlbumListSort.ALBUM_ARTIST,
+ },
+ {
+ defaultOrder: SortOrder.ASC,
+ name: i18n.t('filter.artist', { postProcess: 'titleCase' }),
+ value: AlbumListSort.ARTIST,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.duration', { postProcess: 'titleCase' }),
+ value: AlbumListSort.DURATION,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
+ value: AlbumListSort.PLAY_COUNT,
+ },
+ {
+ defaultOrder: SortOrder.ASC,
+ name: i18n.t('filter.name', { postProcess: 'titleCase' }),
+ value: AlbumListSort.NAME,
+ },
+ {
+ defaultOrder: SortOrder.ASC,
+ name: i18n.t('filter.random', { postProcess: 'titleCase' }),
+ value: AlbumListSort.RANDOM,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.rating', { postProcess: 'titleCase' }),
+ value: AlbumListSort.RATING,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
+ value: AlbumListSort.RECENTLY_ADDED,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
+ value: AlbumListSort.RECENTLY_PLAYED,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.songCount', { postProcess: 'titleCase' }),
+ value: AlbumListSort.SONG_COUNT,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.favorited', { postProcess: 'titleCase' }),
+ value: AlbumListSort.FAVORITED,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
+ value: AlbumListSort.YEAR,
+ },
+ ],
+ [ServerType.SUBSONIC]: [
+ {
+ defaultOrder: SortOrder.ASC,
+ name: i18n.t('filter.albumArtist', { postProcess: 'titleCase' }),
+ value: AlbumListSort.ALBUM_ARTIST,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.mostPlayed', { postProcess: 'titleCase' }),
+ value: AlbumListSort.PLAY_COUNT,
+ },
+ {
+ defaultOrder: SortOrder.ASC,
+ name: i18n.t('filter.name', { postProcess: 'titleCase' }),
+ value: AlbumListSort.NAME,
+ },
+ {
+ defaultOrder: SortOrder.ASC,
+ name: i18n.t('filter.random', { postProcess: 'titleCase' }),
+ value: AlbumListSort.RANDOM,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.recentlyAdded', { postProcess: 'titleCase' }),
+ value: AlbumListSort.RECENTLY_ADDED,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.recentlyPlayed', { postProcess: 'titleCase' }),
+ value: AlbumListSort.RECENTLY_PLAYED,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.favorited', { postProcess: 'titleCase' }),
+ value: AlbumListSort.FAVORITED,
+ },
+ {
+ defaultOrder: SortOrder.DESC,
+ name: i18n.t('filter.releaseYear', { postProcess: 'titleCase' }),
+ value: AlbumListSort.YEAR,
+ },
+ ],
+};
+
+const FILTERS: Partial> = {
+ [LibraryItem.ALBUM]: ALBUM_LIST_FILTERS,
+};
+
+const getPersistenceKey = (listKey: ItemListKey) => {
+ return `item_list_${listKey}-${FILTER_KEYS.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
new file mode 100644
index 000000000..84d3e7232
--- /dev/null
+++ b/src/renderer/features/shared/components/list-sort-order-toggle-button.tsx
@@ -0,0 +1,37 @@
+import { useLocalStorage } from '@mantine/hooks';
+import { parseAsString, useQueryState } from 'nuqs';
+
+import { OrderToggleButton } from '/@/renderer/features/shared/components/order-toggle-button';
+import { FILTER_KEYS } from '/@/renderer/features/shared/utils';
+import { SortOrder } from '/@/shared/types/domain-types';
+import { ItemListKey } from '/@/shared/types/types';
+
+interface ListSortOrderToggleButtonProps {
+ listKey: ItemListKey;
+}
+
+export const ListSortOrderToggleButton = ({ listKey }: ListSortOrderToggleButtonProps) => {
+ const [persisted, setPersisted] = useLocalStorage({
+ defaultValue: SortOrder.ASC,
+ key: getPersistenceKey(listKey),
+ });
+
+ const [sortOrder, setSortOrder] = useQueryState(
+ FILTER_KEYS.SORT_ORDER,
+ parseAsString.withDefault(persisted || SortOrder.ASC),
+ );
+
+ const handleToggleSortOrder = () => {
+ const newSortOrder = sortOrder === SortOrder.ASC ? SortOrder.DESC : SortOrder.ASC;
+ setSortOrder(newSortOrder);
+ setPersisted(newSortOrder);
+ };
+
+ return (
+
+ );
+};
+
+const getPersistenceKey = (listKey: ItemListKey) => {
+ return `item_list_${listKey}-${FILTER_KEYS.SORT_ORDER}`;
+};