diff --git a/src/__tests__/App.test.tsx b/src/__tests__/App.test.tsx index 576e7d8..b1f7374 100644 --- a/src/__tests__/App.test.tsx +++ b/src/__tests__/App.test.tsx @@ -110,6 +110,9 @@ const configState: ConfigPage = { tab: 'playback', columnSelectorTab: 'music', }, + playback: { + filters: [], + }, lookAndFeel: { listView: { music: { diff --git a/src/components/settings/ConfigPanels/PlayerConfig.tsx b/src/components/settings/ConfigPanels/PlayerConfig.tsx index 95849e8..e6082a8 100644 --- a/src/components/settings/ConfigPanels/PlayerConfig.tsx +++ b/src/components/settings/ConfigPanels/PlayerConfig.tsx @@ -1,17 +1,70 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import settings from 'electron-settings'; -import { ControlLabel } from 'rsuite'; +import { ControlLabel, Form } from 'rsuite'; import { ConfigPanel } from '../styled'; -import { StyledCheckbox, StyledInputNumber } from '../../shared/styled'; -import { useAppDispatch } from '../../../redux/hooks'; +import { + StyledButton, + StyledCheckbox, + StyledInput, + StyledInputGroup, + StyledInputNumber, + StyledPanel, +} from '../../shared/styled'; +import { useAppDispatch, useAppSelector } from '../../../redux/hooks'; import { setPlaybackSetting } from '../../../redux/playQueueSlice'; +import ListViewTable from '../../viewtypes/ListViewTable'; +import { appendPlaybackFilter } from '../../../redux/configSlice'; + +const playbackFilterColumns = [ + { + id: '#', + dataKey: 'index', + alignment: 'center', + resizable: false, + width: 50, + label: '#', + }, + { + id: 'Filter', + dataKey: 'filter', + alignment: 'left', + resizable: false, + flexGrow: 2, + label: 'Filter', + }, + { + id: 'Enabled', + dataKey: 'filterEnabled', + alignment: 'left', + resizable: false, + width: 100, + label: 'Enabled', + }, + { + id: 'Delete', + dataKey: 'filterDelete', + alignment: 'left', + resizable: false, + width: 100, + label: 'Delete', + }, +]; const PlayerConfig = () => { const dispatch = useAppDispatch(); + const playQueue = useAppSelector((state) => state.playQueue); + const multiSelect = useAppSelector((state) => state.multiSelect); + const config = useAppSelector((state) => state.config); + const [newFilter, setNewFilter] = useState(''); const [globalMediaHotkeys, setGlobalMediaHotkeys] = useState( Boolean(settings.getSync('globalMediaHotkeys')) ); const [scrobble, setScrobble] = useState(Boolean(settings.getSync('scrobble'))); + + useEffect(() => { + settings.setSync('playbackFilters', config.playback.filters); + }, [config.playback.filters]); + return (

@@ -62,6 +115,64 @@ const PlayerConfig = () => { > Enable scrobbling +
+

Filters
+

+ Any song title that matches a filter will be automatically removed when being added to the + queue. +

+

+ * Adding to the queue by double-clicking a song will ignore filters for that one song +

+
+ +
+ + setNewFilter(e)} + placeholder="Enter regex string" + /> + { + dispatch(appendPlaybackFilter({ filter: newFilter, enabled: true })); + settings.setSync( + 'playbackFilters', + config.playback.filters.concat({ + filter: newFilter, + enabled: true, + }) + ); + setNewFilter(''); + }} + > + Add + + +
+ + {}} + handleRowDoubleClick={() => {}} + config={[]} + virtualized + /> +
); }; diff --git a/src/components/shared/setDefaultSettings.ts b/src/components/shared/setDefaultSettings.ts index 490800b..ca63cac 100644 --- a/src/components/shared/setDefaultSettings.ts +++ b/src/components/shared/setDefaultSettings.ts @@ -146,6 +146,19 @@ const setDefaultSettings = (force: boolean) => { settings.setSync('randomPlaylistTrackCount', 50); } + if (force || !settings.hasSync('playbackFilters')) { + settings.setSync('playbackFilters', [ + { + filter: '(\\(|\\[|~|-|()[Oo]ff [Vv]ocal(\\)|\\]|~|-|))', + enabled: true, + }, + { + filter: '((|\\(|\\[|~|-)[Ii]nst(rumental)?(\\)|\\]|~|-|))', + enabled: true, + }, + ]); + } + if (force || !settings.hasSync('musicListColumns')) { settings.setSync('musicListColumns', [ { diff --git a/src/components/viewtypes/ListViewTable.tsx b/src/components/viewtypes/ListViewTable.tsx index 4b2bb41..458f70b 100644 --- a/src/components/viewtypes/ListViewTable.tsx +++ b/src/components/viewtypes/ListViewTable.tsx @@ -25,7 +25,7 @@ import { import cacheImage from '../shared/cacheImage'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { fixPlayer2Index, setSort, sortPlayQueue } from '../../redux/playQueueSlice'; -import { StyledCheckbox, StyledIconToggle, StyledRate } from '../shared/styled'; +import { StyledCheckbox, StyledIconButton, StyledIconToggle, StyledRate } from '../shared/styled'; import { addModalPage, setContextMenu } from '../../redux/miscSlice'; import { clearSelected, @@ -40,7 +40,7 @@ import { } from '../../redux/multiSelectSlice'; import CustomTooltip from '../shared/CustomTooltip'; import { sortPlaylist } from '../../redux/playlistSlice'; -import { setColumnList } from '../../redux/configSlice'; +import { removePlaybackFilter, setColumnList, setPlaybackFilter } from '../../redux/configSlice'; import { setActive } from '../../redux/albumSlice'; const StyledTable = styled(Table)<{ rowHeight: number; $isDragging: boolean }>` @@ -902,6 +902,46 @@ const ListViewTable = ({ ) ) : column.dataKey === 'custom' ? (
{column.custom}
+ ) : column.dataKey === 'filter' ? ( +
{rowData.filter}
+ ) : column.dataKey === 'filterDelete' ? ( + <> + } + onClick={() => { + dispatch(removePlaybackFilter({ filterName: rowData.filter })); + }} + /> + + ) : column.dataKey === 'filterEnabled' ? ( + <> + f.filter === rowData.filter + )?.enabled === true + } + checked={ + configState.playback.filters.find( + (f: any) => f.filter === rowData.filter + )?.enabled === true + } + onChange={(_v: any, e: boolean) => { + dispatch( + setPlaybackFilter({ + filterName: rowData.filter, + newFilter: { + ...configState.playback.filters.find( + (f: any) => f.filter === rowData.filter + ), + enabled: e, + }, + }) + ); + }} + /> + ) : column.dataKey === 'columnResizable' ? (
) => { + if (!state.playback.filters.find((f: PlaybackFilter) => f.filter === action.payload.filter)) { + state.playback.filters.push(action.payload); + } + }, + + setPlaybackFilter: ( + state, + action: PayloadAction<{ filterName: string; newFilter: PlaybackFilter }> + ) => { + const selectedFilterIndex = state.playback.filters.findIndex( + (f: PlaybackFilter) => f.filter === action.payload.filterName + ); + + state.playback.filters[selectedFilterIndex] = action.payload.newFilter; + }, + + removePlaybackFilter: (state, action: PayloadAction<{ filterName: string }>) => { + state.playback.filters = state.playback.filters.filter( + (f: PlaybackFilter) => f.filter !== action.payload.filterName + ); + }, + + setPlaybackFilters: (state, action: PayloadAction) => { + state.playback.filters = action.payload; + }, + setColumnList: (state, action: PayloadAction<{ listType: ColumnList; entries: any }>) => { state.lookAndFeel.listView[action.payload.listType].columns = action.payload.entries; }, @@ -127,6 +165,10 @@ const configSlice = createSlice({ export const { setActive, + appendPlaybackFilter, + removePlaybackFilter, + setPlaybackFilter, + setPlaybackFilters, setColumnList, setRowHeight, setFontSize, diff --git a/src/shared/mockSettings.ts b/src/shared/mockSettings.ts index 8518322..f74f1f5 100644 --- a/src/shared/mockSettings.ts +++ b/src/shared/mockSettings.ts @@ -16,6 +16,12 @@ export const mockSettings = { fadeDuration: 9, fadeType: 'equalPower', scrobble: false, + playbackFilters: [ + { + filter: '((|\\(|\\[|~|-)[Ii]nst(rumental)?(\\)|\\]|~|-|))', + enabled: true, + }, + ], musicFolder: { id: null, albums: true,