feat(import-ui): surface quality-filter + folder-artist toggles on Import page
The two import behaviour toggles previously lived only in Settings → Import.
Mirror them onto the React Import page (above the processing queue) so they're
visible and adjustable right where you import.
- New ImportOptions component: two Switches ("Quality check on import",
"Use folder as artist") with optimistic update + immediate save.
- API: fetchImportOptions / saveImportOptions / importOptionsQueryOptions —
read the whole settings blob, POST a partial {import: {...}} (the settings
endpoint partial-merges, so the rest of config is untouched). Both default ON
when absent, matching the backend defaults.
- Same import.quality_filter_enabled / import.folder_artist_override config
keys as the Settings page, so the two stay in sync.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
526ed227c7
commit
0f7e15363b
4 changed files with 141 additions and 3 deletions
|
|
@ -10,6 +10,7 @@ import type {
|
||||||
ImportAutoImportResultsPayload,
|
ImportAutoImportResultsPayload,
|
||||||
ImportAutoImportSettingsPayload,
|
ImportAutoImportSettingsPayload,
|
||||||
ImportAutoImportStatusPayload,
|
ImportAutoImportStatusPayload,
|
||||||
|
ImportOptionsPayload,
|
||||||
ImportProcessPayload,
|
ImportProcessPayload,
|
||||||
ImportStagingFilesPayload,
|
ImportStagingFilesPayload,
|
||||||
ImportStagingGroupsPayload,
|
ImportStagingGroupsPayload,
|
||||||
|
|
@ -25,7 +26,7 @@ export const IMPORT_QUERY_KEY = ['import'] as const;
|
||||||
// which left the progress bar stuck at 0 and showing "Failed" while files
|
// which left the progress bar stuck at 0 and showing "Failed" while files
|
||||||
// imported fine (#772). Give the import-process calls a generous bound so the
|
// imported fine (#772). Give the import-process calls a generous bound so the
|
||||||
// responses actually arrive and the bar advances. Scoped to import only.
|
// responses actually arrive and the bar advances. Scoped to import only.
|
||||||
const IMPORT_REQUEST_TIMEOUT_MS = 300_000; // 5 min/track
|
const IMPORT_REQUEST_TIMEOUT_MS = 300_000; // 5 min/track
|
||||||
|
|
||||||
export async function fetchImportStagingFiles(): Promise<ImportStagingFilesPayload> {
|
export async function fetchImportStagingFiles(): Promise<ImportStagingFilesPayload> {
|
||||||
return readJson<ImportStagingFilesPayload>(apiClient.get('import/staging/files'));
|
return readJson<ImportStagingFilesPayload>(apiClient.get('import/staging/files'));
|
||||||
|
|
@ -225,6 +226,45 @@ export function autoImportResultsQueryOptions() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Import behaviour toggles (mirrors Settings → Import) ---
|
||||||
|
// Both live under the `import.*` config namespace. GET reads the whole settings
|
||||||
|
// blob; POST /api/settings partial-merges, so we only send the import keys.
|
||||||
|
type SettingsBlob = {
|
||||||
|
import?: { quality_filter_enabled?: boolean; folder_artist_override?: boolean };
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function fetchImportOptions(): Promise<ImportOptionsPayload> {
|
||||||
|
const data = await readJson<SettingsBlob>(apiClient.get('settings'));
|
||||||
|
const imp = data.import ?? {};
|
||||||
|
return {
|
||||||
|
// Both default ON when the key is absent (matches the backend defaults).
|
||||||
|
qualityFilterEnabled: imp.quality_filter_enabled !== false,
|
||||||
|
folderArtistOverride: imp.folder_artist_override !== false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveImportOptions(
|
||||||
|
options: ImportOptionsPayload,
|
||||||
|
): Promise<{ success: boolean; error?: string }> {
|
||||||
|
return readJson<{ success: boolean; error?: string }>(
|
||||||
|
apiClient.post('settings', {
|
||||||
|
json: {
|
||||||
|
import: {
|
||||||
|
quality_filter_enabled: options.qualityFilterEnabled,
|
||||||
|
folder_artist_override: options.folderArtistOverride,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function importOptionsQueryOptions() {
|
||||||
|
return queryOptions({
|
||||||
|
queryKey: [...IMPORT_QUERY_KEY, 'import-options'],
|
||||||
|
queryFn: fetchImportOptions,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function invalidateImportQueries(queryClient: QueryClient) {
|
export function invalidateImportQueries(queryClient: QueryClient) {
|
||||||
return queryClient.invalidateQueries({ queryKey: IMPORT_QUERY_KEY });
|
return queryClient.invalidateQueries({ queryKey: IMPORT_QUERY_KEY });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -241,3 +241,10 @@ export interface ImportSinglesQueueJob {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ImportQueueJob = ImportAlbumQueueJob | ImportSinglesQueueJob;
|
export type ImportQueueJob = ImportAlbumQueueJob | ImportSinglesQueueJob;
|
||||||
|
|
||||||
|
// The two import behaviour toggles also shown in Settings → Import, mirrored
|
||||||
|
// onto the Import page for visibility. Both map to `import.*` config keys.
|
||||||
|
export interface ImportOptionsPayload {
|
||||||
|
qualityFilterEnabled: boolean;
|
||||||
|
folderArtistOverride: boolean;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,25 @@
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: rgba(255, 255, 255, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.importOptionsRow {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 22px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.importOption {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.importStagingPath {
|
.importStagingPath {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { Link, Outlet } from '@tanstack/react-router';
|
import { Link, Outlet } from '@tanstack/react-router';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { Button } from '@/components/form/form';
|
import { Button, Switch } from '@/components/form/form';
|
||||||
import { Show } from '@/components/primitives';
|
import { Show } from '@/components/primitives';
|
||||||
import { useReactPageShell } from '@/platform/shell/route-controllers';
|
import { useReactPageShell } from '@/platform/shell/route-controllers';
|
||||||
|
|
||||||
import type { ImportQueueEntry } from '../-import.types';
|
import type { ImportOptionsPayload, ImportQueueEntry } from '../-import.types';
|
||||||
|
|
||||||
|
import { importOptionsQueryOptions, IMPORT_QUERY_KEY, saveImportOptions } from '../-import.api';
|
||||||
import {
|
import {
|
||||||
getQueueProgressPercent,
|
getQueueProgressPercent,
|
||||||
getQueueStatusText,
|
getQueueStatusText,
|
||||||
|
|
@ -36,6 +38,7 @@ export function ImportPage() {
|
||||||
lastRefreshedAt={lastRefreshedAt}
|
lastRefreshedAt={lastRefreshedAt}
|
||||||
onRefresh={refreshStaging}
|
onRefresh={refreshStaging}
|
||||||
/>
|
/>
|
||||||
|
<ImportOptions />
|
||||||
<ImportProcessingQueue />
|
<ImportProcessingQueue />
|
||||||
<ImportTabNav />
|
<ImportTabNav />
|
||||||
<section className={clsx(styles.importPageTabContent, styles.active)}>
|
<section className={clsx(styles.importPageTabContent, styles.active)}>
|
||||||
|
|
@ -54,6 +57,75 @@ function formatShortTime(timestamp: number) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The two import behaviour toggles, also in Settings → Import, surfaced here so
|
||||||
|
// they're visible right where you import. Each writes its `import.*` config key
|
||||||
|
// on change (POST /api/settings partial-merges, so the rest of config is safe).
|
||||||
|
function ImportOptions() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const optionsQuery = useQuery(importOptionsQueryOptions());
|
||||||
|
const saveMutation = useMutation({
|
||||||
|
mutationFn: saveImportOptions,
|
||||||
|
onSuccess: () => {
|
||||||
|
window.showToast?.('Import options saved', 'success');
|
||||||
|
},
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
window.showToast?.(
|
||||||
|
error instanceof Error ? error.message : 'Could not save import options',
|
||||||
|
'error',
|
||||||
|
);
|
||||||
|
// Re-sync the toggles to the server's actual state after a failed save.
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: [...IMPORT_QUERY_KEY, 'import-options'],
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const opts = optionsQuery.data;
|
||||||
|
if (!opts) return null;
|
||||||
|
|
||||||
|
const update = (patch: Partial<ImportOptionsPayload>) => {
|
||||||
|
const next = { ...opts, ...patch };
|
||||||
|
// Optimistic: reflect the toggle immediately, the mutation persists it.
|
||||||
|
queryClient.setQueryData([...IMPORT_QUERY_KEY, 'import-options'], next);
|
||||||
|
saveMutation.mutate(next);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className={styles.importOptionsRow} data-testid="import-options">
|
||||||
|
<div className={styles.importOption}>
|
||||||
|
<Switch
|
||||||
|
id="import-quality-filter"
|
||||||
|
checked={opts.qualityFilterEnabled}
|
||||||
|
disabled={saveMutation.isPending}
|
||||||
|
aria-labelledby="import-quality-filter-label"
|
||||||
|
onCheckedChange={(checked) => update({ qualityFilterEnabled: checked })}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
id="import-quality-filter-label"
|
||||||
|
title="Only import tracks that meet your quality profile; otherwise import everything regardless of quality."
|
||||||
|
>
|
||||||
|
Quality check on import
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.importOption}>
|
||||||
|
<Switch
|
||||||
|
id="import-folder-artist"
|
||||||
|
checked={opts.folderArtistOverride}
|
||||||
|
disabled={saveMutation.isPending}
|
||||||
|
aria-labelledby="import-folder-artist-label"
|
||||||
|
onCheckedChange={(checked) => update({ folderArtistOverride: checked })}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
id="import-folder-artist-label"
|
||||||
|
title="Use the top Staging folder as the album artist (good for mixtapes; turn off for mixed piles of songs)."
|
||||||
|
>
|
||||||
|
Use folder as artist
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function ImportHeader({
|
function ImportHeader({
|
||||||
error,
|
error,
|
||||||
fileCountText,
|
fileCountText,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue