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
+
+
+
+
+
+ {}}
+ 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,