diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index ad60bd42c..7cceecede 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -84,6 +84,7 @@ "owner": "owner", "path": "path", "playerMustBePaused": "player must be paused", + "preview": "preview", "previousSong": "previous $t(entity.track_one)", "quit": "quit", "random": "random", diff --git a/src/renderer/api/navidrome.types.ts b/src/renderer/api/navidrome.types.ts index b9c8c688c..951c0d98b 100644 --- a/src/renderer/api/navidrome.types.ts +++ b/src/renderer/api/navidrome.types.ts @@ -400,6 +400,7 @@ export const NDSongQueryFields = [ { label: 'File Type', type: 'string', value: 'filetype' }, { label: 'Genre', type: 'string', value: 'genre' }, { label: 'Has CoverArt', type: 'boolean', value: 'hascoverart' }, + { label: 'Playlist', type: 'playlist', value: 'id' }, { label: 'Is Compilation', type: 'boolean', value: 'compilation' }, { label: 'Is Favorite', type: 'boolean', value: 'loved' }, { label: 'Lyrics', type: 'string', value: 'lyrics' }, @@ -415,6 +416,11 @@ export const NDSongQueryFields = [ { label: 'Year', type: 'number', value: 'year' }, ]; +export const NDSongQueryPlaylistOperators = [ + { label: 'is in', value: 'inPlaylist' }, + { label: 'is not in', value: 'notInPlaylist' }, +]; + export const NDSongQueryDateOperators = [ { label: 'is', value: 'is' }, { label: 'is not', value: 'isNot' }, diff --git a/src/renderer/components/query-builder/index.tsx b/src/renderer/components/query-builder/index.tsx index bce098e2a..f6dab1d92 100644 --- a/src/renderer/components/query-builder/index.tsx +++ b/src/renderer/components/query-builder/index.tsx @@ -54,8 +54,10 @@ interface QueryBuilderProps { boolean: { label: string; value: string }[]; date: { label: string; value: string }[]; number: { label: string; value: string }[]; + playlist: { label: string; value: string }[]; string: { label: string; value: string }[]; }; + playlists?: { label: string; value: string }[]; uniqueId: string; } @@ -73,6 +75,7 @@ export const QueryBuilder = ({ onChangeValue, onClearFilters, onResetFilters, + playlists, groupIndex, uniqueId, filters, @@ -180,6 +183,7 @@ export const QueryBuilder = ({ level={level} noRemove={data?.rules?.length === 1} operators={operators} + selectData={playlists} onChangeField={onChangeField} onChangeOperator={onChangeOperator} onChangeValue={onChangeValue} @@ -204,6 +208,7 @@ export const QueryBuilder = ({ groupIndex={[...(groupIndex || []), index]} level={level + 1} operators={operators} + playlists={playlists} uniqueId={group.uniqueId} onAddRule={onAddRule} onAddRuleGroup={onAddRuleGroup} diff --git a/src/renderer/components/query-builder/query-builder-option.tsx b/src/renderer/components/query-builder/query-builder-option.tsx index 1a34f4317..c558766e0 100644 --- a/src/renderer/components/query-builder/query-builder-option.tsx +++ b/src/renderer/components/query-builder/query-builder-option.tsx @@ -28,9 +28,10 @@ interface QueryOptionProps { number: { label: string; value: string }[]; string: { label: string; value: string }[]; }; + selectData?: { label: string; value: string }[]; } -const QueryValueInput = ({ onChange, type, ...props }: any) => { +const QueryValueInput = ({ onChange, type, data, ...props }: any) => { const [numberRange, setNumberRange] = useState([0, 0]); switch (type) { @@ -59,7 +60,6 @@ const QueryValueInput = ({ onChange, type, ...props }: any) => { {...props} /> ); - case 'dateRange': return ( <> @@ -87,7 +87,6 @@ const QueryValueInput = ({ onChange, type, ...props }: any) => { /> ); - case 'boolean': return ( + ); default: return <>; @@ -116,6 +123,7 @@ export const QueryBuilderOption = ({ onChangeField, onChangeOperator, onChangeValue, + selectData, }: QueryOptionProps) => { const { field, operator, uniqueId, value } = data; @@ -133,10 +141,7 @@ export const QueryBuilderOption = ({ const handleChangeValue = (e: any) => { const isDirectValue = - typeof e === 'string' || - typeof e === 'number' || - typeof e === 'undefined' || - typeof e === null; + typeof e === 'string' || typeof e === 'number' || typeof e === 'undefined'; if (isDirectValue) { return onChangeValue({ @@ -207,6 +212,7 @@ export const QueryBuilderOption = ({ /> {field ? ( void; + playlistId?: string; query: any; sortBy: SongListSort; sortOrder: 'asc' | 'desc'; @@ -84,14 +90,43 @@ export type PlaylistQueryBuilderRef = { export const PlaylistQueryBuilder = forwardRef( ( - { sortOrder, sortBy, limit, isSaving, query, onSave, onSaveAs }: PlaylistQueryBuilderProps, + { + sortOrder, + sortBy, + limit, + isSaving, + query, + onSave, + onSaveAs, + playlistId, + }: PlaylistQueryBuilderProps, ref: Ref, ) => { const { t } = useTranslation(); + const server = useCurrentServer(); const [filters, setFilters] = useState( query ? convertNDQueryToQueryGroup(query) : DEFAULT_QUERY, ); + const { data: playlists } = usePlaylistList({ + query: { sortBy: PlaylistListSort.NAME, sortOrder: SortOrder.ASC, startIndex: 0 }, + serverId: server?.id, + }); + + const playlistData = useMemo(() => { + if (!playlists) return []; + + return playlists.items + .filter((p) => { + if (!playlistId) return true; + return p.id !== playlistId; + }) + .map((p) => ({ + label: p.name, + value: p.id, + })); + }, [playlistId, playlists]); + const extraFiltersForm = useForm({ initialValues: { limit, @@ -131,6 +166,16 @@ export const PlaylistQueryBuilder = forwardRef( onSaveAs?.(convertQueryGroupToNDQuery(filters), extraFiltersForm.values); }; + const openPreviewModal = () => { + const previewValue = convertQueryGroupToNDQuery(filters); + + openModal({ + children: , + size: 'xl', + title: t('common.preview', { postProcess: 'titleCase' }), + }); + }; + const handleAddRuleGroup = (args: AddArgs) => { const { level, groupIndex } = args; const filtersCopy = clone(filters); @@ -367,7 +412,7 @@ export const PlaylistQueryBuilder = forwardRef( return ( {t('common.saveAs', { postProcess: 'titleCase' })} +