mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
refactor album expansion to global scope
This commit is contained in:
@@ -1,3 +1,3 @@
|
||||
.container {
|
||||
height: 500px;
|
||||
.list-expanded-container {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -1,32 +1,23 @@
|
||||
import { motion, Variants } from 'motion/react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import styles from './expanded-list-container.module.css';
|
||||
|
||||
const expandedAnimationVariants: Variants = {
|
||||
hidden: {
|
||||
height: 0,
|
||||
minHeight: 0,
|
||||
},
|
||||
show: {
|
||||
minHeight: '300px',
|
||||
transition: {
|
||||
duration: 0.3,
|
||||
ease: 'easeInOut',
|
||||
},
|
||||
},
|
||||
};
|
||||
const EXPANDED_HEIGHT = 300;
|
||||
|
||||
export const ExpandedListContainer = ({ children }: { children: ReactNode }) => {
|
||||
export interface ExpandedListContainerProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const ExpandedListContainer = ({ children }: ExpandedListContainerProps) => {
|
||||
return (
|
||||
<motion.div
|
||||
animate="show"
|
||||
<div
|
||||
className={styles.listExpandedContainer}
|
||||
exit="hidden"
|
||||
initial="hidden"
|
||||
variants={expandedAnimationVariants}
|
||||
style={{
|
||||
height: EXPANDED_HEIGHT,
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,27 +2,18 @@ import { Suspense } from 'react';
|
||||
|
||||
import styles from './expanded-list-item.module.css';
|
||||
|
||||
import {
|
||||
ItemListStateActions,
|
||||
ItemListStateItem,
|
||||
useItemListStateSubscription,
|
||||
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { ItemListStateItem } from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { ExpandedAlbumListItem } from '/@/renderer/features/albums/components/expanded-album-list-item';
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
import { LibraryItem } from '/@/shared/types/domain-types';
|
||||
|
||||
interface ExpandedListItemProps {
|
||||
internalState: ItemListStateActions;
|
||||
item?: ItemListStateItem;
|
||||
itemType: LibraryItem;
|
||||
}
|
||||
|
||||
export const ExpandedListItem = ({ internalState, itemType }: ExpandedListItemProps) => {
|
||||
const expandedItems = useItemListStateSubscription(internalState, () =>
|
||||
internalState ? internalState.getExpandedItemsCached() : [],
|
||||
);
|
||||
const currentItem = expandedItems[0];
|
||||
|
||||
if (!currentItem) {
|
||||
export const ExpandedListItem = ({ item, itemType }: ExpandedListItemProps) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -30,11 +21,7 @@ export const ExpandedListItem = ({ internalState, itemType }: ExpandedListItemPr
|
||||
<div className={styles.container}>
|
||||
<div className={styles.inner}>
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
<SelectedItem
|
||||
internalState={internalState}
|
||||
item={currentItem as ItemListStateItem}
|
||||
itemType={itemType}
|
||||
/>
|
||||
<SelectedItem item={item} itemType={itemType} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
@@ -42,15 +29,14 @@ export const ExpandedListItem = ({ internalState, itemType }: ExpandedListItemPr
|
||||
};
|
||||
|
||||
interface SelectedItemProps {
|
||||
internalState: ItemListStateActions;
|
||||
item: ItemListStateItem;
|
||||
itemType: LibraryItem;
|
||||
}
|
||||
|
||||
const SelectedItem = ({ internalState, item, itemType }: SelectedItemProps) => {
|
||||
const SelectedItem = ({ item, itemType }: SelectedItemProps) => {
|
||||
switch (itemType) {
|
||||
case LibraryItem.ALBUM:
|
||||
return <ExpandedAlbumListItem internalState={internalState} item={item} />;
|
||||
return <ExpandedAlbumListItem item={item} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ContextMenuController } from '/@/renderer/features/context-menu/context
|
||||
import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import { useSetFavorite } from '/@/renderer/features/shared/hooks/use-set-favorite';
|
||||
import { useSetRating } from '/@/renderer/features/shared/hooks/use-set-rating';
|
||||
import { useAppStore } from '/@/renderer/store';
|
||||
import { LibraryItem, QueueSong, Song } from '/@/shared/types/domain-types';
|
||||
import { Play, TableColumn } from '/@/shared/types/types';
|
||||
|
||||
@@ -277,19 +278,27 @@ export const useDefaultItemListControls = (args?: UseDefaultItemListControlsArgs
|
||||
}
|
||||
},
|
||||
|
||||
onExpand: ({ internalState, item }: DefaultItemControlProps) => {
|
||||
if (!item || !internalState) {
|
||||
return;
|
||||
}
|
||||
onExpand: ({ item, itemType }: DefaultItemControlProps) => {
|
||||
if (!item) return;
|
||||
|
||||
// Extract rowId from the item
|
||||
const rowId = internalState.extractRowId(item);
|
||||
if (!rowId) return;
|
||||
|
||||
// Use the item directly (rowId is separate, used only as key in state)
|
||||
const itemListItem = item as ItemListStateItemWithRequiredProperties;
|
||||
const setGlobalExpanded = useAppStore.getState().actions.setGlobalExpanded;
|
||||
const globalExpanded = useAppStore.getState().globalExpanded;
|
||||
|
||||
return internalState?.toggleExpanded(itemListItem);
|
||||
if (globalExpanded?.item?.id === item.id) {
|
||||
setGlobalExpanded(null);
|
||||
} else {
|
||||
const itemForStore: ItemListStateItemWithRequiredProperties & {
|
||||
imageId: null | string;
|
||||
} = {
|
||||
...itemListItem,
|
||||
imageId: (itemListItem as { imageId?: null | string }).imageId ?? null,
|
||||
};
|
||||
setGlobalExpanded({
|
||||
item: itemForStore,
|
||||
itemType,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onFavorite: ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import clsx from 'clsx';
|
||||
import throttle from 'lodash/throttle';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { motion } from 'motion/react';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
import React, {
|
||||
CSSProperties,
|
||||
@@ -31,15 +31,12 @@ import {
|
||||
ItemCard,
|
||||
ItemCardProps,
|
||||
} from '/@/renderer/components/item-card/item-card';
|
||||
import { ExpandedListContainer } from '/@/renderer/components/item-list/expanded-list-container';
|
||||
import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item';
|
||||
import { createExtractRowId } from '/@/renderer/components/item-list/helpers/extract-row-id';
|
||||
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||
import {
|
||||
ItemListStateActions,
|
||||
ItemListStateItemWithRequiredProperties,
|
||||
useItemListState,
|
||||
useItemListStateSubscription,
|
||||
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { useListHotkeys } from '/@/renderer/components/item-list/helpers/use-list-hotkeys';
|
||||
import { ItemControls, ItemListHandle } from '/@/renderer/components/item-list/types';
|
||||
@@ -829,10 +826,6 @@ const BaseItemGridList = ({
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
<AnimatePresence presenceAffectsLayout>
|
||||
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
||||
{/* {enableSelectionDialog && <SelectionDialog internalState={internalState} />} */}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
@@ -903,25 +896,3 @@ const ListComponent = memo((props: ListChildComponentProps<GridItemProps>) => {
|
||||
export const ItemGridList = memo(BaseItemGridList);
|
||||
|
||||
ItemGridList.displayName = 'ItemGridList';
|
||||
|
||||
const ExpandedContainer = ({
|
||||
internalState,
|
||||
itemType,
|
||||
}: {
|
||||
internalState: ItemListStateActions;
|
||||
itemType: LibraryItem;
|
||||
}) => {
|
||||
const hasExpanded = useItemListStateSubscription(internalState, (state) =>
|
||||
state ? state.expanded.size > 0 : false,
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatePresence initial={false}>
|
||||
{hasExpanded && (
|
||||
<ExpandedListContainer>
|
||||
<ExpandedListItem internalState={internalState} itemType={itemType} />
|
||||
</ExpandedListContainer>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Component adapted from https://github.com/bvaughn/react-window/issues/826
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { AnimatePresence, motion } from 'motion/react';
|
||||
import { motion } from 'motion/react';
|
||||
import React, {
|
||||
type JSXElementConstructor,
|
||||
memo,
|
||||
@@ -18,15 +18,12 @@ import { type CellComponentProps, Grid } from 'react-window-v2';
|
||||
|
||||
import styles from './item-table-list.module.css';
|
||||
|
||||
import { ExpandedListContainer } from '/@/renderer/components/item-list/expanded-list-container';
|
||||
import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item';
|
||||
import { createExtractRowId } from '/@/renderer/components/item-list/helpers/extract-row-id';
|
||||
import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls';
|
||||
import {
|
||||
ItemListStateActions,
|
||||
ItemListStateItemWithRequiredProperties,
|
||||
useItemListState,
|
||||
useItemListStateSubscription,
|
||||
} from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import { parseTableColumns } from '/@/renderer/components/item-list/helpers/parse-table-columns';
|
||||
import { useListHotkeys } from '/@/renderer/components/item-list/helpers/use-list-hotkeys';
|
||||
@@ -1651,8 +1648,6 @@ const BaseItemTableList = ({
|
||||
totalColumnCount={totalColumnCount}
|
||||
totalRowCount={totalRowCount}
|
||||
/>
|
||||
<ExpandedContainer internalState={internalState} itemType={itemType} />
|
||||
{/* {enableSelectionDialog && <SelectionDialog internalState={internalState} />} */}
|
||||
</motion.div>
|
||||
</ItemTableListConfigProvider>
|
||||
</ItemTableListStoreProvider>
|
||||
@@ -1661,26 +1656,4 @@ const BaseItemTableList = ({
|
||||
|
||||
export const ItemTableList = memo(BaseItemTableList);
|
||||
|
||||
const ExpandedContainer = ({
|
||||
internalState,
|
||||
itemType,
|
||||
}: {
|
||||
internalState: ItemListStateActions;
|
||||
itemType: LibraryItem;
|
||||
}) => {
|
||||
const hasExpanded = useItemListStateSubscription(internalState, (state) =>
|
||||
state ? state.expanded.size > 0 : false,
|
||||
);
|
||||
|
||||
return (
|
||||
<AnimatePresence initial={false}>
|
||||
{hasExpanded && (
|
||||
<ExpandedListContainer>
|
||||
<ExpandedListItem internalState={internalState} itemType={itemType} />
|
||||
</ExpandedListContainer>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
};
|
||||
|
||||
ItemTableList.displayName = 'ItemTableList';
|
||||
|
||||
@@ -20,7 +20,6 @@ export function AlbumGridCarousel(props: AlbumGridCarouselProps) {
|
||||
const controls = useDefaultItemListControls();
|
||||
|
||||
const cards = useMemo(() => {
|
||||
// Filter out excluded IDs if provided
|
||||
const filteredItems = excludeIds
|
||||
? data.filter((album) => !excludeIds.includes(album.id))
|
||||
: data;
|
||||
@@ -31,6 +30,7 @@ export function AlbumGridCarousel(props: AlbumGridCarouselProps) {
|
||||
controls={controls}
|
||||
data={album}
|
||||
enableDrag
|
||||
enableExpansion
|
||||
itemType={LibraryItem.ALBUM}
|
||||
rows={rows}
|
||||
type="poster"
|
||||
|
||||
@@ -58,7 +58,6 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps & { rows: DataRow[]
|
||||
const controls = useDefaultItemListControls();
|
||||
|
||||
const cards = useMemo(() => {
|
||||
// Flatten all pages and filter excluded IDs
|
||||
const allItems = albums?.pages.flatMap((page: AlbumListResponse) => page.items) || [];
|
||||
const filteredItems = excludeIds
|
||||
? allItems.filter((album) => !excludeIds.includes(album.id))
|
||||
@@ -70,6 +69,7 @@ const BaseAlbumInfiniteCarousel = (props: AlbumCarouselProps & { rows: DataRow[]
|
||||
controls={controls}
|
||||
data={album}
|
||||
enableDrag
|
||||
enableExpansion
|
||||
itemType={LibraryItem.ALBUM}
|
||||
rows={rows}
|
||||
type="poster"
|
||||
|
||||
@@ -22,6 +22,7 @@ import { usePlayer } from '/@/renderer/features/player/context/player-context';
|
||||
import { PlayButtonGroup } from '/@/renderer/features/shared/components/play-button-group';
|
||||
import { useFastAverageColor } from '/@/renderer/hooks';
|
||||
import { useDragDrop } from '/@/renderer/hooks/use-drag-drop';
|
||||
import { useSetGlobalExpanded } from '/@/renderer/store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area';
|
||||
@@ -30,10 +31,24 @@ import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
import { TextTitle } from '/@/shared/components/text-title/text-title';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { useMergedRef } from '/@/shared/hooks/use-merged-ref';
|
||||
import { LibraryItem, Song } from '/@/shared/types/domain-types';
|
||||
import { LibraryItem, RelatedArtist, Song } from '/@/shared/types/domain-types';
|
||||
import { DragOperation, DragTarget, DragTargetMap } from '/@/shared/types/drag-and-drop';
|
||||
import { Play } from '/@/shared/types/types';
|
||||
|
||||
export interface ExpandedAlbumData {
|
||||
_serverId: string;
|
||||
albumArtists: RelatedArtist[];
|
||||
id: string;
|
||||
imageId: null | string;
|
||||
name: string;
|
||||
songs?: null | Song[];
|
||||
}
|
||||
|
||||
export interface ExpandedAlbumListItemProps {
|
||||
album?: ExpandedAlbumData;
|
||||
item?: ItemListStateItem;
|
||||
}
|
||||
|
||||
interface AlbumTracksTableProps {
|
||||
isDark?: boolean;
|
||||
serverId: string;
|
||||
@@ -46,11 +61,6 @@ interface AlbumTracksTableProps {
|
||||
}>;
|
||||
}
|
||||
|
||||
interface ExpandedAlbumListItemProps {
|
||||
internalState?: ItemListStateActions;
|
||||
item: ItemListStateItem;
|
||||
}
|
||||
|
||||
interface TrackRowProps {
|
||||
controls: ReturnType<typeof useDefaultItemListControls>;
|
||||
internalState: ItemListStateActions;
|
||||
@@ -60,6 +70,23 @@ interface TrackRowProps {
|
||||
songs: Song[];
|
||||
}
|
||||
|
||||
const CloseExpandedButton = () => {
|
||||
const setGlobalExpanded = useSetGlobalExpanded();
|
||||
return (
|
||||
<ActionIcon
|
||||
className={clsx(styles.closeButton)}
|
||||
icon="x"
|
||||
iconProps={{
|
||||
size: 'xl',
|
||||
}}
|
||||
onClick={() => setGlobalExpanded(null)}
|
||||
radius="50%"
|
||||
size="sm"
|
||||
variant="default"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const TrackRow = ({ controls, internalState, player, serverId, song, songs }: TrackRowProps) => {
|
||||
const rowId = internalState.extractRowId(song);
|
||||
const isSelected = useItemSelectionState(internalState, rowId);
|
||||
@@ -188,136 +215,165 @@ const AlbumTracksTable = ({ isDark, serverId, songs }: AlbumTracksTableProps) =>
|
||||
);
|
||||
};
|
||||
|
||||
export const ExpandedAlbumListItem = ({ internalState, item }: ExpandedAlbumListItemProps) => {
|
||||
const { data, isLoading } = useSuspenseQuery(
|
||||
albumQueries.detail({
|
||||
query: { id: item.id },
|
||||
serverId: item._serverId,
|
||||
}),
|
||||
);
|
||||
interface ExpandedAlbumListItemContentProps {
|
||||
albumData: ExpandedAlbumData;
|
||||
}
|
||||
|
||||
const ExpandedAlbumListItemContent = ({ albumData }: ExpandedAlbumListItemContentProps) => {
|
||||
const player = usePlayer();
|
||||
|
||||
const imageUrl = useItemImageUrl({
|
||||
id: item.imageId || undefined,
|
||||
id: albumData.imageId || undefined,
|
||||
itemType: LibraryItem.ALBUM,
|
||||
type: 'itemCard',
|
||||
});
|
||||
|
||||
const color = useFastAverageColor({
|
||||
algorithm: 'sqrt',
|
||||
id: item.id,
|
||||
id: albumData.id,
|
||||
src: imageUrl,
|
||||
srcLoaded: true,
|
||||
});
|
||||
|
||||
const handlePlay = useCallback(
|
||||
(playType: Play) => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.songs) {
|
||||
player.addToQueueByData(data.songs, playType);
|
||||
if (albumData.songs?.length) {
|
||||
player.addToQueueByData(albumData.songs, playType);
|
||||
}
|
||||
},
|
||||
[data, player],
|
||||
[albumData.songs, player],
|
||||
);
|
||||
|
||||
if (color.isLoading) {
|
||||
return null;
|
||||
return <Spinner container />;
|
||||
}
|
||||
|
||||
const songs = albumData.songs ?? null;
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
animate={{
|
||||
opacity: 1,
|
||||
}}
|
||||
animate={{ opacity: 1 }}
|
||||
className={styles.container}
|
||||
exit={{ opacity: 0 }}
|
||||
initial={{ opacity: 0 }}
|
||||
style={{ backgroundColor: color.background }}
|
||||
>
|
||||
{isLoading && (
|
||||
<div className={styles.loading}>
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
<Suspense>
|
||||
<div className={styles.expanded}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerTitle}>
|
||||
<TextTitle
|
||||
className={clsx(styles.itemTitle, {
|
||||
[styles.dark]: color.isDark,
|
||||
})}
|
||||
fw={700}
|
||||
order={4}
|
||||
>
|
||||
{data?.name}
|
||||
</TextTitle>
|
||||
{internalState && (
|
||||
<ActionIcon
|
||||
className={clsx(styles.closeButton)}
|
||||
icon="x"
|
||||
iconProps={{
|
||||
size: 'xl',
|
||||
}}
|
||||
onClick={() => {
|
||||
const rowId = internalState.extractRowId(item);
|
||||
if (rowId) {
|
||||
internalState.clearExpanded();
|
||||
}
|
||||
}}
|
||||
radius="50%"
|
||||
size="sm"
|
||||
variant="default"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Group
|
||||
className={clsx(styles.itemSubtitle, {
|
||||
[styles.dark]: color.isDark,
|
||||
})}
|
||||
gap="xs"
|
||||
<div className={styles.expanded}>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.header}>
|
||||
<div className={styles.headerTitle}>
|
||||
<TextTitle
|
||||
className={clsx(styles.itemTitle, { [styles.dark]: color.isDark })}
|
||||
fw={700}
|
||||
order={4}
|
||||
>
|
||||
{data?.albumArtists.map((artist, index) => (
|
||||
<Fragment key={artist.id}>
|
||||
<Text
|
||||
className={clsx(styles.itemSubtitle, {
|
||||
[styles.dark]: color.isDark,
|
||||
})}
|
||||
>
|
||||
{artist.name}
|
||||
</Text>
|
||||
{index < data?.albumArtists.length - 1 && <Separator />}
|
||||
</Fragment>
|
||||
))}
|
||||
</Group>
|
||||
{albumData.name}
|
||||
</TextTitle>
|
||||
<CloseExpandedButton />
|
||||
</div>
|
||||
<AlbumTracksTable
|
||||
isDark={color.isDark}
|
||||
serverId={item._serverId}
|
||||
songs={data?.songs}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.imageContainer}>
|
||||
<div
|
||||
className={styles.backgroundImage}
|
||||
style={{
|
||||
['--bg-color' as string]: color?.background,
|
||||
backgroundImage: `url(${imageUrl})`,
|
||||
}}
|
||||
/>
|
||||
{data?.songs && data.songs.length > 0 && (
|
||||
<div className={styles.playButtonGroup}>
|
||||
<PlayButtonGroup onPlay={handlePlay} />
|
||||
</div>
|
||||
)}
|
||||
<Group
|
||||
className={clsx(styles.itemSubtitle, { [styles.dark]: color.isDark })}
|
||||
gap="xs"
|
||||
>
|
||||
{albumData.albumArtists?.map((artist, index) => (
|
||||
<Fragment key={artist.id}>
|
||||
<Text
|
||||
className={clsx(styles.itemSubtitle, {
|
||||
[styles.dark]: color.isDark,
|
||||
})}
|
||||
>
|
||||
{artist.name}
|
||||
</Text>
|
||||
{index < (albumData.albumArtists?.length ?? 0) - 1 && (
|
||||
<Separator />
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</Group>
|
||||
</div>
|
||||
<AlbumTracksTable
|
||||
isDark={color.isDark}
|
||||
serverId={albumData._serverId}
|
||||
songs={songs ?? undefined}
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
<div className={styles.imageContainer}>
|
||||
<div
|
||||
className={styles.backgroundImage}
|
||||
style={{
|
||||
['--bg-color' as string]: color?.background,
|
||||
backgroundImage: `url(${imageUrl})`,
|
||||
}}
|
||||
/>
|
||||
{songs && songs.length > 0 && (
|
||||
<div className={styles.playButtonGroup}>
|
||||
<PlayButtonGroup onPlay={handlePlay} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
};
|
||||
|
||||
const ExpandedAlbumListItemWithFetch = ({ item }: { item: ItemListStateItem }) => {
|
||||
const { data } = useSuspenseQuery(
|
||||
albumQueries.detail({
|
||||
query: { id: item.id },
|
||||
serverId: item._serverId,
|
||||
}),
|
||||
);
|
||||
|
||||
const albumData: ExpandedAlbumData = {
|
||||
_serverId: item._serverId,
|
||||
albumArtists: data?.albumArtists ?? [],
|
||||
id: item.id,
|
||||
imageId: item.imageId ?? data?.imageId ?? null,
|
||||
name: data?.name ?? '',
|
||||
songs: data?.songs ?? null,
|
||||
};
|
||||
|
||||
return <ExpandedAlbumListItemContent albumData={albumData} />;
|
||||
};
|
||||
|
||||
function itemToExpandedAlbumData(
|
||||
item: ItemListStateItem & {
|
||||
_playlistSongs?: Song[];
|
||||
albumArtists?: RelatedArtist[];
|
||||
name?: string;
|
||||
},
|
||||
): ExpandedAlbumData | null {
|
||||
const songs =
|
||||
(item as { songs?: Song[] }).songs ?? (item as { _playlistSongs?: Song[] })._playlistSongs;
|
||||
if (songs == null) return null;
|
||||
return {
|
||||
_serverId: item._serverId,
|
||||
albumArtists: item.albumArtists ?? [],
|
||||
id: item.id,
|
||||
imageId: (item as { imageId?: null | string }).imageId ?? null,
|
||||
name: (item as { name?: string }).name ?? '',
|
||||
songs,
|
||||
};
|
||||
}
|
||||
|
||||
export const ExpandedAlbumListItem = (props: ExpandedAlbumListItemProps) => {
|
||||
if (props.album != null) {
|
||||
return <ExpandedAlbumListItemContent albumData={props.album} />;
|
||||
}
|
||||
|
||||
if (props.item != null) {
|
||||
const albumData = itemToExpandedAlbumData(props.item);
|
||||
|
||||
if (albumData != null) {
|
||||
return <ExpandedAlbumListItemContent albumData={albumData} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
<ExpandedAlbumListItemWithFetch item={props.item} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1055,6 +1055,7 @@ interface AlbumSectionProps {
|
||||
albums: Album[];
|
||||
controls: ItemControls;
|
||||
cq: ReturnType<typeof useContainerQuery>;
|
||||
enableExpansion?: boolean;
|
||||
releaseType: string;
|
||||
rows: DataRow[] | undefined;
|
||||
title: React.ReactNode | string;
|
||||
@@ -1074,7 +1075,15 @@ const getItemsPerRow = (cq: ReturnType<typeof useContainerQuery>) => {
|
||||
return 2;
|
||||
};
|
||||
|
||||
const AlbumSection = ({ albums, controls, cq, releaseType, rows, title }: AlbumSectionProps) => {
|
||||
const AlbumSection = ({
|
||||
albums,
|
||||
controls,
|
||||
cq,
|
||||
enableExpansion,
|
||||
releaseType,
|
||||
rows,
|
||||
title,
|
||||
}: AlbumSectionProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const itemsPerRow = getItemsPerRow(cq);
|
||||
@@ -1199,6 +1208,7 @@ const AlbumSection = ({ albums, controls, cq, releaseType, rows, title }: AlbumS
|
||||
controls={controls}
|
||||
data={album}
|
||||
enableDrag
|
||||
enableExpansion={enableExpansion ?? true}
|
||||
itemType={LibraryItem.ALBUM}
|
||||
rows={rows}
|
||||
type="poster"
|
||||
@@ -1376,7 +1386,6 @@ const ArtistAlbums = ({ albumsQuery }: ArtistAlbumsProps) => {
|
||||
const routeId = (artistId || albumArtistId) as string;
|
||||
|
||||
const rows = useGridRows(LibraryItem.ALBUM, ItemListKey.ALBUM);
|
||||
const controls = useDefaultItemListControls();
|
||||
|
||||
const filteredAndSortedAlbums = useMemo(() => {
|
||||
const albums = albumsQuery.data?.items || [];
|
||||
@@ -1384,6 +1393,8 @@ const ArtistAlbums = ({ albumsQuery }: ArtistAlbumsProps) => {
|
||||
return sortAlbumList(searched, sortBy, sortOrder);
|
||||
}, [albumsQuery.data?.items, debouncedSearchTerm, sortBy, sortOrder]);
|
||||
|
||||
const controls = useDefaultItemListControls();
|
||||
|
||||
const albumsByReleaseType = useMemo(() => {
|
||||
return groupAlbumsByReleaseType(filteredAndSortedAlbums, routeId, groupingType);
|
||||
}, [filteredAndSortedAlbums, routeId, groupingType]);
|
||||
@@ -1652,6 +1663,7 @@ const ArtistAlbums = ({ albumsQuery }: ArtistAlbumsProps) => {
|
||||
albums={albums}
|
||||
controls={controls}
|
||||
cq={cq}
|
||||
enableExpansion
|
||||
key={releaseType}
|
||||
releaseType={releaseType}
|
||||
rows={rows}
|
||||
|
||||
@@ -20,7 +20,6 @@ export function AlbumArtistGridCarousel(props: AlbumArtistGridCarouselProps) {
|
||||
const controls = useDefaultItemListControls();
|
||||
|
||||
const cards = useMemo(() => {
|
||||
// Filter out excluded IDs if provided
|
||||
const filteredItems = excludeIds
|
||||
? data.filter((albumArtist) => !excludeIds.includes(albumArtist.id))
|
||||
: data;
|
||||
|
||||
@@ -27,3 +27,17 @@
|
||||
.main-content-container.sidebar-collapsed.right-expanded {
|
||||
grid-template-columns: 80px 1fr var(--right-sidebar-width);
|
||||
}
|
||||
|
||||
.main-content-body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-content-body-scroll {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@@ -6,11 +6,18 @@ import { shallow } from 'zustand/shallow';
|
||||
|
||||
import styles from './main-content.module.css';
|
||||
|
||||
import { ExpandedListContainer } from '/@/renderer/components/item-list/expanded-list-container';
|
||||
import { ExpandedListItem } from '/@/renderer/components/item-list/expanded-list-item';
|
||||
import { FullScreenOverlay } from '/@/renderer/layouts/default-layout/full-screen-overlay';
|
||||
import { FullScreenVisualizerOverlay } from '/@/renderer/layouts/default-layout/full-screen-visualizer-overlay';
|
||||
import { LeftSidebar } from '/@/renderer/layouts/default-layout/left-sidebar';
|
||||
import { RightSidebar } from '/@/renderer/layouts/default-layout/right-sidebar';
|
||||
import { useAppStore, useAppStoreActions, useSideQueueType } from '/@/renderer/store';
|
||||
import {
|
||||
useAppStore,
|
||||
useAppStoreActions,
|
||||
useGlobalExpanded,
|
||||
useSideQueueType,
|
||||
} from '/@/renderer/store';
|
||||
import { constrainRightSidebarWidth, constrainSidebarWidth } from '/@/renderer/utils';
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
|
||||
@@ -159,10 +166,30 @@ export const MainContent = ({ shell }: { shell?: boolean }) => {
|
||||
);
|
||||
};
|
||||
|
||||
function MainContentBody() {
|
||||
function GlobalExpandedPanel() {
|
||||
const globalExpanded = useGlobalExpanded();
|
||||
|
||||
if (!globalExpanded) return null;
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
<ExpandedListContainer>
|
||||
<ExpandedListItem
|
||||
item={globalExpanded.item}
|
||||
itemType={globalExpanded.itemType}
|
||||
/>
|
||||
</ExpandedListContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function MainContentBody() {
|
||||
return (
|
||||
<div className={styles.mainContentBody}>
|
||||
<div className={styles.mainContentBodyScroll}>
|
||||
<Suspense fallback={<Spinner container />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
<GlobalExpandedPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import type { ItemListStateItem } from '/@/renderer/components/item-list/helpers/item-list-state';
|
||||
import type { LibraryItem } from '/@/shared/types/domain-types';
|
||||
|
||||
import merge from 'lodash/merge';
|
||||
import { devtools, persist } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
@@ -17,6 +20,7 @@ export interface AppSlice extends AppState {
|
||||
setArtistSelectMode: (mode: 'multi' | 'single') => void;
|
||||
setGenreIdsMode: (mode: 'and' | 'or') => void;
|
||||
setGenreSelectMode: (mode: 'multi' | 'single') => void;
|
||||
setGlobalExpanded: (value: GlobalExpandedState | null) => void;
|
||||
setPageSidebar: (key: string, value: boolean) => void;
|
||||
setPrivateMode: (enabled: boolean) => void;
|
||||
setShowTimeRemaining: (enabled: boolean) => void;
|
||||
@@ -38,6 +42,7 @@ export interface AppState {
|
||||
commandPalette: CommandPaletteProps;
|
||||
genreIdsMode: 'and' | 'or';
|
||||
genreSelectMode: 'multi' | 'single';
|
||||
globalExpanded: GlobalExpandedState | null;
|
||||
isReorderingQueue: boolean;
|
||||
pageSidebar: Record<string, boolean>;
|
||||
platform: Platform;
|
||||
@@ -47,6 +52,11 @@ export interface AppState {
|
||||
titlebar: TitlebarProps;
|
||||
}
|
||||
|
||||
export interface GlobalExpandedState {
|
||||
item: ItemListStateItem;
|
||||
itemType: LibraryItem;
|
||||
}
|
||||
|
||||
type CommandPaletteProps = {
|
||||
close: () => void;
|
||||
open: () => void;
|
||||
@@ -120,6 +130,11 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
|
||||
state.genreSelectMode = mode;
|
||||
});
|
||||
},
|
||||
setGlobalExpanded: (value) => {
|
||||
set((state) => {
|
||||
state.globalExpanded = value;
|
||||
});
|
||||
},
|
||||
setPageSidebar: (key, value) => {
|
||||
set((state) => {
|
||||
state.pageSidebar[key] = value;
|
||||
@@ -175,6 +190,7 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
|
||||
},
|
||||
genreIdsMode: 'and',
|
||||
genreSelectMode: 'multi',
|
||||
globalExpanded: null,
|
||||
isReorderingQueue: false,
|
||||
pageSidebar: {
|
||||
album: true,
|
||||
@@ -210,7 +226,12 @@ export const useAppStore = createWithEqualityFn<AppSlice>()(
|
||||
return persistedState;
|
||||
},
|
||||
name: 'store_app',
|
||||
version: 3,
|
||||
partialize: (state) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- ignore non-persisted state
|
||||
const { globalExpanded: _, ...rest } = state;
|
||||
return rest;
|
||||
},
|
||||
version: 4,
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -237,3 +258,16 @@ export const usePageSidebar = (key: string): [boolean, (value: boolean) => void]
|
||||
|
||||
return [isOpen, setIsOpen];
|
||||
};
|
||||
|
||||
export const useGlobalExpanded = () => useAppStore((state) => state.globalExpanded);
|
||||
|
||||
export const useSetGlobalExpanded = () => useAppStore((state) => state.actions.setGlobalExpanded);
|
||||
|
||||
export const useGlobalExpandedState = () => {
|
||||
const globalExpanded = useGlobalExpanded();
|
||||
const setGlobalExpanded = useSetGlobalExpanded();
|
||||
|
||||
const clearGlobalExpanded = () => setGlobalExpanded(null);
|
||||
|
||||
return { clearGlobalExpanded, globalExpanded, setGlobalExpanded };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user