diff --git a/src/renderer/features/albums/components/advanced-filters.tsx b/src/renderer/features/albums/components/advanced-filters.tsx
new file mode 100644
index 000000000..4af325d7d
--- /dev/null
+++ b/src/renderer/features/albums/components/advanced-filters.tsx
@@ -0,0 +1,467 @@
+import { useState } from 'react';
+import { Box, Stack, Group } from '@mantine/core';
+import _ from 'lodash';
+import { nanoid } from 'nanoid/non-secure';
+import { RiAddLine, RiMore2Line, RiSubtractLine } from 'react-icons/ri';
+import {
+ Button,
+ DropdownMenu,
+ NumberInput,
+ Select,
+ TextInput,
+} from '@/renderer/components';
+
+enum FilterGroupType {
+ AND = 'and',
+ OR = 'or',
+}
+
+type AdvancedFilterRule = {
+ field: string;
+ operator: string;
+ uniqueId: string;
+ value: string;
+};
+
+type AdvancedFilterGroup = {
+ group: AdvancedFilterGroup[];
+ rules: AdvancedFilterRule[];
+ type: FilterGroupType;
+ uniqueId: string;
+};
+
+const DATE_FILTER_OPTIONS_DATA = [
+ { label: 'is before', value: '<' },
+ { label: 'is after', value: '>' },
+];
+
+const STRING_FILTER_OPTIONS_DATA = [
+ { label: 'contains', value: '~' },
+ { label: 'does not contain', value: '!~' },
+ { label: 'is', value: '=' },
+ { label: 'is not', value: '!=' },
+ { label: 'starts with', value: '^' },
+ { label: 'ends with', value: '$' },
+];
+
+const NUMBER_FILTER_OPTIONS_DATA = [
+ { label: 'is', value: '=' },
+ { label: 'is not', value: '!=' },
+ { label: 'is greater than', value: '>' },
+ { label: 'is less than', value: '<' },
+];
+
+const FILTER_GROUP_OPTIONS_DATA = [
+ {
+ label: 'Match ALL',
+ value: FilterGroupType.AND,
+ },
+ {
+ label: 'Match ANY',
+ value: FilterGroupType.OR,
+ },
+];
+
+const FILTER_OPTIONS_DATA = [
+ {
+ label: 'Artist Title',
+ value: 'artist.title',
+ },
+ {
+ label: 'Artist Rating',
+ value: 'artist.rating',
+ },
+ {
+ label: 'Artist Genre',
+ value: 'artist.genre',
+ },
+ {
+ label: 'Album Title',
+ value: 'album.title',
+ },
+ {
+ label: 'Album Genre',
+ value: 'album.genre',
+ },
+ {
+ label: 'Album Rating',
+ value: 'album.rating',
+ },
+ {
+ label: 'Album Year',
+ value: 'album.year',
+ },
+ {
+ label: 'Album Release Date',
+ value: 'album.releaseDate',
+ },
+ {
+ label: 'Album Plays',
+ value: 'album.playCount',
+ },
+ {
+ label: 'Album Date Added',
+ value: 'album.dateAdded',
+ },
+ {
+ label: 'Track Title',
+ value: 'track.title',
+ },
+ {
+ label: 'Track Plays',
+ value: 'track.plays',
+ },
+ {
+ label: 'Track Rating',
+ value: 'track.rating',
+ },
+];
+
+const OPTIONS_MAP = {
+ 'album.dateAdded': {
+ type: 'date',
+ },
+ 'album.favorite': {
+ type: 'boolean',
+ },
+ 'album.genre': {
+ type: 'string',
+ },
+ 'album.playCount': {
+ type: 'number',
+ },
+ 'album.rating': {
+ type: 'number',
+ },
+ 'album.releaseDate': {
+ type: 'date',
+ },
+ 'album.title': {
+ type: 'string',
+ },
+ 'album.year': {
+ type: 'number',
+ },
+ 'artist.genre': {
+ type: 'string',
+ },
+ 'artist.rating': {
+ type: 'number',
+ },
+ 'artist.title': {
+ type: 'string',
+ },
+ 'track.plays': {
+ type: 'number',
+ },
+ 'track.rating': {
+ type: 'number',
+ },
+ 'track.title': {
+ type: 'string',
+ },
+};
+
+const FilterOption = ({ level, onDeleteRule, uniqueId, groupIndex }: any) => {
+ const [selectedOption, setSelectedOption] = useState<
+ string | null | typeof OPTIONS_MAP
+ >(FILTER_OPTIONS_DATA[0].value);
+
+ const handleDeleteRule = () => {
+ onDeleteRule({ groupIndex, level, uniqueId });
+ };
+
+ const filterMap = {
+ date: ,
+ number: ,
+ string: ,
+ };
+
+ const filterInputMap = {
+ 'album.dateAdded': ,
+ 'album.genre': ,
+ 'album.playCount': ,
+ 'album.rating': ,
+ 'album.releaseDate': ,
+ 'album.title': ,
+ 'album.year': ,
+ 'artist.genre': ,
+ 'artist.rating': ,
+ 'artist.title': ,
+ 'track.plays': ,
+ 'track.rating': ,
+ 'track.title': ,
+ };
+
+ return (
+
+
+ {selectedOption &&
+ filterMap[
+ OPTIONS_MAP[selectedOption as keyof typeof OPTIONS_MAP]
+ .type as keyof typeof filterMap
+ ]}
+ {selectedOption &&
+ filterInputMap[selectedOption as keyof typeof filterInputMap]}
+
+
+ );
+};
+
+type AddArgs = {
+ groupIndex: number[];
+ level: number;
+};
+
+type DeleteArgs = {
+ groupIndex: number[];
+ level: number;
+ uniqueId: string;
+};
+
+interface FilterGroupProps {
+ data: AdvancedFilterGroup;
+ groupIndex: number[];
+ level: number;
+ onAddRule: (args: AddArgs) => void;
+ onAddRuleGroup: (args: AddArgs) => void;
+ onDeleteRule: (args: DeleteArgs) => void;
+ onDeleteRuleGroup: (args: DeleteArgs) => void;
+ uniqueId: string;
+}
+
+const FilterGroup = ({
+ data,
+ level,
+ onAddRule,
+ onDeleteRuleGroup,
+ onDeleteRule,
+ onAddRuleGroup,
+ groupIndex,
+ uniqueId,
+}: FilterGroupProps) => {
+ const handleAddRule = () => {
+ onAddRule({ groupIndex, level });
+ };
+
+ const handleAddRuleGroup = () => {
+ onAddRuleGroup({ groupIndex, level });
+ };
+
+ const handleDeleteRuleGroup = () => {
+ onDeleteRuleGroup({ groupIndex, level, uniqueId });
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ Add rule group
+
+ {level > 0 && (
+
+ Remove rule group
+
+ )}
+
+
+
+ {data.rules.map((rule: AdvancedFilterRule) => (
+ <>
+
+ >
+ ))}
+ {data.group && (
+ <>
+ {data.group.map((group: AdvancedFilterGroup, index: number) => (
+
+ ))}
+ >
+ )}
+
+ );
+};
+
+export const AdvancedFilters = () => {
+ const [filters, setFilters] = useState({
+ group: [],
+ rules: [],
+ type: FilterGroupType.AND,
+ uniqueId: nanoid(),
+ });
+
+ const handleAddRuleGroup = (args: AddArgs) => {
+ const { level, groupIndex } = args;
+ const filtersCopy = { ...filters };
+
+ const getPath = (level: number) => {
+ if (level === 0) return 'group';
+
+ const str = [];
+ for (const index of groupIndex) {
+ str.push(`group[${index}]`);
+ }
+
+ return `${str.join('.')}.group`;
+ };
+
+ const path = getPath(level);
+ const updatedFilters = _.set(filtersCopy, path, [
+ ..._.get(filtersCopy, path),
+ {
+ group: [],
+ rules: [],
+ type: 'push',
+ uniqueId: nanoid(),
+ },
+ ]);
+
+ setFilters(updatedFilters);
+ };
+
+ const handleDeleteRuleGroup = (args: DeleteArgs) => {
+ const { uniqueId, level, groupIndex } = args;
+ const filtersCopy = { ...filters };
+
+ const getPath = (level: number) => {
+ if (level === 0) return 'group';
+
+ const str = [];
+ for (let i = 0; i < groupIndex.length; i += 1) {
+ if (i !== groupIndex.length - 1) {
+ str.push(`group[${groupIndex[i]}]`);
+ } else {
+ str.push(`group`);
+ }
+ }
+
+ return `${str.join('.')}`;
+ };
+
+ const path = getPath(level);
+
+ const updatedFilters = _.set(filtersCopy, path, [
+ ..._.get(filtersCopy, path).filter(
+ (group: AdvancedFilterGroup) => group.uniqueId !== uniqueId
+ ),
+ ]);
+
+ setFilters(updatedFilters);
+ };
+
+ const handleAddRule = (args: AddArgs) => {
+ const { level, groupIndex } = args;
+ const filtersCopy = { ...filters };
+
+ const getPath = (level: number) => {
+ if (level === 0) return 'rules';
+
+ const str = [];
+ for (const index of groupIndex) {
+ str.push(`group[${index}]`);
+ }
+
+ return `${str.join('.')}.rules`;
+ };
+
+ const path = getPath(level);
+ const updatedFilters = _.set(filtersCopy, path, [
+ ..._.get(filtersCopy, path),
+ { filter: 'newrule', uniqueId: nanoid() },
+ ]);
+
+ setFilters(updatedFilters);
+ };
+
+ const handleDeleteRule = (args: DeleteArgs) => {
+ const { uniqueId, level, groupIndex } = args;
+ const filtersCopy = { ...filters };
+
+ const getPath = (level: number) => {
+ if (level === 0) return 'rules';
+
+ const str = [];
+ for (const index of groupIndex) {
+ str.push(`group[${index}]`);
+ }
+
+ return `${str.join('.')}.rules`;
+ };
+
+ const path = getPath(level);
+ const updatedFilters = _.set(
+ filtersCopy,
+ path,
+ _.get(filtersCopy, path).filter(
+ (rule: AdvancedFilterRule) => rule.uniqueId !== uniqueId
+ )
+ );
+
+ setFilters(updatedFilters);
+ };
+
+ return (
+
+
+
+ );
+};