support custom smart playlist tags

This commit is contained in:
jeffvli
2025-11-29 15:56:18 -08:00
parent d22fee887c
commit 6094a520e2
7 changed files with 278 additions and 15 deletions
@@ -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',
})}
/>
);
};