From b6c81183e9738b1ab8eeeff7ec8b8310484b50eb Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 10 Dec 2022 05:41:56 -0800 Subject: [PATCH] Add queue controls --- .../src/components/virtual-table/index.tsx | 13 +- .../virtual-table/table-config-dropdown.tsx | 127 +++++----------- .../components/drawer-play-queue.tsx | 24 +++ .../components/play-queue-list-controls.tsx | 137 ++++++++++++++++++ .../now-playing/components/play-queue.tsx | 41 ++++-- .../components/sidebar-play-queue.tsx | 28 ++++ .../src/features/now-playing/index.ts | 3 + .../renderer/src/layouts/default-layout.tsx | 27 +--- packages/renderer/src/store/player.store.ts | 101 ++++++++++++- 9 files changed, 362 insertions(+), 139 deletions(-) create mode 100644 packages/renderer/src/features/now-playing/components/drawer-play-queue.tsx create mode 100644 packages/renderer/src/features/now-playing/components/play-queue-list-controls.tsx create mode 100644 packages/renderer/src/features/now-playing/components/sidebar-play-queue.tsx diff --git a/packages/renderer/src/components/virtual-table/index.tsx b/packages/renderer/src/components/virtual-table/index.tsx index 79c37dc28..68fef2ffe 100644 --- a/packages/renderer/src/components/virtual-table/index.tsx +++ b/packages/renderer/src/components/virtual-table/index.tsx @@ -1,6 +1,6 @@ import type { Ref } from 'react'; import { forwardRef, useRef } from 'react'; -import { useClickOutside, useMergedRef } from '@mantine/hooks'; +import { useMergedRef } from '@mantine/hooks'; import type { ICellRendererParams, ValueGetterParams, @@ -175,17 +175,8 @@ export const VirtualTable = forwardRef( const mergedRef = useMergedRef(ref, tableRef); - const tableContainerRef = useClickOutside(() => { - if (tableRef?.current) { - tableRef?.current.api.deselectAll(); - } - }); - return ( - + { const setSettings = useSettingsStore((state) => state.setSettings); const tableConfig = useSettingsStore((state) => state.tables); - const [opened, handlers] = useDisclosure(false); - const containerVariants: Variants = { - animate: { - opacity: 0.2, - }, - initial: { - opacity: 0, - }, - }; const handleAddOrRemoveColumns = (values: TableColumn[]) => { const existingColumns = tableConfig[type].columns; @@ -146,69 +123,45 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => { }; return ( - - - - - - - - - Table Columns - column.column)} - dropdownPosition="top" - width={300} - onChange={handleAddOrRemoveColumns} - /> - - - Row Height - - - - Auto Fit Columns - - - - Follow Current Song - - - - - - + + Table Columns + column.column)} + dropdownPosition="top" + width={300} + onChange={handleAddOrRemoveColumns} + /> + + + Row Height + + + + Auto Fit Columns + + + + Follow Current Song + + + ); }; diff --git a/packages/renderer/src/features/now-playing/components/drawer-play-queue.tsx b/packages/renderer/src/features/now-playing/components/drawer-play-queue.tsx new file mode 100644 index 000000000..d19888311 --- /dev/null +++ b/packages/renderer/src/features/now-playing/components/drawer-play-queue.tsx @@ -0,0 +1,24 @@ +import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; +import { Stack } from '@mantine/core'; +import { PlayQueue } from '/@/features/now-playing/components/play-queue'; +import { PlayQueueListControls } from './play-queue-list-controls'; +import { useRef } from 'react'; +import type { Song } from '/@/api/types'; + +export const DrawerPlayQueue = () => { + const queueRef = useRef<{ grid: AgGridReactType } | null>(null); + + return ( + + + + + ); +}; diff --git a/packages/renderer/src/features/now-playing/components/play-queue-list-controls.tsx b/packages/renderer/src/features/now-playing/components/play-queue-list-controls.tsx new file mode 100644 index 000000000..6c13b53ef --- /dev/null +++ b/packages/renderer/src/features/now-playing/components/play-queue-list-controls.tsx @@ -0,0 +1,137 @@ +import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; +import { Group } from '@mantine/core'; +import { + RiArrowDownLine, + RiArrowUpLine, + RiShuffleLine, + RiDeleteBinLine, + RiListSettingsLine, + RiEraserLine, +} from 'react-icons/ri'; +import type { Song } from '/@/api/types'; +import { TableConfigDropdown, Button, Popover } from '/@/components'; +import { useQueueControls } from '/@/store'; +import type { TableType } from '/@/types'; +import { mpvPlayer } from '#preload'; + +interface PlayQueueListOptionsProps { + gridApi?: AgGridReactType['api']; + gridColumnApi?: AgGridReactType['columnApi']; + type: TableType; +} + +export const PlayQueueListControls = ({ type, gridApi }: PlayQueueListOptionsProps) => { + const { clearQueue, moveToBottomOfQueue, moveToTopOfQueue, shuffleQueue, removeFromQueue } = + useQueueControls(); + + const handleMoveToBottom = () => { + const selectedRows = gridApi?.getSelectedRows(); + const uniqueIds = selectedRows?.map((row) => row.uniqueId); + if (!uniqueIds?.length) return; + + const playerData = moveToBottomOfQueue(uniqueIds); + mpvPlayer.setQueueNext(playerData); + }; + + const handleMoveToTop = () => { + const selectedRows = gridApi?.getSelectedRows(); + const uniqueIds = selectedRows?.map((row) => row.uniqueId); + if (!uniqueIds?.length) return; + + const playerData = moveToTopOfQueue(uniqueIds); + mpvPlayer.setQueueNext(playerData); + }; + + const handleRemoveSelected = () => { + const selectedRows = gridApi?.getSelectedRows(); + const uniqueIds = selectedRows?.map((row) => row.uniqueId); + if (!uniqueIds?.length) return; + + const playerData = removeFromQueue(uniqueIds); + mpvPlayer.setQueueNext(playerData); + }; + + const handleClearQueue = () => { + const playerData = clearQueue(); + mpvPlayer.setQueue(playerData); + mpvPlayer.stop(); + }; + + const handleShuffleQueue = () => { + const playerData = shuffleQueue(); + mpvPlayer.setQueueNext(playerData); + }; + + return ( + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/renderer/src/features/now-playing/components/play-queue.tsx b/packages/renderer/src/features/now-playing/components/play-queue.tsx index a920e2916..f4b9fbad7 100644 --- a/packages/renderer/src/features/now-playing/components/play-queue.tsx +++ b/packages/renderer/src/features/now-playing/components/play-queue.tsx @@ -1,9 +1,10 @@ -import { useEffect, useMemo, useRef } from 'react'; +import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react'; import type { CellDoubleClickedEvent, ColDef, RowClassRules, RowDragEvent, + RowNode, } from '@ag-grid-community/core'; import '@ag-grid-community/styles/ag-theme-alpine.css'; import { VirtualGridAutoSizerContainer, VirtualGridContainer, getColumnDefs } from '/@/components'; @@ -16,17 +17,19 @@ import { } from '/@/store'; import { useSettingsStore } from '/@/store/settings.store'; import type { QueueSong, TableType } from '/@/types'; +import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; import { ErrorBoundary } from 'react-error-boundary'; import { mpvPlayer } from '#preload'; import { VirtualTable } from '/@/components/virtual-table'; import { ErrorFallback } from '/@/features/action-required'; +import type { Song } from '/@/api/types'; type QueueProps = { type: TableType; }; -export const PlayQueue = ({ type }: QueueProps) => { - const gridRef = useRef(null); +export const PlayQueue = forwardRef(({ type }: QueueProps, ref: any) => { + const gridRef = useRef | null | any>(null); const queue = useDefaultQueue(); const { reorderQueue, setCurrentTrack } = useQueueControls(); const currentSong = useCurrentSong(); @@ -35,6 +38,12 @@ export const PlayQueue = ({ type }: QueueProps) => { const { setAppStore } = useAppStoreActions(); const tableConfig = useSettingsStore((state) => state.tables[type]); + useImperativeHandle(ref, () => ({ + get grid() { + return gridRef?.current; + }, + })); + const columnDefs = useMemo(() => getColumnDefs(tableConfig.columns), [tableConfig.columns]); const defaultColumnDefs: ColDef = useMemo(() => { return { @@ -71,19 +80,22 @@ export const PlayQueue = ({ type }: QueueProps) => { const { api } = gridRef?.current || {}; clearTimeout(timeout); - timeout = setTimeout(() => api.redrawRows(), 250); + timeout = setTimeout(() => api?.redrawRows(), 250); }; const handleGridReady = () => { const { api } = gridRef?.current || {}; - const currentNode = api.getRowNode(currentSong?.uniqueId); - api.ensureNodeVisible(currentNode, 'middle'); + if (currentSong?.uniqueId) { + const currentNode = api?.getRowNode(currentSong?.uniqueId); + api?.ensureNodeVisible(currentNode, 'middle'); + } }; const handleColumnChange = () => { const { columnApi } = gridRef?.current || {}; - const columnsOrder = columnApi.getAllGridColumns(); + const columnsOrder = columnApi?.getAllGridColumns(); + if (!columnsOrder) return; const columnsInSettings = useSettingsStore.getState().tables[type].columns; @@ -114,7 +126,7 @@ export const PlayQueue = ({ type }: QueueProps) => { const handleGridSizeChange = () => { if (tableConfig.autoFit) { - gridRef?.current.api.sizeColumnsToFit(); + gridRef?.current?.api.sizeColumnsToFit(); } }; @@ -134,10 +146,12 @@ export const PlayQueue = ({ type }: QueueProps) => { return; } - const currentNode = api.getRowNode(currentSong?.uniqueId); - const previousNode = api.getRowNode(previousSong?.uniqueId); + const currentNode = currentSong?.uniqueId ? api.getRowNode(currentSong.uniqueId) : undefined; + const previousNode = previousSong?.uniqueId + ? api.getRowNode(previousSong?.uniqueId) + : undefined; - const rowNodes = [currentNode, previousNode]; + const rowNodes = [currentNode, previousNode].filter((e) => e !== undefined) as RowNode[]; if (rowNodes) { api.redrawRows({ rowNodes }); @@ -186,8 +200,6 @@ export const PlayQueue = ({ type }: QueueProps) => { rowData={queue} rowHeight={tableConfig.rowHeight || 40} rowSelection="multiple" - // onCellClicked={(e) => console.log('clicked', e)} - // onCellContextMenu={(e) => console.log(e)} onCellDoubleClicked={handlePlayByRowClick} onColumnMoved={handleColumnChange} onColumnResized={handleColumnChange} @@ -198,7 +210,6 @@ export const PlayQueue = ({ type }: QueueProps) => { /> - {/* */} ); -}; +}); diff --git a/packages/renderer/src/features/now-playing/components/sidebar-play-queue.tsx b/packages/renderer/src/features/now-playing/components/sidebar-play-queue.tsx new file mode 100644 index 000000000..fc606902c --- /dev/null +++ b/packages/renderer/src/features/now-playing/components/sidebar-play-queue.tsx @@ -0,0 +1,28 @@ +import { useRef } from 'react'; +import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact'; +import { Stack } from '@mantine/core'; +import { PlayQueue } from '/@/features/now-playing/components/play-queue'; +import { PlayQueueListControls } from './play-queue-list-controls'; +import type { Song } from '/@/api/types'; + +export const SidebarPlayQueue = () => { + const queueRef = useRef<{ grid: AgGridReactType } | null>(null); + + return ( + + + + + ); +}; diff --git a/packages/renderer/src/features/now-playing/index.ts b/packages/renderer/src/features/now-playing/index.ts index b3e1696b2..4c2467af8 100644 --- a/packages/renderer/src/features/now-playing/index.ts +++ b/packages/renderer/src/features/now-playing/index.ts @@ -1 +1,4 @@ export * from './components/play-queue'; +export * from './components/sidebar-play-queue'; +export * from './components/drawer-play-queue'; +export * from './components/play-queue-list-controls'; diff --git a/packages/renderer/src/layouts/default-layout.tsx b/packages/renderer/src/layouts/default-layout.tsx index b0a9a8cf1..fcab1f3a4 100644 --- a/packages/renderer/src/layouts/default-layout.tsx +++ b/packages/renderer/src/layouts/default-layout.tsx @@ -15,7 +15,7 @@ import { constrainRightSidebarWidth, constrainSidebarWidth } from '/@/utils'; import { Playerbar } from '/@/features/player'; import { Sidebar } from '/@/features/sidebar/components/sidebar'; import { useAppStoreActions } from '/@/store/app.store'; -import { PlayQueue } from '/@/features/now-playing'; +import { DrawerPlayQueue, SidebarPlayQueue } from '/@/features/now-playing'; if (!isElectron()) { useSettingsStore.getState().setSettings({ @@ -111,20 +111,6 @@ const QueueDrawerArea = styled(motion.div)` user-select: none; `; -const SideQueueContainer = styled.div` - width: 100%; - height: 100%; - - .ag-root ::-webkit-scrollbar-track-piece { - background: var(--main-bg); - } - - .ag-theme-alpine-dark { - --ag-background-color: var(--sidebar-bg) !important; - --ag-odd-row-background-color: var(--sidebar-bg) !important; - } -`; - interface DefaultLayoutProps { shell?: boolean; } @@ -183,10 +169,9 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => { x: '50vw', }, open: { - boxShadow: '4px 4px 10px 0px rgba(0,0,0,.75)', + boxShadow: '2px 2px 10px 10px rgba(0,0,0,.1)', height: 'calc(100vh - 150px)', minWidth: '400px', - opacity: 0.98, position: 'absolute', right: '20px', top: '50px', @@ -315,9 +300,7 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => { }, 50); }} > - - - + )} @@ -344,9 +327,7 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => { startResizing('right'); }} /> - - - + )} diff --git a/packages/renderer/src/store/player.store.ts b/packages/renderer/src/store/player.store.ts index 827cd6bf3..c1854cdf5 100644 --- a/packages/renderer/src/store/player.store.ts +++ b/packages/renderer/src/store/player.store.ts @@ -1,4 +1,5 @@ import map from 'lodash/map'; +import merge from 'lodash/merge'; import shuffle from 'lodash/shuffle'; import { nanoid } from 'nanoid/non-secure'; import create from 'zustand'; @@ -59,16 +60,18 @@ export interface PlayerSlice extends PlayerState { autoNext: () => PlayerData; checkIsFirstTrack: () => boolean; checkIsLastTrack: () => boolean; - // getNextTrack: () => QueueSong; - // getPreviousTrack: () => QueueSong; + clearQueue: () => PlayerData; getPlayerData: () => PlayerData; getQueueData: () => QueueData; + moveToBottomOfQueue: (uniqueIds: string[]) => PlayerData; + moveToTopOfQueue: (uniqueIds: string[]) => PlayerData; next: () => PlayerData; pause: () => void; play: () => void; player1: () => QueueSong | undefined; player2: () => QueueSong | undefined; previous: () => PlayerData; + removeFromQueue: (uniqueIds: string[]) => PlayerData; reorderQueue: (rowUniqueIds: string[], afterUniqueId?: string) => PlayerData; setCurrentIndex: (index: number) => PlayerData; setCurrentTime: (time: number) => void; @@ -79,6 +82,7 @@ export interface PlayerSlice extends PlayerState { setShuffledIndex: (index: number) => PlayerData; setStore: (data: Partial) => void; setVolume: (volume: number) => void; + shuffleQueue: () => PlayerData; }; } @@ -232,6 +236,19 @@ export const usePlayerStore = create()( return currentIndex === get().queue.default.length - 1; }, + clearQueue: () => { + set((state) => { + state.queue.default = []; + state.queue.shuffled = []; + state.queue.sorted = []; + state.current.index = 0; + state.current.shuffledIndex = 0; + state.current.player = 1; + state.current.song = undefined; + }); + + return get().actions.getPlayerData(); + }, getPlayerData: () => { const queue = get().queue.default; const currentPlayer = get().current.player; @@ -370,6 +387,46 @@ export const usePlayerStore = create()( previous: queue[get().current.index - 1], }; }, + moveToBottomOfQueue: (uniqueIds) => { + const queue = get().queue.default; + + const songsToMove = queue.filter((song) => uniqueIds.includes(song.uniqueId)); + const songsToStay = queue.filter((song) => !uniqueIds.includes(song.uniqueId)); + + const reorderedQueue = [...songsToStay, ...songsToMove]; + + const currentSongUniqueId = get().current.song?.uniqueId; + const newCurrentSongIndex = reorderedQueue.findIndex( + (song) => song.uniqueId === currentSongUniqueId, + ); + + set((state) => { + state.current.index = newCurrentSongIndex; + state.queue.default = reorderedQueue; + }); + + return get().actions.getPlayerData(); + }, + moveToTopOfQueue: (uniqueIds) => { + const queue = get().queue.default; + + const songsToMove = queue.filter((song) => uniqueIds.includes(song.uniqueId)); + const songsToStay = queue.filter((song) => !uniqueIds.includes(song.uniqueId)); + + const reorderedQueue = [...songsToMove, ...songsToStay]; + + const currentSongUniqueId = get().current.song?.uniqueId; + const newCurrentSongIndex = reorderedQueue.findIndex( + (song) => song.uniqueId === currentSongUniqueId, + ); + + set((state) => { + state.current.index = newCurrentSongIndex; + state.queue.default = reorderedQueue; + }); + + return get().actions.getPlayerData(); + }, next: () => { const isLastTrack = get().actions.checkIsLastTrack(); const repeat = get().repeat; @@ -472,6 +529,17 @@ export const usePlayerStore = create()( return get().actions.getPlayerData(); }, + removeFromQueue: (uniqueIds) => { + const queue = get().queue.default; + + const newQueue = queue.filter((song) => !uniqueIds.includes(song.uniqueId)); + + set((state) => { + state.queue.default = newQueue; + }); + + return get().actions.getPlayerData(); + }, reorderQueue: (rowUniqueIds: string[], afterUniqueId?: string) => { // Don't move if dropping on top of a selected row if (afterUniqueId && rowUniqueIds.includes(afterUniqueId)) { @@ -633,6 +701,22 @@ export const usePlayerStore = create()( state.volume = volume; }); }, + shuffleQueue: () => { + const queue = get().queue.default; + const shuffledQueue = shuffle(queue); + + const currentSongUniqueId = get().current.song?.uniqueId; + const newCurrentSongIndex = shuffledQueue.findIndex( + (song) => song.uniqueId === currentSongUniqueId, + ); + + set((state) => { + state.current.index = newCurrentSongIndex; + state.queue.default = shuffledQueue; + }); + + return get().actions.getPlayerData(); + }, }, current: { index: 0, @@ -658,7 +742,13 @@ export const usePlayerStore = create()( })), { name: 'store_player' }, ), - { name: 'store_player' }, + { + merge: (persistedState, currentState) => { + return merge(currentState, persistedState); + }, + name: 'store_player', + version: 1, + }, ), ); @@ -686,10 +776,15 @@ export const useQueueControls = () => usePlayerStore( (state) => ({ addToQueue: state.actions.addToQueue, + clearQueue: state.actions.clearQueue, + moveToBottomOfQueue: state.actions.moveToBottomOfQueue, + moveToTopOfQueue: state.actions.moveToTopOfQueue, + removeFromQueue: state.actions.removeFromQueue, reorderQueue: state.actions.reorderQueue, setCurrentIndex: state.actions.setCurrentIndex, setCurrentTrack: state.actions.setCurrentTrack, setShuffledIndex: state.actions.setShuffledIndex, + shuffleQueue: state.actions.shuffleQueue, }), shallow, );