mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-14 04:20:07 +02:00
support custom smart playlist tags
This commit is contained in:
@@ -18,6 +18,7 @@ import { QueryBuilder } from '/@/renderer/components/query-builder';
|
||||
import { playlistsQueries } from '/@/renderer/features/playlists/api/playlists-api';
|
||||
import { convertNDQueryToQueryGroup } from '/@/renderer/features/playlists/utils';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { useQueryBuilderSettings } from '/@/renderer/store/settings.store';
|
||||
import {
|
||||
NDSongQueryBooleanOperators,
|
||||
NDSongQueryDateOperators,
|
||||
@@ -168,6 +169,7 @@ export const PlaylistQueryBuilder = forwardRef(
|
||||
) => {
|
||||
const { t } = useTranslation();
|
||||
const server = useCurrentServer();
|
||||
const queryBuilderSettings = useQueryBuilderSettings();
|
||||
|
||||
// Memoize initial filters to avoid recalculation
|
||||
const initialFilters = useMemo(
|
||||
@@ -412,6 +414,54 @@ export const PlaylistQueryBuilder = forwardRef(
|
||||
});
|
||||
}, []);
|
||||
|
||||
const customFields = useMemo(() => {
|
||||
return queryBuilderSettings.tag
|
||||
.filter((field) => field.value && field.value.trim() !== '')
|
||||
.map((field) => ({
|
||||
label: field.label,
|
||||
type: field.type,
|
||||
value: field.value,
|
||||
}));
|
||||
}, [queryBuilderSettings.tag]);
|
||||
|
||||
const groupedFilters = useMemo(() => {
|
||||
type FilterGroup = {
|
||||
group: string;
|
||||
items: Array<{ label: string; type: string; value: string }>;
|
||||
};
|
||||
const groups: FilterGroup[] = [];
|
||||
|
||||
// Custom Fields group
|
||||
if (customFields.length > 0) {
|
||||
groups.push({
|
||||
group: t('queryBuilder.customTags', {
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
items: customFields,
|
||||
});
|
||||
}
|
||||
|
||||
// Standard Fields group
|
||||
if (NDSongQueryFields.length > 0) {
|
||||
groups.push({
|
||||
group: t('queryBuilder.standardTags', {
|
||||
postProcess: 'titleCase',
|
||||
}),
|
||||
items: NDSongQueryFields,
|
||||
});
|
||||
}
|
||||
|
||||
if (groups.length === 0) {
|
||||
return NDSongQueryFields;
|
||||
}
|
||||
|
||||
if (groups.length === 1) {
|
||||
return groups[0].items;
|
||||
}
|
||||
|
||||
return groups;
|
||||
}, [customFields, t]);
|
||||
|
||||
// Memoize sort options
|
||||
const sortOptions = useMemo(
|
||||
() => [
|
||||
@@ -483,7 +533,7 @@ export const PlaylistQueryBuilder = forwardRef(
|
||||
<Stack gap="md" h="100%" p="1rem">
|
||||
<QueryBuilder
|
||||
data={filters}
|
||||
filters={NDSongQueryFields}
|
||||
filters={groupedFilters}
|
||||
groupIndex={[]}
|
||||
level={0}
|
||||
onAddRule={handleAddRule}
|
||||
|
||||
@@ -1,24 +1,40 @@
|
||||
import { useMemo } from 'react';
|
||||
import { Fragment } from 'react/jsx-runtime';
|
||||
|
||||
import { ApplicationSettings } from '/@/renderer/features/settings/components/general/application-settings';
|
||||
import { ControlSettings } from '/@/renderer/features/settings/components/general/control-settings';
|
||||
import { LyricSettings } from '/@/renderer/features/settings/components/general/lyric-settings';
|
||||
import { QueryBuilderSettings } from '/@/renderer/features/settings/components/general/query-builder-settings';
|
||||
import { ScrobbleSettings } from '/@/renderer/features/settings/components/general/scrobble-settings';
|
||||
import { SidebarSettings } from '/@/renderer/features/settings/components/general/sidebar-settings';
|
||||
import { ThemeSettings } from '/@/renderer/features/settings/components/general/theme-settings';
|
||||
import { useCurrentServer } from '/@/renderer/store';
|
||||
import { hasFeature } from '/@/shared/api/utils';
|
||||
import { Divider } from '/@/shared/components/divider/divider';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
|
||||
const sections = [
|
||||
{ component: ApplicationSettings, key: 'application' },
|
||||
{ component: ThemeSettings, key: 'theme' },
|
||||
{ component: ControlSettings, key: 'control' },
|
||||
{ component: SidebarSettings, key: 'sidebar' },
|
||||
{ component: ScrobbleSettings, key: 'scrobble' },
|
||||
{ component: LyricSettings, key: 'lyrics' },
|
||||
];
|
||||
import { ServerFeature } from '/@/shared/types/features-types';
|
||||
|
||||
export const GeneralTab = () => {
|
||||
const server = useCurrentServer();
|
||||
const supportsSmartPlaylists = hasFeature(server, ServerFeature.PLAYLISTS_SMART);
|
||||
|
||||
const sections = useMemo(() => {
|
||||
const baseSections = [
|
||||
{ component: ApplicationSettings, key: 'application' },
|
||||
{ component: ThemeSettings, key: 'theme' },
|
||||
{ component: ControlSettings, key: 'control' },
|
||||
{ component: SidebarSettings, key: 'sidebar' },
|
||||
{ component: ScrobbleSettings, key: 'scrobble' },
|
||||
{ component: LyricSettings, key: 'lyrics' },
|
||||
];
|
||||
|
||||
if (supportsSmartPlaylists) {
|
||||
baseSections.push({ component: QueryBuilderSettings, key: 'queryBuilder' });
|
||||
}
|
||||
|
||||
return baseSections;
|
||||
}, [supportsSmartPlaylists]);
|
||||
|
||||
return (
|
||||
<Stack gap="md">
|
||||
{sections.map(({ component: Section, key }, index) => (
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import {
|
||||
SettingOption,
|
||||
SettingsSection,
|
||||
} from '/@/renderer/features/settings/components/settings-section';
|
||||
import { useQueryBuilderSettings, useSettingsStoreActions } from '/@/renderer/store/settings.store';
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Select } from '/@/shared/components/select/select';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
import { TextInput } from '/@/shared/components/text-input/text-input';
|
||||
|
||||
const QUERY_VALUE_INPUT_TYPES = [
|
||||
{ label: 'Boolean', value: 'boolean' },
|
||||
{ label: 'Date', value: 'date' },
|
||||
{ label: 'Date Range', value: 'dateRange' },
|
||||
{ label: 'Number', value: 'number' },
|
||||
{ label: 'Playlist', value: 'playlist' },
|
||||
{ label: 'String', value: 'string' },
|
||||
] as const;
|
||||
|
||||
export const QueryBuilderSettings = () => {
|
||||
const { t } = useTranslation();
|
||||
const queryBuilder = useQueryBuilderSettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
|
||||
const handleAddCustomField = () => {
|
||||
setSettings({
|
||||
queryBuilder: {
|
||||
tag: [
|
||||
...queryBuilder.tag,
|
||||
{
|
||||
label: '',
|
||||
type: 'string',
|
||||
value: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveCustomField = (index: number) => {
|
||||
setSettings({
|
||||
queryBuilder: {
|
||||
tag: queryBuilder.tag.filter((_, i) => i !== index),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateCustomField = (
|
||||
index: number,
|
||||
field: 'label' | 'type' | 'value',
|
||||
newValue: string,
|
||||
) => {
|
||||
setSettings({
|
||||
queryBuilder: {
|
||||
tag: queryBuilder.tag.map((item, i) =>
|
||||
i === index ? { ...item, [field]: newValue } : item,
|
||||
),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const customFieldsOptions: SettingOption[] = [
|
||||
{
|
||||
control: (
|
||||
<Stack gap="md">
|
||||
{queryBuilder.tag.length > 0 && (
|
||||
<Stack gap="sm">
|
||||
{queryBuilder.tag.map((field, index) => (
|
||||
<Group gap="sm" key={index}>
|
||||
<TextInput
|
||||
onChange={(e) =>
|
||||
handleUpdateCustomField(
|
||||
index,
|
||||
'label',
|
||||
e.currentTarget.value,
|
||||
)
|
||||
}
|
||||
placeholder={t(
|
||||
'setting.queryBuilderCustomFields_inputLabel',
|
||||
{
|
||||
postProcess: 'sentenceCase',
|
||||
},
|
||||
)}
|
||||
value={field.label}
|
||||
width="30%"
|
||||
/>
|
||||
<Select
|
||||
data={QUERY_VALUE_INPUT_TYPES}
|
||||
onChange={(e) =>
|
||||
handleUpdateCustomField(index, 'type', e || 'string')
|
||||
}
|
||||
value={field.type}
|
||||
width="25%"
|
||||
/>
|
||||
<TextInput
|
||||
onChange={(e) =>
|
||||
handleUpdateCustomField(
|
||||
index,
|
||||
'value',
|
||||
e.currentTarget.value,
|
||||
)
|
||||
}
|
||||
placeholder={t(
|
||||
'setting.queryBuilderCustomFields_inputTag',
|
||||
{
|
||||
postProcess: 'sentenceCase',
|
||||
},
|
||||
)}
|
||||
value={field.value}
|
||||
width="30%"
|
||||
/>
|
||||
<ActionIcon
|
||||
icon="remove"
|
||||
onClick={() => handleRemoveCustomField(index)}
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
/>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
<Group grow>
|
||||
<Button onClick={handleAddCustomField} variant="filled">
|
||||
{t('common.add', { postProcess: 'titleCase' })}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
),
|
||||
description: t('setting.queryBuilderCustomFields', {
|
||||
context: 'description',
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
title: t('setting.queryBuilderCustomFields', {
|
||||
postProcess: 'sentenceCase',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<SettingsSection
|
||||
options={customFieldsOptions}
|
||||
title={t('page.setting.queryBuilder', {
|
||||
postProcess: 'sentenceCase',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user