mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
allow user to unpin list sidebar
This commit is contained in:
@@ -5,11 +5,13 @@ import { ItemListKey } from '/@/shared/types/types';
|
|||||||
interface ListContextProps {
|
interface ListContextProps {
|
||||||
customFilters?: Record<string, unknown>;
|
customFilters?: Record<string, unknown>;
|
||||||
id?: string;
|
id?: string;
|
||||||
|
isSidebarOpen?: boolean;
|
||||||
isSmartPlaylist?: boolean;
|
isSmartPlaylist?: boolean;
|
||||||
itemCount?: number;
|
itemCount?: number;
|
||||||
listData?: unknown[];
|
listData?: unknown[];
|
||||||
mode?: 'edit' | 'view';
|
mode?: 'edit' | 'view';
|
||||||
pageKey: ItemListKey | string;
|
pageKey: ItemListKey | string;
|
||||||
|
setIsSidebarOpen?: (isSidebarOpen: boolean) => void;
|
||||||
setItemCount?: (itemCount: number) => void;
|
setItemCount?: (itemCount: number) => void;
|
||||||
setListData?: (items: unknown[]) => void;
|
setListData?: (items: unknown[]) => void;
|
||||||
setMode?: (mode: 'edit' | 'view') => void;
|
setMode?: (mode: 'edit' | 'view') => void;
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import { lazy, Suspense, useMemo } from 'react';
|
|||||||
|
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
||||||
import { ListFilters } from '/@/renderer/features/shared/components/list-filters';
|
import { ListFilters, ListFiltersTitle } from '/@/renderer/features/shared/components/list-filters';
|
||||||
import { ListWithSidebarContainer } from '/@/renderer/features/shared/components/list-with-sidebar-container';
|
import { ListWithSidebarContainer } from '/@/renderer/features/shared/components/list-with-sidebar-container';
|
||||||
import { ItemListSettings, useCurrentServer, useListSettings } from '/@/renderer/store';
|
import { ItemListSettings, useCurrentServer, useListSettings } from '/@/renderer/store';
|
||||||
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { AlbumListQuery, LibraryItem } from '/@/shared/types/domain-types';
|
import { AlbumListQuery, LibraryItem } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey, ListDisplayType, ListPaginationType } from '/@/shared/types/types';
|
import { ItemListKey, ListDisplayType, ListPaginationType } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -37,9 +38,12 @@ const AlbumListPaginatedTable = lazy(() =>
|
|||||||
const AlbumListFilters = () => {
|
const AlbumListFilters = () => {
|
||||||
return (
|
return (
|
||||||
<ListWithSidebarContainer.SidebarPortal>
|
<ListWithSidebarContainer.SidebarPortal>
|
||||||
|
<Stack h="100%">
|
||||||
|
<ListFiltersTitle />
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<ListFilters itemType={LibraryItem.ALBUM} />
|
<ListFilters itemType={LibraryItem.ALBUM} />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
</Stack>
|
||||||
</ListWithSidebarContainer.SidebarPortal>
|
</ListWithSidebarContainer.SidebarPortal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ import { useListContext } from '/@/renderer/context/list-context';
|
|||||||
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
||||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||||
import { ListDisplayTypeToggleButton } from '/@/renderer/features/shared/components/list-display-type-toggle-button';
|
import { ListDisplayTypeToggleButton } from '/@/renderer/features/shared/components/list-display-type-toggle-button';
|
||||||
import { ListFiltersModal } from '/@/renderer/features/shared/components/list-filters';
|
import {
|
||||||
|
isFilterValueSet,
|
||||||
|
ListFiltersModal,
|
||||||
|
} from '/@/renderer/features/shared/components/list-filters';
|
||||||
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 { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||||
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
|
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
|
||||||
import { GenreTarget, useGenreTarget, useSettingsStoreActions } from '/@/renderer/store';
|
import { GenreTarget, useGenreTarget, useSettingsStoreActions } from '/@/renderer/store';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
@@ -36,14 +40,29 @@ export const AlbumListHeaderFilters = ({ toggleGenreTarget }: { toggleGenreTarge
|
|||||||
}, [target, t]);
|
}, [target, t]);
|
||||||
|
|
||||||
const handleToggleGenreTarget = useCallback(() => {
|
const handleToggleGenreTarget = useCallback(() => {
|
||||||
// Clear all filter query states
|
|
||||||
albumFilters.clear();
|
albumFilters.clear();
|
||||||
songFilters.clear();
|
songFilters.clear();
|
||||||
|
|
||||||
// Toggle the genre target
|
|
||||||
setGenreBehavior(target === GenreTarget.ALBUM ? GenreTarget.TRACK : GenreTarget.ALBUM);
|
setGenreBehavior(target === GenreTarget.ALBUM ? GenreTarget.TRACK : GenreTarget.ALBUM);
|
||||||
}, [target, setGenreBehavior, albumFilters, songFilters]);
|
}, [target, setGenreBehavior, albumFilters, songFilters]);
|
||||||
|
|
||||||
|
const hasActiveFilters = useMemo(() => {
|
||||||
|
const query = albumFilters.query;
|
||||||
|
|
||||||
|
return Boolean(
|
||||||
|
isFilterValueSet(query[FILTER_KEYS.ALBUM._CUSTOM]) ||
|
||||||
|
isFilterValueSet(query[FILTER_KEYS.ALBUM.ARTIST_IDS]) ||
|
||||||
|
query[FILTER_KEYS.ALBUM.COMPILATION] !== undefined ||
|
||||||
|
query[FILTER_KEYS.ALBUM.FAVORITE] !== undefined ||
|
||||||
|
isFilterValueSet(query[FILTER_KEYS.ALBUM.GENRE_ID]) ||
|
||||||
|
query[FILTER_KEYS.ALBUM.HAS_RATING] !== undefined ||
|
||||||
|
isFilterValueSet(query[FILTER_KEYS.ALBUM.MAX_YEAR]) ||
|
||||||
|
isFilterValueSet(query[FILTER_KEYS.ALBUM.MIN_YEAR]) ||
|
||||||
|
query[FILTER_KEYS.ALBUM.RECENTLY_PLAYED] !== undefined ||
|
||||||
|
isFilterValueSet(query[FILTER_KEYS.SHARED.SEARCH_TERM]),
|
||||||
|
);
|
||||||
|
}, [albumFilters.query]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justify="space-between">
|
<Flex justify="space-between">
|
||||||
<Group gap="sm" w="100%">
|
<Group gap="sm" w="100%">
|
||||||
@@ -69,7 +88,7 @@ export const AlbumListHeaderFilters = ({ toggleGenreTarget }: { toggleGenreTarge
|
|||||||
defaultSortOrder={SortOrder.ASC}
|
defaultSortOrder={SortOrder.ASC}
|
||||||
listKey={pageKey as ItemListKey}
|
listKey={pageKey as ItemListKey}
|
||||||
/>
|
/>
|
||||||
<ListFiltersModal itemType={LibraryItem.ALBUM} />
|
<ListFiltersModal isActive={hasActiveFilters} itemType={LibraryItem.ALBUM} />
|
||||||
<ListRefreshButton listKey={pageKey as ItemListKey} />
|
<ListRefreshButton listKey={pageKey as ItemListKey} />
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { AlbumListHeader } from '/@/renderer/features/albums/components/album-li
|
|||||||
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
import { AnimatedPage } from '/@/renderer/features/shared/components/animated-page';
|
||||||
import { ListWithSidebarContainer } from '/@/renderer/features/shared/components/list-with-sidebar-container';
|
import { ListWithSidebarContainer } from '/@/renderer/features/shared/components/list-with-sidebar-container';
|
||||||
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||||
|
import { usePageSidebar } from '/@/renderer/store/app.store';
|
||||||
import { AlbumListQuery } from '/@/shared/types/domain-types';
|
import { AlbumListQuery } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ const AlbumListRoute = () => {
|
|||||||
const pageKey = getPageKey({ albumArtistId, genreId });
|
const pageKey = getPageKey({ albumArtistId, genreId });
|
||||||
|
|
||||||
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
|
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = usePageSidebar(pageKey);
|
||||||
|
|
||||||
const customFilters: Partial<AlbumListQuery> = useMemo(() => {
|
const customFilters: Partial<AlbumListQuery> = useMemo(() => {
|
||||||
if (albumArtistId) {
|
if (albumArtistId) {
|
||||||
@@ -48,11 +50,21 @@ const AlbumListRoute = () => {
|
|||||||
return {
|
return {
|
||||||
customFilters,
|
customFilters,
|
||||||
id: albumArtistId ?? genreId,
|
id: albumArtistId ?? genreId,
|
||||||
|
isSidebarOpen,
|
||||||
itemCount,
|
itemCount,
|
||||||
pageKey,
|
pageKey,
|
||||||
|
setIsSidebarOpen,
|
||||||
setItemCount,
|
setItemCount,
|
||||||
};
|
};
|
||||||
}, [albumArtistId, customFilters, genreId, itemCount, pageKey]);
|
}, [
|
||||||
|
albumArtistId,
|
||||||
|
customFilters,
|
||||||
|
genreId,
|
||||||
|
isSidebarOpen,
|
||||||
|
itemCount,
|
||||||
|
pageKey,
|
||||||
|
setIsSidebarOpen,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ const FolderListRoute = () => {
|
|||||||
pageKey,
|
pageKey,
|
||||||
setItemCount,
|
setItemCount,
|
||||||
};
|
};
|
||||||
}, [itemCount, pageKey, setItemCount]);
|
}, [itemCount, pageKey]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
<ListContext.Provider value={providerValue}>
|
<ListContext.Provider value={providerValue}>
|
||||||
<FolderListHeader />
|
<FolderListHeader />
|
||||||
<ListWithSidebarContainer>
|
<ListWithSidebarContainer useBreakpoint>
|
||||||
<FolderListContent />
|
<FolderListContent />
|
||||||
</ListWithSidebarContainer>
|
</ListWithSidebarContainer>
|
||||||
</ListContext.Provider>
|
</ListContext.Provider>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters';
|
import { JellyfinAlbumFilters } from '/@/renderer/features/albums/components/jellyfin-album-filters';
|
||||||
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
|
import { NavidromeAlbumFilters } from '/@/renderer/features/albums/components/navidrome-album-filters';
|
||||||
import { SubsonicAlbumFilters } from '/@/renderer/features/albums/components/subsonic-album-filters';
|
import { SubsonicAlbumFilters } from '/@/renderer/features/albums/components/subsonic-album-filters';
|
||||||
@@ -10,8 +11,11 @@ import { JellyfinSongFilters } from '/@/renderer/features/songs/components/jelly
|
|||||||
import { NavidromeSongFilters } from '/@/renderer/features/songs/components/navidrome-song-filters';
|
import { NavidromeSongFilters } from '/@/renderer/features/songs/components/navidrome-song-filters';
|
||||||
import { SubsonicSongFilters } from '/@/renderer/features/songs/components/subsonic-song-filters';
|
import { SubsonicSongFilters } from '/@/renderer/features/songs/components/subsonic-song-filters';
|
||||||
import { useCurrentServer } from '/@/renderer/store';
|
import { useCurrentServer } from '/@/renderer/store';
|
||||||
|
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||||
|
import { Group } from '/@/shared/components/group/group';
|
||||||
import { Modal } from '/@/shared/components/modal/modal';
|
import { Modal } from '/@/shared/components/modal/modal';
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
import { useDisclosure } from '/@/shared/hooks/use-disclosure';
|
||||||
import { LibraryItem, ServerType } from '/@/shared/types/domain-types';
|
import { LibraryItem, ServerType } from '/@/shared/types/domain-types';
|
||||||
|
|
||||||
@@ -20,9 +24,18 @@ interface ListFiltersProps {
|
|||||||
itemType: LibraryItem;
|
itemType: LibraryItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isFilterValueSet = (value: unknown): boolean => {
|
||||||
|
if (value === undefined || value === null) return false;
|
||||||
|
if (typeof value === 'string' && value.trim() === '') return false;
|
||||||
|
if (Array.isArray(value) && value.length === 0) return false;
|
||||||
|
if (typeof value === 'object' && Object.keys(value).length === 0) return false;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
export const ListFiltersModal = ({ isActive, itemType }: ListFiltersProps) => {
|
export const ListFiltersModal = ({ isActive, itemType }: ListFiltersProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const server = useCurrentServer();
|
const server = useCurrentServer();
|
||||||
|
const { isSidebarOpen, setIsSidebarOpen } = useListContext();
|
||||||
|
|
||||||
const serverType = server.type;
|
const serverType = server.type;
|
||||||
|
|
||||||
@@ -30,13 +43,39 @@ export const ListFiltersModal = ({ isActive, itemType }: ListFiltersProps) => {
|
|||||||
|
|
||||||
const [isOpen, handlers] = useDisclosure(false);
|
const [isOpen, handlers] = useDisclosure(false);
|
||||||
|
|
||||||
|
const handlePin = () => {
|
||||||
|
setIsSidebarOpen?.(!isSidebarOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
const canPin = Boolean(setIsSidebarOpen);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FilterButton isActive={isActive} onClick={handlers.toggle} />
|
<FilterButton isActive={isActive} onClick={handlers.toggle} />
|
||||||
<Modal
|
<Modal
|
||||||
handlers={handlers}
|
handlers={handlers}
|
||||||
opened={isOpen}
|
opened={isOpen}
|
||||||
title={t('common.filters', { postProcess: 'sentenceCase' })}
|
size="lg"
|
||||||
|
styles={{
|
||||||
|
content: {
|
||||||
|
height: '100%',
|
||||||
|
maxHeight: '640px',
|
||||||
|
maxWidth: 'var(--theme-content-max-width)',
|
||||||
|
width: '100%',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
title={
|
||||||
|
<Group>
|
||||||
|
{canPin && (
|
||||||
|
<ActionIcon
|
||||||
|
icon={isSidebarOpen ? 'unpin' : 'pin'}
|
||||||
|
onClick={handlePin}
|
||||||
|
variant="subtle"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{t('common.filters', { postProcess: 'sentenceCase' })}
|
||||||
|
</Group>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<FilterComponent />
|
<FilterComponent />
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -58,6 +97,26 @@ export const ListFilters = ({ itemType }: ListFiltersProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ListFiltersTitle = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setIsSidebarOpen } = useListContext();
|
||||||
|
|
||||||
|
const handleUnpin = () => {
|
||||||
|
setIsSidebarOpen?.(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const canUnpin = Boolean(setIsSidebarOpen);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Group justify="space-between" p="md" pb={0}>
|
||||||
|
<Text fw={500} size="xl">
|
||||||
|
{t('common.filters', { postProcess: 'sentenceCase' })}
|
||||||
|
</Text>
|
||||||
|
{canUnpin && <ActionIcon icon="unpin" onClick={handleUnpin} variant="subtle" />}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const FILTERS = {
|
const FILTERS = {
|
||||||
[ServerType.JELLYFIN]: {
|
[ServerType.JELLYFIN]: {
|
||||||
[LibraryItem.ALBUM]: JellyfinAlbumFilters,
|
[LibraryItem.ALBUM]: JellyfinAlbumFilters,
|
||||||
|
|||||||
@@ -21,10 +21,16 @@
|
|||||||
border-right: 1px solid var(--theme-colors-border);
|
border-right: 1px solid var(--theme-colors-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@container (min-width: $mantine-breakpoint-lg) {
|
@container (min-width: $mantine-breakpoint-xs) {
|
||||||
.sidebar-container {
|
.container[data-sidebar-open='true'] .sidebar-container {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@container (min-width: $mantine-breakpoint-lg) {
|
||||||
|
.container[data-use-breakpoint='true'] .sidebar-container {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-container {
|
.content-container {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { createContext, ReactNode, useContext, useMemo, useRef } from 'react';
|
|||||||
|
|
||||||
import styles from './list-with-sidebar-container.module.css';
|
import styles from './list-with-sidebar-container.module.css';
|
||||||
|
|
||||||
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { animationProps } from '/@/shared/components/animations/animation-props';
|
import { animationProps } from '/@/shared/components/animations/animation-props';
|
||||||
import { Portal } from '/@/shared/components/portal/portal';
|
import { Portal } from '/@/shared/components/portal/portal';
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ const ListWithSidebarContainerContext = createContext<ListWithSidebarContainerCo
|
|||||||
interface ListWithSidebarContainerProps {
|
interface ListWithSidebarContainerProps {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
sidebarBreakpoint?: number;
|
sidebarBreakpoint?: number;
|
||||||
|
useBreakpoint?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SidebarPortalProps {
|
interface SidebarPortalProps {
|
||||||
@@ -63,9 +65,10 @@ function SidebarPortal({ children }: SidebarPortalProps) {
|
|||||||
|
|
||||||
export const ListWithSidebarContainer = ({
|
export const ListWithSidebarContainer = ({
|
||||||
children,
|
children,
|
||||||
sidebarBreakpoint = 1200,
|
useBreakpoint = false,
|
||||||
}: ListWithSidebarContainerProps) => {
|
}: ListWithSidebarContainerProps) => {
|
||||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { isSidebarOpen = false } = useListContext();
|
||||||
|
|
||||||
const contextValue = useMemo(
|
const contextValue = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
@@ -76,7 +79,11 @@ export const ListWithSidebarContainer = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<ListWithSidebarContainerContext.Provider value={contextValue}>
|
<ListWithSidebarContainerContext.Provider value={contextValue}>
|
||||||
<div className={styles.container} data-sidebar-breakpoint={sidebarBreakpoint}>
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
data-sidebar-open={useBreakpoint ? undefined : isSidebarOpen}
|
||||||
|
data-use-breakpoint={useBreakpoint}
|
||||||
|
>
|
||||||
<div className={styles.sidebarContainer} ref={sidebarRef} />
|
<div className={styles.sidebarContainer} ref={sidebarRef} />
|
||||||
<div className={styles.contentContainer}>{children}</div>
|
<div className={styles.contentContainer}>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { lazy, Suspense, useMemo } from 'react';
|
import { lazy, Suspense, useMemo } from 'react';
|
||||||
|
|
||||||
import { useListContext } from '/@/renderer/context/list-context';
|
import { useListContext } from '/@/renderer/context/list-context';
|
||||||
import { ListFilters } from '/@/renderer/features/shared/components/list-filters';
|
import { ListFilters, ListFiltersTitle } from '/@/renderer/features/shared/components/list-filters';
|
||||||
import { ListWithSidebarContainer } from '/@/renderer/features/shared/components/list-with-sidebar-container';
|
import { ListWithSidebarContainer } from '/@/renderer/features/shared/components/list-with-sidebar-container';
|
||||||
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
|
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
|
||||||
import { ItemListSettings, useCurrentServer, useListSettings } from '/@/renderer/store';
|
import { ItemListSettings, useCurrentServer, useListSettings } from '/@/renderer/store';
|
||||||
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
||||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
import { LibraryItem, SongListQuery } from '/@/shared/types/domain-types';
|
import { LibraryItem, SongListQuery } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey, ListDisplayType, ListPaginationType } from '/@/shared/types/types';
|
import { ItemListKey, ListDisplayType, ListPaginationType } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -43,9 +44,12 @@ export const SongListContent = () => {
|
|||||||
const SongListFilters = () => {
|
const SongListFilters = () => {
|
||||||
return (
|
return (
|
||||||
<ListWithSidebarContainer.SidebarPortal>
|
<ListWithSidebarContainer.SidebarPortal>
|
||||||
|
<Stack h="100%">
|
||||||
|
<ListFiltersTitle />
|
||||||
<ScrollArea>
|
<ScrollArea>
|
||||||
<ListFilters itemType={LibraryItem.SONG} />
|
<ListFilters itemType={LibraryItem.SONG} />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
</Stack>
|
||||||
</ListWithSidebarContainer.SidebarPortal>
|
</ListWithSidebarContainer.SidebarPortal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,10 +6,14 @@ import { useListContext } from '/@/renderer/context/list-context';
|
|||||||
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
import { useAlbumListFilters } from '/@/renderer/features/albums/hooks/use-album-list-filters';
|
||||||
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
import { ListConfigMenu } from '/@/renderer/features/shared/components/list-config-menu';
|
||||||
import { ListDisplayTypeToggleButton } from '/@/renderer/features/shared/components/list-display-type-toggle-button';
|
import { ListDisplayTypeToggleButton } from '/@/renderer/features/shared/components/list-display-type-toggle-button';
|
||||||
import { ListFiltersModal } from '/@/renderer/features/shared/components/list-filters';
|
import {
|
||||||
|
isFilterValueSet,
|
||||||
|
ListFiltersModal,
|
||||||
|
} from '/@/renderer/features/shared/components/list-filters';
|
||||||
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 { FILTER_KEYS } from '/@/renderer/features/shared/utils';
|
||||||
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
|
import { useSongListFilters } from '/@/renderer/features/songs/hooks/use-song-list-filters';
|
||||||
import { GenreTarget, useGenreTarget, useSettingsStoreActions } from '/@/renderer/store';
|
import { GenreTarget, useGenreTarget, useSettingsStoreActions } from '/@/renderer/store';
|
||||||
import { Button } from '/@/shared/components/button/button';
|
import { Button } from '/@/shared/components/button/button';
|
||||||
@@ -44,6 +48,20 @@ export const SongListHeaderFilters = ({ toggleGenreTarget }: { toggleGenreTarget
|
|||||||
: t('entity.track_other', { postProcess: 'titleCase' });
|
: t('entity.track_other', { postProcess: 'titleCase' });
|
||||||
}, [target, t]);
|
}, [target, t]);
|
||||||
|
|
||||||
|
const hasActiveFilters = useMemo(() => {
|
||||||
|
const query = songFilters.query;
|
||||||
|
return Boolean(
|
||||||
|
isFilterValueSet(query[FILTER_KEYS.SONG._CUSTOM]) ||
|
||||||
|
isFilterValueSet(query[FILTER_KEYS.SONG.ALBUM_IDS]) ||
|
||||||
|
isFilterValueSet(query[FILTER_KEYS.SONG.ARTIST_IDS]) ||
|
||||||
|
query[FILTER_KEYS.SONG.FAVORITE] !== undefined ||
|
||||||
|
isFilterValueSet(query[FILTER_KEYS.SONG.GENRE_ID]) ||
|
||||||
|
isFilterValueSet(query[FILTER_KEYS.SONG.MAX_YEAR]) ||
|
||||||
|
isFilterValueSet(query[FILTER_KEYS.SONG.MIN_YEAR]) ||
|
||||||
|
isFilterValueSet(query[FILTER_KEYS.SHARED.SEARCH_TERM]),
|
||||||
|
);
|
||||||
|
}, [songFilters.query]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex justify="space-between">
|
<Flex justify="space-between">
|
||||||
<Group gap="sm" w="100%">
|
<Group gap="sm" w="100%">
|
||||||
@@ -69,7 +87,7 @@ export const SongListHeaderFilters = ({ toggleGenreTarget }: { toggleGenreTarget
|
|||||||
defaultSortOrder={SortOrder.ASC}
|
defaultSortOrder={SortOrder.ASC}
|
||||||
listKey={pageKey as ItemListKey}
|
listKey={pageKey as ItemListKey}
|
||||||
/>
|
/>
|
||||||
<ListFiltersModal itemType={LibraryItem.SONG} />
|
<ListFiltersModal isActive={hasActiveFilters} itemType={LibraryItem.SONG} />
|
||||||
<ListRefreshButton listKey={pageKey as ItemListKey} />
|
<ListRefreshButton listKey={pageKey as ItemListKey} />
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="sm" wrap="nowrap">
|
<Group gap="sm" wrap="nowrap">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ListWithSidebarContainer } from '/@/renderer/features/shared/components
|
|||||||
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
import { PageErrorBoundary } from '/@/renderer/features/shared/components/page-error-boundary';
|
||||||
import { SongListContent } from '/@/renderer/features/songs/components/song-list-content';
|
import { SongListContent } from '/@/renderer/features/songs/components/song-list-content';
|
||||||
import { SongListHeader } from '/@/renderer/features/songs/components/song-list-header';
|
import { SongListHeader } from '/@/renderer/features/songs/components/song-list-header';
|
||||||
|
import { usePageSidebar } from '/@/renderer/store/app.store';
|
||||||
import { SongListQuery } from '/@/shared/types/domain-types';
|
import { SongListQuery } from '/@/shared/types/domain-types';
|
||||||
import { ItemListKey } from '/@/shared/types/types';
|
import { ItemListKey } from '/@/shared/types/types';
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ const SongListRoute = () => {
|
|||||||
const pageKey = getPageKey({ albumArtistId, genreId });
|
const pageKey = getPageKey({ albumArtistId, genreId });
|
||||||
|
|
||||||
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
|
const [itemCount, setItemCount] = useState<number | undefined>(undefined);
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = usePageSidebar(pageKey);
|
||||||
|
|
||||||
const customFilters: Partial<SongListQuery> = useMemo(() => {
|
const customFilters: Partial<SongListQuery> = useMemo(() => {
|
||||||
if (albumArtistId) {
|
if (albumArtistId) {
|
||||||
@@ -48,11 +50,21 @@ const SongListRoute = () => {
|
|||||||
return {
|
return {
|
||||||
customFilters,
|
customFilters,
|
||||||
id: albumArtistId ?? genreId,
|
id: albumArtistId ?? genreId,
|
||||||
|
isSidebarOpen,
|
||||||
itemCount,
|
itemCount,
|
||||||
pageKey,
|
pageKey,
|
||||||
|
setIsSidebarOpen,
|
||||||
setItemCount,
|
setItemCount,
|
||||||
};
|
};
|
||||||
}, [albumArtistId, customFilters, genreId, itemCount, pageKey]);
|
}, [
|
||||||
|
albumArtistId,
|
||||||
|
customFilters,
|
||||||
|
genreId,
|
||||||
|
isSidebarOpen,
|
||||||
|
itemCount,
|
||||||
|
pageKey,
|
||||||
|
setIsSidebarOpen,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedPage>
|
<AnimatedPage>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Platform } from '/@/shared/types/types';
|
|||||||
export interface AppSlice extends AppState {
|
export interface AppSlice extends AppState {
|
||||||
actions: {
|
actions: {
|
||||||
setAppStore: (data: Partial<AppSlice>) => void;
|
setAppStore: (data: Partial<AppSlice>) => void;
|
||||||
|
setPageSidebar: (key: string, value: boolean) => void;
|
||||||
setPrivateMode: (enabled: boolean) => void;
|
setPrivateMode: (enabled: boolean) => void;
|
||||||
setShowTimeRemaining: (enabled: boolean) => void;
|
setShowTimeRemaining: (enabled: boolean) => void;
|
||||||
setSideBar: (options: Partial<SidebarProps>) => void;
|
setSideBar: (options: Partial<SidebarProps>) => void;
|
||||||
@@ -18,6 +19,7 @@ export interface AppSlice extends AppState {
|
|||||||
export interface AppState {
|
export interface AppState {
|
||||||
commandPalette: CommandPaletteProps;
|
commandPalette: CommandPaletteProps;
|
||||||
isReorderingQueue: boolean;
|
isReorderingQueue: boolean;
|
||||||
|
pageSidebar: Record<string, boolean>;
|
||||||
platform: Platform;
|
platform: Platform;
|
||||||
privateMode: boolean;
|
privateMode: boolean;
|
||||||
showTimeRemaining: boolean;
|
showTimeRemaining: boolean;
|
||||||
@@ -54,6 +56,15 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
|
|||||||
setAppStore: (data) => {
|
setAppStore: (data) => {
|
||||||
set({ ...get(), ...data });
|
set({ ...get(), ...data });
|
||||||
},
|
},
|
||||||
|
setPageSidebar: (key, value) => {
|
||||||
|
set((state) => {
|
||||||
|
if (value) {
|
||||||
|
state.pageSidebar[key] = value;
|
||||||
|
} else {
|
||||||
|
delete state.pageSidebar[key];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
setPrivateMode: (privateMode) => {
|
setPrivateMode: (privateMode) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
state.privateMode = privateMode;
|
state.privateMode = privateMode;
|
||||||
@@ -94,6 +105,7 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
isReorderingQueue: false,
|
isReorderingQueue: false,
|
||||||
|
pageSidebar: {},
|
||||||
platform: Platform.WINDOWS,
|
platform: Platform.WINDOWS,
|
||||||
privateMode: false,
|
privateMode: false,
|
||||||
showTimeRemaining: false,
|
showTimeRemaining: false,
|
||||||
@@ -140,3 +152,14 @@ export const useSetTitlebar = () => useAppStore((state) => state.actions.setTitl
|
|||||||
export const useTitlebarStore = () => useAppStore((state) => state.titlebar);
|
export const useTitlebarStore = () => useAppStore((state) => state.titlebar);
|
||||||
|
|
||||||
export const useCommandPalette = () => useAppStore((state) => state.commandPalette);
|
export const useCommandPalette = () => useAppStore((state) => state.commandPalette);
|
||||||
|
|
||||||
|
export const usePageSidebar = (key: string): [boolean, (value: boolean) => void] => {
|
||||||
|
const isOpen = useAppStore((state) => state.pageSidebar[key] ?? false);
|
||||||
|
const setPageSidebar = useAppStore((state) => state.actions.setPageSidebar);
|
||||||
|
|
||||||
|
const setIsOpen = (value: boolean) => {
|
||||||
|
setPageSidebar(key, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [isOpen, setIsOpen];
|
||||||
|
};
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ import {
|
|||||||
LuPanelRightOpen,
|
LuPanelRightOpen,
|
||||||
LuPause,
|
LuPause,
|
||||||
LuPencilLine,
|
LuPencilLine,
|
||||||
|
LuPin,
|
||||||
|
LuPinOff,
|
||||||
LuPlay,
|
LuPlay,
|
||||||
LuPlus,
|
LuPlus,
|
||||||
LuRadio,
|
LuRadio,
|
||||||
@@ -200,6 +202,7 @@ export const AppIcon = {
|
|||||||
minus: LuMinus,
|
minus: LuMinus,
|
||||||
panelRightClose: LuPanelRightClose,
|
panelRightClose: LuPanelRightClose,
|
||||||
panelRightOpen: LuPanelRightOpen,
|
panelRightOpen: LuPanelRightOpen,
|
||||||
|
pin: LuPin,
|
||||||
playlist: LuListMusic,
|
playlist: LuListMusic,
|
||||||
playlistAdd: LuListPlus,
|
playlistAdd: LuListPlus,
|
||||||
playlistDelete: LuListMinus,
|
playlistDelete: LuListMinus,
|
||||||
@@ -228,6 +231,7 @@ export const AppIcon = {
|
|||||||
themeLight: LuSun,
|
themeLight: LuSun,
|
||||||
track: LuMusic2,
|
track: LuMusic2,
|
||||||
unfavorite: LuHeartCrack,
|
unfavorite: LuHeartCrack,
|
||||||
|
unpin: LuPinOff,
|
||||||
upload: LuUpload,
|
upload: LuUpload,
|
||||||
user: LuUser,
|
user: LuUser,
|
||||||
userManage: LuUserRoundCog,
|
userManage: LuUserRoundCog,
|
||||||
|
|||||||
Reference in New Issue
Block a user