feat(upload): persistent "Recently uploaded" list (§A8)
The transient client-side upload queue vanished on refresh, so a just- uploaded track seemed to disappear. Add a server-backed "Recently uploaded" section (source=upload, newest first) that survives refresh and auto-refreshes after each upload (the upload mutation already invalidates the `Track` tag this query provides). - api: `source` filter on `LibraryFilters` → `GET /tracks?source=` - i18n: `upload.recent.*` (en + ru); loading/error/empty states Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -42,6 +42,7 @@ function trackParams(f: LibraryFilters) {
|
||||
q: f.search,
|
||||
artist_id: f.artistId,
|
||||
album_id: f.albumId,
|
||||
source: f.source,
|
||||
sort_by: f.sortBy ? SORT_BY[f.sortBy] : undefined,
|
||||
order: f.sortOrder,
|
||||
...paging(f.page, f.pageSize),
|
||||
|
||||
@@ -177,6 +177,8 @@ export interface LibraryFilters {
|
||||
genre?: string;
|
||||
artistId?: string;
|
||||
albumId?: string;
|
||||
/** Filter by ingest origin, e.g. `upload`, `youtube`, `local`. */
|
||||
source?: string;
|
||||
liked?: boolean;
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
|
||||
@@ -6,8 +6,23 @@ import {
|
||||
buildUploadFormData,
|
||||
useUploadTrackMutation,
|
||||
} from '../../api/endpoints/upload';
|
||||
import { useGetTrackQuery } from '../../api/endpoints/library';
|
||||
import {
|
||||
useGetTrackQuery,
|
||||
useGetTracksQuery,
|
||||
} from '../../api/endpoints/library';
|
||||
import { MetadataStatusBadge } from '../../components/track/MetadataStatusBadge';
|
||||
import { TrackRow } from '../../components/track/TrackRow';
|
||||
import { LoadingSkeleton } from '../../components/common/LoadingSkeleton';
|
||||
import { ErrorState } from '../../components/common/ErrorState';
|
||||
|
||||
/** A8 "Recently uploaded": server-backed list (source=upload, newest first) so
|
||||
* it survives a page refresh — unlike the transient client-side queue above. */
|
||||
const RECENT_UPLOADS = {
|
||||
source: 'upload',
|
||||
sortBy: 'dateAdded',
|
||||
sortOrder: 'desc',
|
||||
pageSize: 20,
|
||||
} as const;
|
||||
|
||||
/** Pure client-side state — this is a transient upload queue, never server data. */
|
||||
type ItemStatus = 'queued' | 'uploading' | 'done' | 'duplicate' | 'error';
|
||||
@@ -40,6 +55,10 @@ export function UploadPage() {
|
||||
const [items, setItems] = useState<QueueItem[]>([]);
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
// Persisted view of past uploads. Auto-refreshes after each upload because the
|
||||
// upload mutation invalidates the `Track` tag this query provides.
|
||||
const recentQuery = useGetTracksQuery(RECENT_UPLOADS);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const idCounter = useRef(0);
|
||||
const activeCount = useRef(0);
|
||||
@@ -228,6 +247,32 @@ export function UploadPage() {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}
|
||||
>
|
||||
<span style={{ fontWeight: 600, fontSize: '0.875rem' }}>
|
||||
{t('upload.recent.title')}
|
||||
</span>
|
||||
{recentQuery.isLoading && <LoadingSkeleton rows={4} />}
|
||||
{recentQuery.isError && (
|
||||
<ErrorState onRetry={() => recentQuery.refetch()} />
|
||||
)}
|
||||
{recentQuery.data && recentQuery.data.items.length === 0 && (
|
||||
<p
|
||||
style={{
|
||||
margin: 0,
|
||||
fontSize: '0.8125rem',
|
||||
color: 'var(--color-text-3)',
|
||||
}}
|
||||
>
|
||||
{t('upload.recent.empty')}
|
||||
</p>
|
||||
)}
|
||||
{recentQuery.data?.items.map((track, i) => (
|
||||
<TrackRow key={track.id} track={track} index={i} showAlbum />
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
|
||||
@@ -310,6 +310,10 @@ const en = {
|
||||
clearCompleted: 'Clear completed',
|
||||
retry: 'Retry',
|
||||
editMetadata: 'Edit metadata',
|
||||
recent: {
|
||||
title: 'Recently uploaded',
|
||||
empty: 'Nothing uploaded yet.',
|
||||
},
|
||||
metadataPending:
|
||||
'Uploaded tracks land as “Unknown Artist” with metadata pending — enrich them afterwards.',
|
||||
unknownArtist: 'Unknown Artist · metadata pending',
|
||||
|
||||
@@ -312,6 +312,10 @@ const ru: Translations = {
|
||||
clearCompleted: 'Убрать завершённые',
|
||||
retry: 'Повторить',
|
||||
editMetadata: 'Изменить метаданные',
|
||||
recent: {
|
||||
title: 'Недавно загруженные',
|
||||
empty: 'Пока ничего не загружено.',
|
||||
},
|
||||
metadataPending:
|
||||
'Загруженные треки появляются как «Unknown Artist» с метаданными в ожидании — дозаполните их позже.',
|
||||
unknownArtist: 'Unknown Artist · метаданные в ожидании',
|
||||
|
||||
Reference in New Issue
Block a user