mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 10:23:33 +00:00
* Remove 1920px max width * Fix position of list controls menu * Match size and color of search input * Adjust library header sizing * Move app menu to sidebar * Increase row buffer on play queue list * Fix query builder styles * Fix playerbar slider track bg * Adjust titlebar styles * Fix invalid modal prop * Various adjustments to detail pages * Fix sidebar height calculation * Fix list null indicators, add filter indicator * Adjust playqueue styles * Fix jellyfin releaseYear normalization * Suppress browser context menu on ag-grid * Add radius to drawer queue -- normalize layout * Add modal styles to provider theme * Fix playlist song list pagination * Add disc number to albums with more than one disc * Fix query builder boolean values * Adjust input placeholder color * Properly handle rating/favorite from context menu on table * Conform dropdown menu styles to context menu * Increase sort type select width * Fix drawer queue radius * Change primary color * Prevent volume wheel from invalid values * Add icons to query builder dropdowns * Update notification styles * Update scrollbar thumb styles * Remove "add to playlist" on smart playlists * Fix "add to playlist" from context menu
213 lines
6.4 KiB
TypeScript
213 lines
6.4 KiB
TypeScript
import type { Ref } from 'react';
|
|
import { useState, forwardRef, useEffect, useImperativeHandle, useMemo, useRef } from 'react';
|
|
import type {
|
|
CellDoubleClickedEvent,
|
|
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, getColumnDefs } from '/@/renderer/components';
|
|
import {
|
|
useAppStoreActions,
|
|
useCurrentSong,
|
|
useDefaultQueue,
|
|
usePreviousSong,
|
|
useQueueControls,
|
|
} from '/@/renderer/store';
|
|
import {
|
|
usePlayerType,
|
|
useSettingsStore,
|
|
useSettingsStoreActions,
|
|
useTableSettings,
|
|
} from '/@/renderer/store/settings.store';
|
|
import { useMergedRef } from '@mantine/hooks';
|
|
import isElectron from 'is-electron';
|
|
import debounce from 'lodash/debounce';
|
|
import { ErrorBoundary } from 'react-error-boundary';
|
|
import { VirtualTable } from '/@/renderer/components/virtual-table';
|
|
import { ErrorFallback } from '/@/renderer/features/action-required';
|
|
import { PlaybackType, TableType } from '/@/renderer/types';
|
|
import { QueueSong } from '/@/renderer/api/types';
|
|
|
|
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null;
|
|
|
|
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>();
|
|
const playerType = usePlayerType();
|
|
|
|
useEffect(() => {
|
|
if (tableRef.current) {
|
|
setGridApi(tableRef.current);
|
|
}
|
|
}, []);
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
get grid() {
|
|
return gridApi;
|
|
},
|
|
}));
|
|
|
|
const columnDefs = useMemo(() => getColumnDefs(tableConfig.columns), [tableConfig.columns]);
|
|
|
|
const handleDoubleClick = (e: CellDoubleClickedEvent) => {
|
|
const playerData = setCurrentTrack(e.data.uniqueId);
|
|
|
|
if (playerType === PlaybackType.LOCAL) {
|
|
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);
|
|
|
|
if (playerType === PlaybackType.LOCAL) {
|
|
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.getColDef().colId);
|
|
|
|
if (columnInSettings) {
|
|
updatedColumns.push({
|
|
...columnInSettings,
|
|
...(!useSettingsStore.getState().tables[type].autoFit && {
|
|
width: column.getActualWidth(),
|
|
}),
|
|
});
|
|
}
|
|
}
|
|
|
|
setSettings({
|
|
tables: {
|
|
...useSettingsStore.getState().tables,
|
|
[type]: {
|
|
...useSettingsStore.getState().tables[type],
|
|
columns: updatedColumns,
|
|
},
|
|
},
|
|
});
|
|
};
|
|
|
|
const debouncedColumnChange = debounce(handleColumnChange, 250);
|
|
|
|
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]);
|
|
|
|
return (
|
|
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
|
<VirtualGridAutoSizerContainer>
|
|
<VirtualTable
|
|
ref={mergedRef}
|
|
alwaysShowHorizontalScroll
|
|
rowDragEntireRow
|
|
rowDragMultiRow
|
|
autoFitColumns={tableConfig.autoFit}
|
|
columnDefs={columnDefs}
|
|
getRowId={(data) => data.data.uniqueId}
|
|
rowBuffer={50}
|
|
rowClassRules={rowClassRules}
|
|
rowData={queue}
|
|
rowHeight={tableConfig.rowHeight || 40}
|
|
onCellDoubleClicked={handleDoubleClick}
|
|
onColumnMoved={handleColumnChange}
|
|
onColumnResized={debouncedColumnChange}
|
|
onDragStarted={handleDragStart}
|
|
onGridReady={handleGridReady}
|
|
onGridSizeChanged={handleGridSizeChange}
|
|
onRowDragEnd={handleDragEnd}
|
|
/>
|
|
</VirtualGridAutoSizerContainer>
|
|
</ErrorBoundary>
|
|
);
|
|
});
|