Browse Source

Add initial advanced filters for album list

- Add check picker component
- Add filter hook
- Add filter component
- Add support for favorite filter
- Add support for genre filter (AND/OR)
master
jeffvli 3 years ago
committed by Jeff
parent
commit
9569ee6bcd
  1. 130
      src/components/library/AdvancedFilters.tsx
  2. 42
      src/components/library/AlbumList.tsx
  3. 10
      src/components/shared/styled.ts
  4. 60
      src/hooks/useAdvancedFilter.ts
  5. 28
      src/redux/albumSlice.ts

130
src/components/library/AdvancedFilters.tsx

@ -0,0 +1,130 @@
/* eslint-disable react/destructuring-assignment */
import _ from 'lodash';
import React, { useEffect, useRef, useState } from 'react';
import { RadioGroup } from 'rsuite';
import styled from 'styled-components';
import { useAppDispatch } from '../../redux/hooks';
import {
StyledCheckbox,
StyledCheckPicker,
StyledInputPickerContainer,
StyledRadio,
} from '../shared/styled';
const FilterHeader = styled.h1`
font-size: 16px;
line-height: unset;
`;
const AdvancedFilters = ({ filteredData, originalData, filter, setAdvancedFilters }: any) => {
const dispatch = useAppDispatch();
const [availableGenres, setAvailableGenres] = useState<any[]>([]);
const genreFilterPickerContainerRef = useRef<any>();
useEffect(() => {
setAvailableGenres(
_.orderBy(
_.uniqBy(
_.flatten(
_.map(
filter.properties.starred || filter.properties.genre.type === 'and'
? filteredData
: originalData,
'genre'
)
),
'title'
),
[
(entry: any) => {
return typeof entry.title === 'string'
? entry.title.toLowerCase() || ''
: +entry.title || '';
},
]
)
);
}, [filter.properties.genre.type, filter.properties.starred, filteredData, originalData]);
return (
<div>
<FilterHeader>Filters</FilterHeader>
<StyledCheckbox
defaultChecked={filter.enabled}
checked={filter.enabled}
onChange={(_v: any, e: boolean) => {
dispatch(setAdvancedFilters({ ...filter, enabled: e }));
}}
>
Enabled
</StyledCheckbox>
<StyledCheckbox
defaultChecked={filter.properties.starred}
checked={filter.properties.starred}
onChange={(_v: any, e: boolean) => {
dispatch(
setAdvancedFilters({
...filter,
properties: {
...filter.properties,
starred: e,
},
})
);
}}
>
Is favorite
</StyledCheckbox>
<br />
<FilterHeader>Genres</FilterHeader>
<RadioGroup
inline
defaultValue={filter.properties.genre.type}
onChange={(e: string) => {
dispatch(
setAdvancedFilters({
...filter,
properties: {
...filter.properties,
genre: {
...filter.properties.genre,
type: e,
},
},
})
);
}}
>
<StyledRadio value="and">AND</StyledRadio>
<StyledRadio value="or">OR</StyledRadio>
</RadioGroup>
<StyledInputPickerContainer ref={genreFilterPickerContainerRef}>
<StyledCheckPicker
container={() => genreFilterPickerContainerRef.current}
data={availableGenres}
value={filter.properties.genre.list}
labelKey="title"
valueKey="title"
sticky
style={{ width: '250px' }}
onChange={(e: string[]) => {
dispatch(
setAdvancedFilters({
...filter,
properties: {
...filter.properties,
genre: {
...filter.properties.genre,
list: e,
},
},
})
);
}}
/>
</StyledInputPickerContainer>
</div>
);
};
export default AdvancedFilters;

42
src/components/library/AlbumList.tsx

@ -1,7 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import _ from 'lodash';
import settings from 'electron-settings';
import { ButtonToolbar } from 'rsuite';
import { ButtonToolbar, Icon, Whisper } from 'rsuite';
import { useQuery, useQueryClient } from 'react-query';
import { useHistory } from 'react-router-dom';
import GridViewType from '../viewtypes/GridViewType';
@ -17,12 +17,19 @@ import {
toggleRangeSelected,
clearSelected,
} from '../../redux/multiSelectSlice';
import { StyledInputPicker, StyledInputPickerContainer } from '../shared/styled';
import {
StyledIconButton,
StyledInputPicker,
StyledInputPickerContainer,
StyledPopover,
} from '../shared/styled';
import { RefreshButton } from '../shared/ToolbarButtons';
import { setActive } from '../../redux/albumSlice';
import { setActive, setAdvancedFilters } from '../../redux/albumSlice';
import { setSearchQuery } from '../../redux/miscSlice';
import { apiController } from '../../api/controller';
import { Server } from '../../types';
import AdvancedFilters from './AdvancedFilters';
import useAdvancedFilter from '../../hooks/useAdvancedFilter';
const ALBUM_SORT_TYPES = [
{ label: 'A-Z (Name)', value: 'alphabeticalByName', role: 'Default' },
@ -118,13 +125,15 @@ const AlbumList = () => {
});
});
const filteredData = useSearchQuery(misc.searchQuery, albums, [
const searchedData = useSearchQuery(misc.searchQuery, albums, [
'title',
'artist',
'genre',
'year',
]);
const filteredData = useAdvancedFilter(albums, album.advancedFilters);
useEffect(() => {
setSortTypes(_.compact(_.concat(ALBUM_SORT_TYPES, genres)));
}, [genres]);
@ -241,13 +250,30 @@ const AlbumList = () => {
setIsRefresh(false);
}}
/>
<Whisper
trigger="click"
enterable
placement="bottom"
speaker={
<StyledPopover width="400px">
<AdvancedFilters
filteredData={filteredData}
originalData={albums}
filter={album.advancedFilters}
setAdvancedFilters={setAdvancedFilters}
/>
</StyledPopover>
}
>
<StyledIconButton size="sm" icon={<Icon icon="filter" />} />
</Whisper>
<RefreshButton
onClick={handleRefresh}
size="sm"
loading={isRefreshing}
width={100}
/>
/>{' '}
{filteredData?.length}
</ButtonToolbar>
</StyledInputPickerContainer>
}
@ -263,7 +289,7 @@ const AlbumList = () => {
{isError && <div>Error: {error}</div>}
{!isLoading && !isError && viewType === 'list' && (
<ListViewType
data={misc.searchQuery !== '' ? filteredData : albums}
data={misc.searchQuery !== '' ? searchedData : filteredData}
tableColumns={config.lookAndFeel.listView.album.columns}
rowHeight={config.lookAndFeel.listView.album.rowHeight}
fontSize={config.lookAndFeel.listView.album.fontSize}
@ -293,7 +319,7 @@ const AlbumList = () => {
)}
{!isLoading && !isError && viewType === 'grid' && (
<GridViewType
data={misc.searchQuery !== '' ? filteredData : albums}
data={misc.searchQuery !== '' ? searchedData : filteredData}
cardTitle={{
prefix: '/library/album',
property: 'title',

10
src/components/shared/styled.ts

@ -15,6 +15,7 @@ import {
Panel,
TagPicker,
Tag,
CheckPicker,
} from 'rsuite';
import styled from 'styled-components';
import TagLink from './TagLink';
@ -163,7 +164,9 @@ export const StyledCheckbox = styled(Checkbox)`
span {
&:before {
background-color: ${(props) =>
props.defaultChecked ? `${props.theme.colors.primary} !important` : undefined};
props.checked || props.defaultChecked
? `${props.theme.colors.primary} !important`
: undefined};
}
&:after {
border: transparent !important;
@ -438,12 +441,13 @@ export const ContextMenuPopover = styled(Popover)`
z-index: 2000;
`;
export const StyledPopover = styled(Popover)`
export const StyledPopover = styled(Popover)<{ width?: string }>`
color: ${(props) => props.theme.colors.popover.color};
background: ${(props) => props.theme.colors.popover.background};
border: 1px #3c4043 solid;
position: absolute;
z-index: 1000;
width: ${(props) => props.width};
`;
export const SectionTitleWrapper = styled.div`
@ -523,6 +527,8 @@ export const StyledTagPicker = styled(TagPicker)`
}
`;
export const StyledCheckPicker = styled(CheckPicker)``;
export const StyledTag = styled(Tag)`
color: ${(props) => props.theme.colors.tag.text} !important;
background: ${(props) => props.theme.colors.tag.background};

60
src/hooks/useAdvancedFilter.ts

@ -0,0 +1,60 @@
import { useState, useEffect } from 'react';
import _ from 'lodash';
interface AdvancedFilters {
enabled: boolean;
properties: {
starred: boolean;
genre: {
list: any[];
type: 'or' | 'and';
};
};
}
const useAdvancedFilter = (data: any[], filters: AdvancedFilters) => {
const [filteredData, setFilteredData] = useState<any[]>([]);
const [filterProps, setFilterProps] = useState(filters);
useEffect(() => {
setFilterProps(filters);
if (filterProps.enabled) {
// Favorite/Star filter
const filteredByStarred = filterProps.properties.starred
? (data || []).filter((entry) => {
return entry.starred !== undefined;
})
: data;
// Genre filter
const genreRegex = new RegExp(filterProps.properties?.genre?.list.join('|'), 'i');
const filteredByGenres =
filterProps.properties.genre.list.length > 0
? (filteredByStarred || []).filter((entry) => {
const entryGenres = _.map(entry.genre, 'title');
if (filterProps.properties.genre.type === 'or') {
return entryGenres.some((genre) => genre.match(genreRegex));
}
const matches = [];
for (let i = 0; i < filterProps.properties.genre.list.length; i += 1) {
if (entryGenres.includes(filterProps.properties.genre.list[i])) {
matches.push(entry);
}
}
return matches.length === filterProps.properties.genre.list.length;
})
: filteredByStarred;
setFilteredData(_.compact(_.uniqBy(filteredByGenres, 'uniqueId')));
} else {
setFilteredData(data);
}
}, [data, filterProps, filters]);
return filteredData;
};
export default useAdvancedFilter;

28
src/redux/albumSlice.ts

@ -4,12 +4,34 @@ export interface AlbumPage {
active: {
filter: string;
};
advancedFilters: AdvancedFilters;
}
export interface AdvancedFilters {
enabled: boolean;
properties: {
starred: boolean;
genre: {
list: any[];
type: 'and' | 'or';
};
};
}
const initialState: AlbumPage = {
active: {
filter: 'random',
},
advancedFilters: {
enabled: false,
properties: {
starred: false,
genre: {
list: [],
type: 'and',
},
},
},
};
const albumSlice = createSlice({
@ -19,8 +41,12 @@ const albumSlice = createSlice({
setActive: (state, action: PayloadAction<any>) => {
state.active = action.payload;
},
setAdvancedFilters: (state, action: PayloadAction<any>) => {
state.advancedFilters = action.payload;
},
},
});
export const { setActive } = albumSlice.actions;
export const { setActive, setAdvancedFilters } = albumSlice.actions;
export default albumSlice.reducer;

Loading…
Cancel
Save