Add files

This commit is contained in:
jeffvli 2022-12-19 15:59:14 -08:00
commit e87c814068
266 changed files with 63938 additions and 0 deletions

View file

@ -0,0 +1,26 @@
import { useRef } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Stack } from '@mantine/core';
import { PlayQueueListControls } from './play-queue-list-controls';
import { Song } from '/@/renderer/api/types';
import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue';
export const DrawerPlayQueue = () => {
const queueRef = useRef<{ grid: AgGridReactType<Song> } | null>(null);
return (
<Stack
pb="1rem"
sx={{ height: '100%' }}
>
<PlayQueue
ref={queueRef}
type="sideQueue"
/>
<PlayQueueListControls
tableRef={queueRef}
type="sideQueue"
/>
</Stack>
);
};

View file

@ -0,0 +1,52 @@
import { useEffect, useState } from 'react';
import { Group } from '@mantine/core';
import { FastAverageColor } from 'fast-average-color';
import { PageHeader, Text } from '/@/renderer/components';
import { useCurrentSong } from '/@/renderer/store';
import { getHeaderColor } from '/@/renderer/utils';
import { useTheme } from '/@/renderer/hooks';
export const NowPlayingHeader = () => {
const [headerColor, setHeaderColor] = useState({ isDark: false, value: 'rgba(0, 0, 0, 0)' });
const currentSong = useCurrentSong();
const theme = useTheme();
useEffect(() => {
const fac = new FastAverageColor();
const url = currentSong?.imageUrl;
if (url) {
fac
.getColorAsync(currentSong?.imageUrl, {
algorithm: 'simple',
ignoredColor: [
[255, 255, 255, 255], // White
[0, 0, 0, 255], // Black
],
mode: 'precision',
})
.then((color) => {
const isDark = color.isDark;
setHeaderColor({
isDark,
value: getHeaderColor(color.rgb, theme === 'dark' ? 0.5 : 0.8),
});
})
.catch((e) => {
console.log(e);
});
}
return () => {
fac.destroy();
};
}, [currentSong?.imageUrl, theme]);
return (
<PageHeader backgroundColor={headerColor.value}>
<Group>
<Text size="xl">Queue</Text>
</Group>
</PageHeader>
);
};

View file

@ -0,0 +1,138 @@
import type { MutableRefObject } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Group } from '@mantine/core';
import { Button, Popover, TableConfigDropdown } from '/@/renderer/components';
import {
RiArrowDownLine,
RiArrowUpLine,
RiShuffleLine,
RiDeleteBinLine,
RiListSettingsLine,
RiEraserLine,
} from 'react-icons/ri';
import { Song } from '/@/renderer/api/types';
import { useQueueControls } from '/@/renderer/store';
import { TableType } from '/@/renderer/types';
const mpvPlayer = window.electron.mpvPlayer;
interface PlayQueueListOptionsProps {
tableRef: MutableRefObject<{ grid: AgGridReactType<Song> } | null>;
type: TableType;
}
export const PlayQueueListControls = ({ type, tableRef }: PlayQueueListOptionsProps) => {
const { clearQueue, moveToBottomOfQueue, moveToTopOfQueue, shuffleQueue, removeFromQueue } =
useQueueControls();
const handleMoveToBottom = () => {
const selectedRows = tableRef?.current?.grid.api.getSelectedRows();
const uniqueIds = selectedRows?.map((row) => row.uniqueId);
if (!uniqueIds?.length) return;
const playerData = moveToBottomOfQueue(uniqueIds);
mpvPlayer.setQueueNext(playerData);
};
const handleMoveToTop = () => {
const selectedRows = tableRef?.current?.grid.api.getSelectedRows();
const uniqueIds = selectedRows?.map((row) => row.uniqueId);
if (!uniqueIds?.length) return;
const playerData = moveToTopOfQueue(uniqueIds);
mpvPlayer.setQueueNext(playerData);
};
const handleRemoveSelected = () => {
const selectedRows = tableRef?.current?.grid.api.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>
);
};

View file

