mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
Add queue controls
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import type { Ref } from 'react';
|
import type { Ref } from 'react';
|
||||||
import { forwardRef, useRef } from 'react';
|
import { forwardRef, useRef } from 'react';
|
||||||
import { useClickOutside, useMergedRef } from '@mantine/hooks';
|
import { useMergedRef } from '@mantine/hooks';
|
||||||
import type {
|
import type {
|
||||||
ICellRendererParams,
|
ICellRendererParams,
|
||||||
ValueGetterParams,
|
ValueGetterParams,
|
||||||
@@ -175,17 +175,8 @@ export const VirtualTable = forwardRef(
|
|||||||
|
|
||||||
const mergedRef = useMergedRef(ref, tableRef);
|
const mergedRef = useMergedRef(ref, tableRef);
|
||||||
|
|
||||||
const tableContainerRef = useClickOutside(() => {
|
|
||||||
if (tableRef?.current) {
|
|
||||||
tableRef?.current.api.deselectAll();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableWrapper
|
<TableWrapper className="ag-theme-alpine-dark">
|
||||||
ref={tableContainerRef}
|
|
||||||
className="ag-theme-alpine-dark"
|
|
||||||
>
|
|
||||||
<AgGridReact
|
<AgGridReact
|
||||||
ref={mergedRef}
|
ref={mergedRef}
|
||||||
suppressMoveWhenRowDragging
|
suppressMoveWhenRowDragging
|
||||||
|
|||||||
@@ -1,19 +1,12 @@
|
|||||||
import type { ChangeEvent } from 'react';
|
import type { ChangeEvent } from 'react';
|
||||||
import { Stack } from '@mantine/core';
|
import { Stack } from '@mantine/core';
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
|
||||||
import type { Variants } from 'framer-motion';
|
|
||||||
import { motion } from 'framer-motion';
|
|
||||||
import { RiListSettingsLine } from 'react-icons/ri';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
import { Button } from '/@/components/button';
|
|
||||||
import { Popover } from '/@/components/popover';
|
|
||||||
import { MultiSelect } from '/@/components/select';
|
|
||||||
import { Slider } from '/@/components/slider';
|
import { Slider } from '/@/components/slider';
|
||||||
import { Switch } from '/@/components/switch';
|
import { Switch } from '/@/components/switch';
|
||||||
import { Text } from '/@/components/text';
|
import { Text } from '/@/components/text';
|
||||||
import { useSettingsStore } from '/@/store/settings.store';
|
import { useSettingsStore } from '/@/store/settings.store';
|
||||||
import type { TableType } from '/@/types';
|
import type { TableType } from '/@/types';
|
||||||
import { TableColumn } from '/@/types';
|
import { TableColumn } from '/@/types';
|
||||||
|
import { MultiSelect } from '/@/components/select';
|
||||||
|
|
||||||
export const tableColumns = [
|
export const tableColumns = [
|
||||||
{ label: 'Row Index', value: TableColumn.ROW_INDEX },
|
{ label: 'Row Index', value: TableColumn.ROW_INDEX },
|
||||||
@@ -38,13 +31,6 @@ export const tableColumns = [
|
|||||||
{ label: 'Date Added', value: TableColumn.DATE_ADDED },
|
{ label: 'Date Added', value: TableColumn.DATE_ADDED },
|
||||||
];
|
];
|
||||||
|
|
||||||
const Container = styled(motion.div)`
|
|
||||||
position: absolute;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 500;
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface TableConfigDropdownProps {
|
interface TableConfigDropdownProps {
|
||||||
type: TableType;
|
type: TableType;
|
||||||
}
|
}
|
||||||
@@ -52,15 +38,6 @@ interface TableConfigDropdownProps {
|
|||||||
export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
||||||
const setSettings = useSettingsStore((state) => state.setSettings);
|
const setSettings = useSettingsStore((state) => state.setSettings);
|
||||||
const tableConfig = useSettingsStore((state) => state.tables);
|
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 handleAddOrRemoveColumns = (values: TableColumn[]) => {
|
||||||
const existingColumns = tableConfig[type].columns;
|
const existingColumns = tableConfig[type].columns;
|
||||||
@@ -146,27 +123,6 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
|
||||||
animate="animate"
|
|
||||||
initial="initial"
|
|
||||||
variants={containerVariants}
|
|
||||||
whileHover={{ opacity: 1 }}
|
|
||||||
>
|
|
||||||
<Popover
|
|
||||||
opened={opened}
|
|
||||||
position="top-start"
|
|
||||||
withArrow={false}
|
|
||||||
>
|
|
||||||
<Popover.Target>
|
|
||||||
<Button
|
|
||||||
compact
|
|
||||||
variant="subtle"
|
|
||||||
onClick={() => handlers.toggle()}
|
|
||||||
>
|
|
||||||
<RiListSettingsLine size={20} />
|
|
||||||
</Button>
|
|
||||||
</Popover.Target>
|
|
||||||
<Popover.Dropdown>
|
|
||||||
<Stack
|
<Stack
|
||||||
p="1rem"
|
p="1rem"
|
||||||
spacing="xl"
|
spacing="xl"
|
||||||
@@ -207,8 +163,5 @@ export const TableConfigDropdown = ({ type }: TableConfigDropdownProps) => {
|
|||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Popover.Dropdown>
|
|
||||||
</Popover>
|
|
||||||
</Container>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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<Song> } | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
pb="1rem"
|
||||||
|
sx={{ height: '100%' }}
|
||||||
|
>
|
||||||
|
<PlayQueue type="sideQueue" />
|
||||||
|
<PlayQueueListControls
|
||||||
|
gridApi={queueRef.current?.grid.api}
|
||||||
|
gridColumnApi={queueRef.current?.grid?.columnApi}
|
||||||
|
type="sideQueue"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<Song>['api'];
|
||||||
|
gridColumnApi?: AgGridReactType<Song>['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 (
|
||||||
|
<Group
|
||||||
|
position="apart"
|
||||||
|
px="1rem"
|
||||||
|
sx={{ alignItems: 'center' }}
|
||||||
|
>
|
||||||
|
<Group>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
size="sm"
|
||||||
|
tooltip={{ label: 'Shuffle queue' }}
|
||||||
|
variant="default"
|
||||||
|
onClick={handleShuffleQueue}
|
||||||
|
>
|
||||||
|
<RiShuffleLine size={15} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
size="sm"
|
||||||
|
tooltip={{ label: 'Move selected to top' }}
|
||||||
|
variant="default"
|
||||||
|
onClick={handleMoveToTop}
|
||||||
|
>
|
||||||
|
<RiArrowUpLine size={15} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
size="sm"
|
||||||
|
tooltip={{ label: 'Move selected to bottom' }}
|
||||||
|
variant="default"
|
||||||
|
onClick={handleMoveToBottom}
|
||||||
|
>
|
||||||
|
<RiArrowDownLine size={15} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
size="sm"
|
||||||
|
tooltip={{ label: 'Remove selected' }}
|
||||||
|
variant="default"
|
||||||
|
onClick={handleRemoveSelected}
|
||||||
|
>
|
||||||
|
<RiEraserLine size={15} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
size="sm"
|
||||||
|
tooltip={{ label: 'Clear queue' }}
|
||||||
|
variant="default"
|
||||||
|
onClick={handleClearQueue}
|
||||||
|
>
|
||||||
|
<RiDeleteBinLine size={15} />
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Group>
|
||||||
|
<Popover>
|
||||||
|
<Popover.Target>
|
||||||
|
<Button
|
||||||
|
compact
|
||||||
|
size="sm"
|
||||||
|
tooltip={{ label: 'Configure' }}
|
||||||
|
variant="default"
|
||||||
|
>
|
||||||
|
<RiListSettingsLine size={15} />
|
||||||
|
</Button>
|
||||||
|
</Popover.Target>
|
||||||
|
<Popover.Dropdown>
|
||||||
|
<TableConfigDropdown type={type} />
|
||||||
|
</Popover.Dropdown>
|
||||||
|
</Popover>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useMemo, useRef } from 'react';
|
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
|
||||||
import type {
|
import type {
|
||||||
CellDoubleClickedEvent,
|
CellDoubleClickedEvent,
|
||||||
ColDef,
|
ColDef,
|
||||||
RowClassRules,
|
RowClassRules,
|
||||||
RowDragEvent,
|
RowDragEvent,
|
||||||
|
RowNode,
|
||||||
} from '@ag-grid-community/core';
|
} from '@ag-grid-community/core';
|
||||||
import '@ag-grid-community/styles/ag-theme-alpine.css';
|
import '@ag-grid-community/styles/ag-theme-alpine.css';
|
||||||
import { VirtualGridAutoSizerContainer, VirtualGridContainer, getColumnDefs } from '/@/components';
|
import { VirtualGridAutoSizerContainer, VirtualGridContainer, getColumnDefs } from '/@/components';
|
||||||
@@ -16,17 +17,19 @@ import {
|
|||||||
} from '/@/store';
|
} from '/@/store';
|
||||||
import { useSettingsStore } from '/@/store/settings.store';
|
import { useSettingsStore } from '/@/store/settings.store';
|
||||||
import type { QueueSong, TableType } from '/@/types';
|
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 { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { mpvPlayer } from '#preload';
|
import { mpvPlayer } from '#preload';
|
||||||
import { VirtualTable } from '/@/components/virtual-table';
|
import { VirtualTable } from '/@/components/virtual-table';
|
||||||
import { ErrorFallback } from '/@/features/action-required';
|
import { ErrorFallback } from '/@/features/action-required';
|
||||||
|
import type { Song } from '/@/api/types';
|
||||||
|
|
||||||
type QueueProps = {
|
type QueueProps = {
|
||||||
type: TableType;
|
type: TableType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PlayQueue = ({ type }: QueueProps) => {
|
export const PlayQueue = forwardRef(({ type }: QueueProps, ref: any) => {
|
||||||
const gridRef = useRef<any>(null);
|
const gridRef = useRef<AgGridReactType<Song> | null | any>(null);
|
||||||
const queue = useDefaultQueue();
|
const queue = useDefaultQueue();
|
||||||
const { reorderQueue, setCurrentTrack } = useQueueControls();
|
const { reorderQueue, setCurrentTrack } = useQueueControls();
|
||||||
const currentSong = useCurrentSong();
|
const currentSong = useCurrentSong();
|
||||||
@@ -35,6 +38,12 @@ export const PlayQueue = ({ type }: QueueProps) => {
|
|||||||
const { setAppStore } = useAppStoreActions();
|
const { setAppStore } = useAppStoreActions();
|
||||||
const tableConfig = useSettingsStore((state) => state.tables[type]);
|
const tableConfig = useSettingsStore((state) => state.tables[type]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
get grid() {
|
||||||
|
return gridRef?.current;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const columnDefs = useMemo(() => getColumnDefs(tableConfig.columns), [tableConfig.columns]);
|
const columnDefs = useMemo(() => getColumnDefs(tableConfig.columns), [tableConfig.columns]);
|
||||||
const defaultColumnDefs: ColDef = useMemo(() => {
|
const defaultColumnDefs: ColDef = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@@ -71,19 +80,22 @@ export const PlayQueue = ({ type }: QueueProps) => {
|
|||||||
|
|
||||||
const { api } = gridRef?.current || {};
|
const { api } = gridRef?.current || {};
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
timeout = setTimeout(() => api.redrawRows(), 250);
|
timeout = setTimeout(() => api?.redrawRows(), 250);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGridReady = () => {
|
const handleGridReady = () => {
|
||||||
const { api } = gridRef?.current || {};
|
const { api } = gridRef?.current || {};
|
||||||
|
|
||||||
const currentNode = api.getRowNode(currentSong?.uniqueId);
|
if (currentSong?.uniqueId) {
|
||||||
api.ensureNodeVisible(currentNode, 'middle');
|
const currentNode = api?.getRowNode(currentSong?.uniqueId);
|
||||||
|
api?.ensureNodeVisible(currentNode, 'middle');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleColumnChange = () => {
|
const handleColumnChange = () => {
|
||||||
const { columnApi } = gridRef?.current || {};
|
const { columnApi } = gridRef?.current || {};
|
||||||
const columnsOrder = columnApi.getAllGridColumns();
|
const columnsOrder = columnApi?.getAllGridColumns();
|
||||||
|
if (!columnsOrder) return;
|
||||||
|
|
||||||
const columnsInSettings = useSettingsStore.getState().tables[type].columns;
|
const columnsInSettings = useSettingsStore.getState().tables[type].columns;
|
||||||
|
|
||||||
@@ -114,7 +126,7 @@ export const PlayQueue = ({ type }: QueueProps) => {
|
|||||||
|
|
||||||
const handleGridSizeChange = () => {
|
const handleGridSizeChange = () => {
|
||||||
if (tableConfig.autoFit) {
|
if (tableConfig.autoFit) {
|
||||||
gridRef?.current.api.sizeColumnsToFit();
|
gridRef?.current?.api.sizeColumnsToFit();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -134,10 +146,12 @@ export const PlayQueue = ({ type }: QueueProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentNode = api.getRowNode(currentSong?.uniqueId);
|
const currentNode = currentSong?.uniqueId ? api.getRowNode(currentSong.uniqueId) : undefined;
|
||||||
const previousNode = api.getRowNode(previousSong?.uniqueId);
|
const previousNode = previousSong?.uniqueId
|
||||||
|
? api.getRowNode(previousSong?.uniqueId)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
const rowNodes = [currentNode, previousNode];
|
const rowNodes = [currentNode, previousNode].filter((e) => e !== undefined) as RowNode<any>[];
|
||||||
|
|
||||||
if (rowNodes) {
|
if (rowNodes) {
|
||||||
api.redrawRows({ rowNodes });
|
api.redrawRows({ rowNodes });
|
||||||
@@ -186,8 +200,6 @@ export const PlayQueue = ({ type }: QueueProps) => {
|
|||||||
rowData={queue}
|
rowData={queue}
|
||||||
rowHeight={tableConfig.rowHeight || 40}
|
rowHeight={tableConfig.rowHeight || 40}
|
||||||
rowSelection="multiple"
|
rowSelection="multiple"
|
||||||
// onCellClicked={(e) => console.log('clicked', e)}
|
|
||||||
// onCellContextMenu={(e) => console.log(e)}
|
|
||||||
onCellDoubleClicked={handlePlayByRowClick}
|
onCellDoubleClicked={handlePlayByRowClick}
|
||||||
onColumnMoved={handleColumnChange}
|
onColumnMoved={handleColumnChange}
|
||||||
onColumnResized={handleColumnChange}
|
onColumnResized={handleColumnChange}
|
||||||
@@ -198,7 +210,6 @@ export const PlayQueue = ({ type }: QueueProps) => {
|
|||||||
/>
|
/>
|
||||||
</VirtualGridAutoSizerContainer>
|
</VirtualGridAutoSizerContainer>
|
||||||
</VirtualGridContainer>
|
</VirtualGridContainer>
|
||||||
{/* <TableConfigDropdown type={type} /> */}
|
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|||||||
@@ -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<Song> } | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
pb="1rem"
|
||||||
|
pt="2.5rem"
|
||||||
|
sx={{ height: '100%' }}
|
||||||
|
>
|
||||||
|
<PlayQueue
|
||||||
|
ref={queueRef}
|
||||||
|
type="sideQueue"
|
||||||
|
/>
|
||||||
|
<PlayQueueListControls
|
||||||
|
gridApi={queueRef.current?.grid.api}
|
||||||
|
gridColumnApi={queueRef.current?.grid.columnApi}
|
||||||
|
type="sideQueue"
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1 +1,4 @@
|
|||||||
export * from './components/play-queue';
|
export * from './components/play-queue';
|
||||||
|
export * from './components/sidebar-play-queue';
|
||||||
|
export * from './components/drawer-play-queue';
|
||||||
|
export * from './components/play-queue-list-controls';
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { constrainRightSidebarWidth, constrainSidebarWidth } from '/@/utils';
|
|||||||
import { Playerbar } from '/@/features/player';
|
import { Playerbar } from '/@/features/player';
|
||||||
import { Sidebar } from '/@/features/sidebar/components/sidebar';
|
import { Sidebar } from '/@/features/sidebar/components/sidebar';
|
||||||
import { useAppStoreActions } from '/@/store/app.store';
|
import { useAppStoreActions } from '/@/store/app.store';
|
||||||
import { PlayQueue } from '/@/features/now-playing';
|
import { DrawerPlayQueue, SidebarPlayQueue } from '/@/features/now-playing';
|
||||||
|
|
||||||
if (!isElectron()) {
|
if (!isElectron()) {
|
||||||
useSettingsStore.getState().setSettings({
|
useSettingsStore.getState().setSettings({
|
||||||
@@ -111,20 +111,6 @@ const QueueDrawerArea = styled(motion.div)`
|
|||||||
user-select: none;
|
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 {
|
interface DefaultLayoutProps {
|
||||||
shell?: boolean;
|
shell?: boolean;
|
||||||
}
|
}
|
||||||
@@ -183,10 +169,9 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
|
|||||||
x: '50vw',
|
x: '50vw',
|
||||||
},
|
},
|
||||||
open: {
|
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)',
|
height: 'calc(100vh - 150px)',
|
||||||
minWidth: '400px',
|
minWidth: '400px',
|
||||||
opacity: 0.98,
|
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
right: '20px',
|
right: '20px',
|
||||||
top: '50px',
|
top: '50px',
|
||||||
@@ -315,9 +300,7 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
|
|||||||
}, 50);
|
}, 50);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SideQueueContainer>
|
<DrawerPlayQueue />
|
||||||
<PlayQueue type="sideDrawerQueue" />
|
|
||||||
</SideQueueContainer>
|
|
||||||
</QueueDrawer>
|
</QueueDrawer>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
@@ -344,9 +327,7 @@ export const DefaultLayout = ({ shell }: DefaultLayoutProps) => {
|
|||||||
startResizing('right');
|
startResizing('right');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SideQueueContainer>
|
<SidebarPlayQueue />
|
||||||
<PlayQueue type="sideQueue" />
|
|
||||||
</SideQueueContainer>
|
|
||||||
</RightSidebarContainer>
|
</RightSidebarContainer>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import map from 'lodash/map';
|
import map from 'lodash/map';
|
||||||
|
import merge from 'lodash/merge';
|
||||||
import shuffle from 'lodash/shuffle';
|
import shuffle from 'lodash/shuffle';
|
||||||
import { nanoid } from 'nanoid/non-secure';
|
import { nanoid } from 'nanoid/non-secure';
|
||||||
import create from 'zustand';
|
import create from 'zustand';
|
||||||
@@ -59,16 +60,18 @@ export interface PlayerSlice extends PlayerState {
|
|||||||
autoNext: () => PlayerData;
|
autoNext: () => PlayerData;
|
||||||
checkIsFirstTrack: () => boolean;
|
checkIsFirstTrack: () => boolean;
|
||||||
checkIsLastTrack: () => boolean;
|
checkIsLastTrack: () => boolean;
|
||||||
// getNextTrack: () => QueueSong;
|
clearQueue: () => PlayerData;
|
||||||
// getPreviousTrack: () => QueueSong;
|
|
||||||
getPlayerData: () => PlayerData;
|
getPlayerData: () => PlayerData;
|
||||||
getQueueData: () => QueueData;
|
getQueueData: () => QueueData;
|
||||||
|
moveToBottomOfQueue: (uniqueIds: string[]) => PlayerData;
|
||||||
|
moveToTopOfQueue: (uniqueIds: string[]) => PlayerData;
|
||||||
next: () => PlayerData;
|
next: () => PlayerData;
|
||||||
pause: () => void;
|
pause: () => void;
|
||||||
play: () => void;
|
play: () => void;
|
||||||
player1: () => QueueSong | undefined;
|
player1: () => QueueSong | undefined;
|
||||||
player2: () => QueueSong | undefined;
|
player2: () => QueueSong | undefined;
|
||||||
previous: () => PlayerData;
|
previous: () => PlayerData;
|
||||||
|
removeFromQueue: (uniqueIds: string[]) => PlayerData;
|
||||||
reorderQueue: (rowUniqueIds: string[], afterUniqueId?: string) => PlayerData;
|
reorderQueue: (rowUniqueIds: string[], afterUniqueId?: string) => PlayerData;
|
||||||
setCurrentIndex: (index: number) => PlayerData;
|
setCurrentIndex: (index: number) => PlayerData;
|
||||||
setCurrentTime: (time: number) => void;
|
setCurrentTime: (time: number) => void;
|
||||||
@@ -79,6 +82,7 @@ export interface PlayerSlice extends PlayerState {
|
|||||||
setShuffledIndex: (index: number) => PlayerData;
|
setShuffledIndex: (index: number) => PlayerData;
|
||||||
setStore: (data: Partial<PlayerState>) => void;
|
setStore: (data: Partial<PlayerState>) => void;
|
||||||
setVolume: (volume: number) => void;
|
setVolume: (volume: number) => void;
|
||||||
|
shuffleQueue: () => PlayerData;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,6 +236,19 @@ export const usePlayerStore = create<PlayerSlice>()(
|
|||||||
|
|
||||||
return currentIndex === get().queue.default.length - 1;
|
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: () => {
|
getPlayerData: () => {
|
||||||
const queue = get().queue.default;
|
const queue = get().queue.default;
|
||||||
const currentPlayer = get().current.player;
|
const currentPlayer = get().current.player;
|
||||||
@@ -370,6 +387,46 @@ export const usePlayerStore = create<PlayerSlice>()(
|
|||||||
previous: queue[get().current.index - 1],
|
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: () => {
|
next: () => {
|
||||||
const isLastTrack = get().actions.checkIsLastTrack();
|
const isLastTrack = get().actions.checkIsLastTrack();
|
||||||
const repeat = get().repeat;
|
const repeat = get().repeat;
|
||||||
@@ -472,6 +529,17 @@ export const usePlayerStore = create<PlayerSlice>()(
|
|||||||
|
|
||||||
return get().actions.getPlayerData();
|
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) => {
|
reorderQueue: (rowUniqueIds: string[], afterUniqueId?: string) => {
|
||||||
// Don't move if dropping on top of a selected row
|
// Don't move if dropping on top of a selected row
|
||||||
if (afterUniqueId && rowUniqueIds.includes(afterUniqueId)) {
|
if (afterUniqueId && rowUniqueIds.includes(afterUniqueId)) {
|
||||||
@@ -633,6 +701,22 @@ export const usePlayerStore = create<PlayerSlice>()(
|
|||||||
state.volume = volume;
|
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: {
|
current: {
|
||||||
index: 0,
|
index: 0,
|
||||||
@@ -658,7 +742,13 @@ export const usePlayerStore = create<PlayerSlice>()(
|
|||||||
})),
|
})),
|
||||||
{ name: 'store_player' },
|
{ 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(
|
usePlayerStore(
|
||||||
(state) => ({
|
(state) => ({
|
||||||
addToQueue: state.actions.addToQueue,
|
addToQueue: state.actions.addToQueue,
|
||||||
|
clearQueue: state.actions.clearQueue,
|
||||||
|
moveToBottomOfQueue: state.actions.moveToBottomOfQueue,
|
||||||
|
moveToTopOfQueue: state.actions.moveToTopOfQueue,
|
||||||
|
removeFromQueue: state.actions.removeFromQueue,
|
||||||
reorderQueue: state.actions.reorderQueue,
|
reorderQueue: state.actions.reorderQueue,
|
||||||
setCurrentIndex: state.actions.setCurrentIndex,
|
setCurrentIndex: state.actions.setCurrentIndex,
|
||||||
setCurrentTrack: state.actions.setCurrentTrack,
|
setCurrentTrack: state.actions.setCurrentTrack,
|
||||||
setShuffledIndex: state.actions.setShuffledIndex,
|
setShuffledIndex: state.actions.setShuffledIndex,
|
||||||
|
shuffleQueue: state.actions.shuffleQueue,
|
||||||
}),
|
}),
|
||||||
shallow,
|
shallow,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user