mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 02:13:33 +00:00
Add files
This commit is contained in:
commit
e87c814068
266 changed files with 63938 additions and 0 deletions
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
236
src/renderer/features/now-playing/components/play-queue.tsx
Normal file
236
src/renderer/features/now-playing/components/play-queue.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
4
src/renderer/features/now-playing/index.ts
Normal file
4
src/renderer/features/now-playing/index.ts
Normal 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';
|
||||
|
|
@ -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;
|
||||
Loading…
Add table
Add a link
Reference in a new issue