@ -0,0 +1,236 @@
import type { Ref } from 'react';
import { useState, forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
import type {
CellDoubleClickedEvent,
ColDef,
RowClassRules,
RowDragEvent,
RowNode,
} from '@ag-grid-community/core';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import '@ag-grid-community/styles/ag-theme-alpine.css';
import {
VirtualGridAutoSizerContainer,
VirtualGridContainer,
getColumnDefs,
} from '/@/renderer/components';
import {
useAppStoreActions,
useCurrentSong,
useDefaultQueue,
usePreviousSong,
useQueueControls,
} from '/@/renderer/store';
import {
useSettingsStore,
useSettingsStoreActions,
useTableSettings,
} from '/@/renderer/store/settings.store';
import { useMergedRef } from '@mantine/hooks';
import { ErrorBoundary } from 'react-error-boundary';
import { VirtualTable } from '/@/renderer/components/virtual-table';
import { ErrorFallback } from '/@/renderer/features/action-required';
import { TableType, QueueSong } from '/@/renderer/types';
const mpvPlayer = window.electron.mpvPlayer;
type QueueProps = {
type: TableType;
};
export const PlayQueue = forwardRef(({ type }: QueueProps, ref: Ref<any>) => {
const tableRef = useRef<AgGridReactType | null>(null);
const mergedRef = useMergedRef(ref, tableRef);
const queue = useDefaultQueue();
const { reorderQueue, setCurrentTrack } = useQueueControls();
const currentSong = useCurrentSong();
const previousSong = usePreviousSong();
const { setSettings } = useSettingsStoreActions();
const { setAppStore } = useAppStoreActions();
const tableConfig = useTableSettings(type);
const [gridApi, setGridApi] = useState<AgGridReactType | undefined>();
useEffect(() => {
if (tableRef.current) {
setGridApi(tableRef.current);
}
}, []);
useImperativeHandle(ref, () => ({
get grid() {
return gridApi;
},
}));
const columnDefs = useMemo(() => getColumnDefs(tableConfig.columns), [tableConfig.columns]);
const defaultColumnDefs: ColDef = useMemo(() => {
return {
lockPinned: true,
lockVisible: true,
resizable: true,
};
}, []);
const handlePlayByRowClick = (e: CellDoubleClickedEvent) => {
const playerData = setCurrentTrack(e.data.uniqueId);
mpvPlayer.setQueue(playerData);
};
const handleDragStart = () => {
if (type === 'sideDrawerQueue') {
setAppStore({ isReorderingQueue: true });
}
};
let timeout: any;
const handleDragEnd = (e: RowDragEvent<QueueSong>) => {
if (!e.nodes.length) return;
const selectedUniqueIds = e.nodes
.map((node) => node.data?.uniqueId)
.filter((e) => e !== undefined);
const playerData = reorderQueue(selectedUniqueIds as string[], e.overNode?.data?.uniqueId);
mpvPlayer.setQueueNext(playerData);
if (type === 'sideDrawerQueue') {
setAppStore({ isReorderingQueue: false });
}
const { api } = tableRef?.current || {};
clearTimeout(timeout);
timeout = setTimeout(() => api?.redrawRows(), 250);
};
const handleGridReady = () => {
const { api } = tableRef?.current || {};
if (currentSong?.uniqueId) {
const currentNode = api?.getRowNode(currentSong?.uniqueId);
if (!currentNode) return;
api?.ensureNodeVisible(currentNode, 'middle');
}
};
const handleColumnChange = () => {
const { columnApi } = tableRef?.current || {};
const columnsOrder = columnApi?.getAllGridColumns();
if (!columnsOrder) return;
const columnsInSettings = useSettingsStore.getState().tables[type].columns;
const updatedColumns = [];
for (const column of columnsOrder) {
const columnInSettings = columnsInSettings.find((c) => c.column === column.colId);
if (columnInSettings) {
updatedColumns.push({
...columnInSettings,
...(!useSettingsStore.getState().tables[type].autoFit && {
width: column.actualWidth,
}),
});
}
}
setSettings({
tables: {
...useSettingsStore.getState().tables,
[type]: {
...useSettingsStore.getState().tables[type],
columns: updatedColumns,
},
},
});
};
const handleGridSizeChange = () => {
if (tableConfig.autoFit) {
tableRef?.current?.api.sizeColumnsToFit();
}
};
const rowClassRules = useMemo<RowClassRules>(() => {
return {
'current-song': (params) => {
return params.data.uniqueId === currentSong?.uniqueId;
},
};
}, [currentSong?.uniqueId]);
// Redraw the current song row when the previous song changes
useEffect(() => {
if (tableRef?.current) {
const { api, columnApi } = tableRef?.current || {};
if (api == null || columnApi == null) {
return;
}
const currentNode = currentSong?.uniqueId ? api.getRowNode(currentSong.uniqueId) : undefined;
const previousNode = previousSong?.uniqueId
? api.getRowNode(previousSong?.uniqueId)
: undefined;
const rowNodes = [currentNode, previousNode].filter((e) => e !== undefined) as RowNode<any>[];
if (rowNodes) {
api.redrawRows({ rowNodes });
if (tableConfig.followCurrentSong) {
if (!currentNode) return;
api.ensureNodeVisible(currentNode, 'middle');
}
}
}
}, [currentSong, previousSong, tableConfig.followCurrentSong]);
// Auto resize the columns when the column config changes
useEffect(() => {
if (tableConfig.autoFit) {
const { api } = tableRef?.current || {};
api?.sizeColumnsToFit();
}
}, [tableConfig.autoFit, tableConfig.columns]);
useEffect(() => {
const { api } = tableRef?.current || {};
api?.resetRowHeights();
api?.redrawRows();
}, [tableConfig.rowHeight]);
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<VirtualGridContainer>
<VirtualGridAutoSizerContainer>
<VirtualTable
ref={mergedRef}
alwaysShowHorizontalScroll
animateRows
maintainColumnOrder
rowDragEntireRow
rowDragMultiRow
suppressCopyRowsToClipboard
suppressMoveWhenRowDragging
suppressRowDrag
suppressScrollOnNewData
columnDefs={columnDefs}
defaultColDef={defaultColumnDefs}
enableCellChangeFlash={false}
getRowId={(data) => data.data.uniqueId}
rowBuffer={30}
rowClassRules={rowClassRules}
rowData={queue}
rowHeight={tableConfig.rowHeight || 40}
rowSelection="multiple"
onCellDoubleClicked={handlePlayByRowClick}
onColumnMoved={handleColumnChange}
onColumnResized={handleColumnChange}
onDragStarted={handleDragStart}
onGridReady={handleGridReady}
onGridSizeChanged={handleGridSizeChange}
onRowDragEnd={handleDragEnd}
/>
</VirtualGridAutoSizerContainer>
</VirtualGridContainer>
</ErrorBoundary>
);
});

