reimplement smart playlists

This commit is contained in:
jeffvli
2025-11-28 21:27:27 -08:00
parent 9d3c44ef15
commit 06d0c715af
8 changed files with 381 additions and 193 deletions
@@ -43,18 +43,29 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
},
});
const [isSmartPlaylist, setIsSmartPlaylist] = useState(false);
const [step, setStep] = useState<1 | 2>(1);
const handleSubmit = form.onSubmit((values) => {
if (isSmartPlaylist) {
values._custom!.navidrome = {
...values._custom?.navidrome,
rules: queryBuilderRef.current?.getFilters(),
};
if (!server) return;
// If creating a smart playlist and we're on the first step, advance to step 2
// to configure the query instead of submitting immediately.
if (isSmartPlaylist && step === 1) {
setStep(2);
return;
}
const smartPlaylist = queryBuilderRef.current?.getFilters();
if (!server) return;
const rules =
isSmartPlaylist && smartPlaylist?.filters
? {
...convertQueryGroupToNDQuery(smartPlaylist.filters),
limit: smartPlaylist.extraFilters.limit,
order: smartPlaylist.extraFilters.sortOrder,
sort: smartPlaylist.extraFilters.sortBy,
}
: undefined;
mutation.mutate(
{
@@ -64,16 +75,10 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
_custom: {
navidrome: {
...values._custom?.navidrome,
rules:
isSmartPlaylist && smartPlaylist?.filters
? {
...convertQueryGroupToNDQuery(smartPlaylist.filters),
limit: smartPlaylist.extraFilters.limit,
order: smartPlaylist.extraFilters.sortOrder,
sort: smartPlaylist.extraFilters.sortBy,
}
: undefined,
rules,
},
// Top-level rules field is what Navidrome expects for smart playlists.
...(rules ? { rules } : {}),
},
},
},
@@ -100,51 +105,63 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
return (
<form onSubmit={handleSubmit}>
<Stack>
<TextInput
data-autofocus
label={t('form.createPlaylist.input', {
context: 'name',
postProcess: 'titleCase',
})}
required
{...form.getInputProps('name')}
/>
{server?.type === ServerType.NAVIDROME && (
<Textarea
autosize
label={t('form.createPlaylist.input', {
context: 'description',
postProcess: 'titleCase',
})}
minRows={5}
{...form.getInputProps('comment')}
/>
)}
<Group>
{isPublicDisplayed && (
<Switch
{step === 1 && (
<>
<TextInput
data-autofocus
label={t('form.createPlaylist.input', {
context: 'public',
context: 'name',
postProcess: 'titleCase',
})}
{...form.getInputProps('public', {
type: 'checkbox',
})}
required
{...form.getInputProps('name')}
/>
)}
{server?.type === ServerType.NAVIDROME &&
hasFeature(server, ServerFeature.PLAYLISTS_SMART) && (
<Switch
label="Is smart playlist?"
onChange={(e) => setIsSmartPlaylist(e.currentTarget.checked)}
{server?.type === ServerType.NAVIDROME && (
<Textarea
autosize
label={t('form.createPlaylist.input', {
context: 'description',
postProcess: 'titleCase',
})}
minRows={5}
{...form.getInputProps('comment')}
/>
)}
</Group>
{server?.type === ServerType.NAVIDROME && isSmartPlaylist && (
<Group>
{isPublicDisplayed && (
<Switch
label={t('form.createPlaylist.input', {
context: 'public',
postProcess: 'titleCase',
})}
{...form.getInputProps('public', {
type: 'checkbox',
})}
/>
)}
{server?.type === ServerType.NAVIDROME &&
hasFeature(server, ServerFeature.PLAYLISTS_SMART) && (
<Switch
checked={isSmartPlaylist}
label="Is smart playlist?"
onChange={(e) => {
const next = e.currentTarget.checked;
setIsSmartPlaylist(next);
if (!next) {
setStep(1);
}
}}
/>
)}
</Group>
</>
)}
{isSmartPlaylist && step === 2 && (
<Stack pt="1rem">
<Text>Query Editor</Text>
<PlaylistQueryBuilder
isSaving={false}
isSaving={mutation.isPending}
limit={undefined}
query={undefined}
ref={queryBuilderRef}
@@ -155,6 +172,11 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
)}
<Group justify="flex-end">
{isSmartPlaylist && step === 2 && (
<ModalButton onClick={() => setStep(1)} px="2xl" uppercase variant="subtle">
Back
</ModalButton>
)}
<ModalButton onClick={onCancel} px="2xl" uppercase variant="subtle">
{t('common.cancel')}
</ModalButton>
@@ -164,7 +186,9 @@ export const CreatePlaylistForm = ({ onCancel }: CreatePlaylistFormProps) => {
type="submit"
variant="filled"
>
{t('common.create')}
{isSmartPlaylist && step === 1
? t('common.confirm', { postProcess: 'sentenceCase' })
: t('common.create')}
</ModalButton>
</Group>
</Stack>