diff --git a/src/renderer/features/albums/components/expanded-album-list-item.module.css b/src/renderer/features/albums/components/expanded-album-list-item.module.css index f215a82c3..11097fe0e 100644 --- a/src/renderer/features/albums/components/expanded-album-list-item.module.css +++ b/src/renderer/features/albums/components/expanded-album-list-item.module.css @@ -128,16 +128,90 @@ } } -.tracks * { - color: black; - - /* stylelint-disable-next-line selector-class-pattern */ - :global(.table-row-module_row) { - /* height: var(--table-row-config-condensed-height); */ - border-bottom: none; - } +.tracks-list { + display: flex; + flex-direction: column; } -.tracks.dark * { +.track-row { + position: relative; + display: grid; + grid-template-columns: 55px 1fr 50px; + gap: var(--theme-spacing-sm); + align-items: center; + padding: var(--theme-spacing-xs) var(--theme-spacing-sm); + color: black; + cursor: pointer; +} + +.track-row:hover { + background-color: rgb(0 0 0 / 20%); +} + +.track-number { + text-align: left; +} + +.track-name { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.track-duration { + text-align: right; +} + +.row-selected { + position: relative; +} + +.row-selected::before { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 0; + pointer-events: none; + content: ''; + background-color: rgb(0 0 0 / 20%); + opacity: 0.7; +} + +.row-selected > * { + position: relative; + z-index: 1; +} + +.tracks.dark .track-row { color: white; } + +.track-row.dragging { + opacity: 0.5; +} + +.track-row.dragged-over-top::after { + position: absolute; + top: 0; + right: 0; + left: 0; + z-index: 3; + height: 2px; + pointer-events: none; + content: ''; + background-color: var(--theme-colors-primary); +} + +.track-row.dragged-over-bottom::after { + position: absolute; + right: 0; + bottom: 0; + left: 0; + z-index: 3; + height: 2px; + pointer-events: none; + content: ''; + background-color: var(--theme-colors-primary); +} diff --git a/src/renderer/features/albums/components/expanded-album-list-item.tsx b/src/renderer/features/albums/components/expanded-album-list-item.tsx index 04e77d2ac..52894c2b1 100644 --- a/src/renderer/features/albums/components/expanded-album-list-item.tsx +++ b/src/renderer/features/albums/components/expanded-album-list-item.tsx @@ -1,26 +1,168 @@ +import { useMergedRef } from '@mantine/hooks'; import { useSuspenseQuery } from '@tanstack/react-query'; import clsx from 'clsx'; import formatDuration from 'format-duration'; import { motion } from 'motion/react'; -import { Fragment, Suspense } from 'react'; +import { Fragment, Suspense, useCallback, useRef } from 'react'; import styles from './expanded-album-list-item.module.css'; -import { ItemListStateItem } from '/@/renderer/components/item-list/helpers/item-list-state'; +import { getDraggedItems } from '/@/renderer/components/item-list/helpers/get-dragged-items'; +import { useDefaultItemListControls } from '/@/renderer/components/item-list/helpers/item-list-controls'; +import { + ItemListStateActions, + ItemListStateItem, + useItemListState, +} from '/@/renderer/components/item-list/helpers/item-list-state'; +import { ItemListItem } from '/@/renderer/components/item-list/types'; import { albumQueries } from '/@/renderer/features/albums/api/album-api'; import { useFastAverageColor } from '/@/renderer/hooks'; +import { useDragDrop } from '/@/renderer/hooks/use-drag-drop'; import { Group } from '/@/shared/components/group/group'; import { ScrollArea } from '/@/shared/components/scroll-area/scroll-area'; import { Separator } from '/@/shared/components/separator/separator'; import { Spinner } from '/@/shared/components/spinner/spinner'; -import { Table } from '/@/shared/components/table/table'; import { TextTitle } from '/@/shared/components/text-title/text-title'; import { Text } from '/@/shared/components/text/text'; +import { LibraryItem, Song } from '/@/shared/types/domain-types'; +import { DragOperation, DragTarget, DragTargetMap } from '/@/shared/types/drag-and-drop'; + +interface AlbumTracksTableProps { + isDark?: boolean; + serverId: string; + songs?: Array<{ + discNumber: number; + duration: number; + id: string; + name: string; + trackNumber: number; + }>; +} interface ExpandedAlbumListItemProps { item: ItemListStateItem; } +interface TrackRowProps { + controls: ReturnType; + internalState: ItemListStateActions; + serverId: string; + song: NonNullable[0]; +} + +const TrackRow = ({ controls, internalState, serverId, song }: TrackRowProps) => { + const rowId = internalState.extractRowId(song); + const isSelected = rowId ? internalState.isSelected(rowId) : false; + + const songWithMetadata = { + ...song, + _serverId: serverId, + itemType: LibraryItem.SONG, + } as unknown as ItemListItem; + + const { + isDraggedOver, + isDragging: isDraggingLocal, + ref: dragRef, + } = useDragDrop({ + drag: { + getId: () => { + const draggedItems = getDraggedItems( + songWithMetadata as unknown as Song, + internalState, + ); + return draggedItems.map((draggedItem) => draggedItem.id); + }, + getItem: () => { + const draggedItems = getDraggedItems( + songWithMetadata as unknown as Song, + internalState, + ); + return draggedItems; + }, + itemType: LibraryItem.SONG, + onDragStart: () => { + const draggedItems = getDraggedItems( + songWithMetadata as unknown as Song, + internalState, + ); + internalState.setDragging(draggedItems); + }, + onDrop: () => { + internalState.setDragging([]); + }, + operation: [DragOperation.ADD], + target: DragTargetMap[LibraryItem.SONG] || DragTarget.GENERIC, + }, + isEnabled: true, + }); + + const isDragging = internalState.isDragging(song.id) || isDraggingLocal; + + const containerRef = useRef(null); + const mergedRef = useMergedRef(containerRef, dragRef); + + return ( + + controls.onClick?.({ + event: e, + internalState, + item: songWithMetadata, + itemType: LibraryItem.SONG, + }) + } + ref={mergedRef} + size="sm" + > + + {song.discNumber} - {song.trackNumber} + + {song.name} + {formatDuration(song.duration)} + + ); +}; + +const AlbumTracksTable = ({ isDark, serverId, songs }: AlbumTracksTableProps) => { + const getDataFn = useCallback(() => songs || [], [songs]); + + const extractRowId = useCallback((item: unknown) => { + if (item && typeof item === 'object' && 'id' in item) { + return (item as { id: string }).id; + } + return undefined; + }, []); + + const internalState = useItemListState(getDataFn, extractRowId); + + const controls = useDefaultItemListControls(); + + return ( +
+ +
+ {songs?.map((song) => ( + + ))} +
+
+
+ ); +}; + export const ExpandedAlbumListItem = ({ item }: ExpandedAlbumListItemProps) => { const { data, isLoading } = useSuspenseQuery( albumQueries.detail({ @@ -86,25 +228,11 @@ export const ExpandedAlbumListItem = ({ item }: ExpandedAlbumListItemProps) => { ))} -
- - - - {data?.songs?.map((song) => ( - - - {song.discNumber} - {song.trackNumber} - - {song.name} - - {formatDuration(song.duration)} - - - ))} - -
-
-
+