View file

@ -0,0 +1,27 @@
import { useRef } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Stack } from '@mantine/core';
import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue';
import { PlayQueueListControls } from './play-queue-list-controls';
import { Song } from '/@/renderer/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
tableRef={queueRef}
type="sideQueue"
/>
</Stack>
);
};

View file

@ -0,0 +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';

View file

@ -0,0 +1,35 @@
import { useRef } from 'react';
import type { AgGridReact as AgGridReactType } from '@ag-grid-community/react/lib/agGridReact';
import { Stack } from '@mantine/core';
import { NowPlayingHeader } from '/@/renderer/features/now-playing/components/now-playing-header';
import { PlayQueue } from '/@/renderer/features/now-playing/components/play-queue';
import { PlayQueueListControls } from '/@/renderer/features/now-playing/components/play-queue-list-controls';
import type { Song } from '/@/renderer/api/types';
import { AnimatedPage } from '/@/renderer/features/shared';
const NowPlayingRoute = () => {
const queueRef = useRef<{ grid: AgGridReactType<Song> } | null>(null);
return (
<AnimatedPage>
<Stack
pb="1rem"
spacing={0}
sx={{ height: '100%' }}
>
<NowPlayingHeader />
<PlayQueue
ref={queueRef}
type="nowPlaying"
/>
<PlayQueueListControls
tableRef={queueRef}
type="nowPlaying"
/>
</Stack>
</AnimatedPage>
);
};
export default NowPlayingRoute;