61dbb1abd2
Implement the A8 upload screen against the existing /upload contract:
- UploadResponse type ({track_id, title, already_exists}) + mutation typed to it
- buildUploadFormData helper (single file under field `file`, per FastAPI)
- UploadPage: drag-and-drop + file picker, client-side queue with
concurrency cap (3), per-file status badges, retry on error,
already_exists -> "Already in library", deep-link to A7 metadata editor
- i18n upload.* section (en/ru) incl. "metadata pending" hint
Indeterminate spinner per file; percent progress is a follow-up
(needs an XHR baseQuery — fetchBaseQuery gives no upload progress).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
30 lines
810 B
TypeScript
30 lines
810 B
TypeScript
import { api } from '../index';
|
|
import type { UploadResponse } from '../types';
|
|
|
|
/**
|
|
* Build the multipart body for `/upload`. The backend expects exactly one file
|
|
* per request under the field name `file` (anything else → FastAPI 422).
|
|
*/
|
|
export function buildUploadFormData(file: File): FormData {
|
|
const fd = new FormData();
|
|
fd.append('file', file);
|
|
return fd;
|
|
}
|
|
|
|
export const uploadApi = api.injectEndpoints({
|
|
endpoints: (build) => ({
|
|
uploadTrack: build.mutation<UploadResponse, FormData>({
|
|
query: (formData) => ({
|
|
url: '/upload',
|
|
method: 'POST',
|
|
body: formData,
|
|
formData: true,
|
|
}),
|
|
invalidatesTags: ['Track', 'Album', 'Artist', 'Storage'],
|
|
}),
|
|
}),
|
|
overrideExisting: false,
|
|
});
|
|
|
|
export const { useUploadTrackMutation } = uploadApi;
|