From 9569ee6bcd66f253a7217f864ff2cf0b000f9454 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 11 Dec 2021 15:05:43 -0800 Subject: [PATCH] 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) --- src/components/library/AdvancedFilters.tsx | 130 +++++++++++++++++++++ src/components/library/AlbumList.tsx | 42 +++++-- src/components/shared/styled.ts | 10 +- src/hooks/useAdvancedFilter.ts | 60 ++++++++++ src/redux/albumSlice.ts | 28 ++++- 5 files changed, 259 insertions(+), 11 deletions(-) create mode 100644 src/components/library/AdvancedFilters.tsx create mode 100644 src/hooks/useAdvancedFilter.ts diff --git a/src/components/library/AdvancedFilters.tsx b/src/components/library/AdvancedFilters.tsx new file mode 100644 index 0000000..96910c7 --- /dev/null +++ b/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([]); + const genreFilterPickerContainerRef = useRef(); + + 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 ( +
+ Filters + { + dispatch(setAdvancedFilters({ ...filter, enabled: e })); + }} + > + Enabled + + { + dispatch( + setAdvancedFilters({ + ...filter, + properties: { + ...filter.properties, + starred: e, + }, + }) + ); + }} + > + Is favorite + +
+ Genres + { + dispatch( + setAdvancedFilters({ + ...filter, + properties: { + ...filter.properties, + genre: { + ...filter.properties.genre, + type: e, + }, + }, + }) + ); + }} + > + AND + OR + + + 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, + }, + }, + }) + ); + }} + /> + +
+ ); +}; + +export default AdvancedFilters; diff --git a/src/components/library/AlbumList.tsx b/src/components/library/AlbumList.tsx index 3bf7055..b2ab8a5 100644 --- a/src/components/library/AlbumList.tsx +++ b/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); }} /> - + + + + } + > + } /> + + />{' '} + {filteredData?.length} } @@ -263,7 +289,7 @@ const AlbumList = () => { {isError &&
Error: {error}
} {!isLoading && !isError && viewType === 'list' && ( { )} {!isLoading && !isError && viewType === 'grid' && ( - 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}; diff --git a/src/hooks/useAdvancedFilter.ts b/src/hooks/useAdvancedFilter.ts new file mode 100644 index 0000000..0295e95 --- /dev/null +++ b/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([]); + 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; diff --git a/src/redux/albumSlice.ts b/src/redux/albumSlice.ts index 9622690..e9a7463 100644 --- a/src/redux/albumSlice.ts +++ b/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) => { state.active = action.payload; }, + + setAdvancedFilters: (state, action: PayloadAction) => { + state.advancedFilters = action.payload; + }, }, }); -export const { setActive } = albumSlice.actions; +export const { setActive, setAdvancedFilters } = albumSlice.actions; export default albumSlice.reducer;