Refactor media upload handling in MediaPanel: remove direct upload logic and integrate useUploadStore for file enqueueing. Update translations for upload status and add UploadSnackbar component to display upload notifications in the UI.
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
import { create } from 'zustand'
|
||||
import { queryClient } from '@/shared/api/query-client'
|
||||
import i18n from '@/shared/lib/i18n'
|
||||
import { toast } from '@/shared/ui/toast-store'
|
||||
import { listMedia, uploadMedia } from './api'
|
||||
|
||||
export type UploadItem = {
|
||||
id: string
|
||||
name: string
|
||||
percent: number
|
||||
status: 'queued' | 'uploading' | 'done' | 'error'
|
||||
}
|
||||
|
||||
type UploadStore = {
|
||||
items: UploadItem[]
|
||||
active: boolean
|
||||
minimized: boolean
|
||||
enqueue: (files: File[]) => Promise<void>
|
||||
toggleMinimize: () => void
|
||||
dismiss: () => void
|
||||
}
|
||||
|
||||
// Очередь и флаг живут вне React — загрузка продолжается при любой навигации.
|
||||
let counter = 0
|
||||
const queue: { id: string; file: File }[] = []
|
||||
let running = false
|
||||
|
||||
const patch = (id: string, changes: Partial<UploadItem>) =>
|
||||
useUploadStore.setState((s) => ({
|
||||
items: s.items.map((i) => (i.id === id ? { ...i, ...changes } : i)),
|
||||
}))
|
||||
|
||||
async function pump() {
|
||||
if (running) return
|
||||
running = true
|
||||
useUploadStore.setState({ active: true, minimized: false })
|
||||
|
||||
let uploaded = 0
|
||||
while (queue.length > 0) {
|
||||
const job = queue.shift()!
|
||||
patch(job.id, { status: 'uploading', percent: 0 })
|
||||
try {
|
||||
await uploadMedia(job.file, (percent) => patch(job.id, { percent }))
|
||||
patch(job.id, { status: 'done', percent: 100 })
|
||||
uploaded++
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin', 'media'] })
|
||||
} catch {
|
||||
patch(job.id, { status: 'error' })
|
||||
}
|
||||
}
|
||||
|
||||
running = false
|
||||
useUploadStore.setState({ active: false })
|
||||
if (uploaded > 0) toast.success(i18n.t('admin.media.uploadedCount', { count: uploaded }))
|
||||
}
|
||||
|
||||
export const useUploadStore = create<UploadStore>((set) => ({
|
||||
items: [],
|
||||
active: false,
|
||||
minimized: false,
|
||||
|
||||
enqueue: async (files) => {
|
||||
// Пропускаем дубликаты: уже в библиотеке (не проваленные) и уже в текущей очереди.
|
||||
let existing = new Set<string>()
|
||||
try {
|
||||
const all = await listMedia({
|
||||
page: 1,
|
||||
pageSize: 1000,
|
||||
statuses: ['Pending', 'Processing', 'Ready'],
|
||||
})
|
||||
existing = new Set(all.items.map((a) => a.originalFileName))
|
||||
} catch {
|
||||
// положимся на серверную проверку
|
||||
}
|
||||
const inFlight = new Set(
|
||||
useUploadStore
|
||||
.getState()
|
||||
.items.filter((i) => i.status !== 'error')
|
||||
.map((i) => i.name),
|
||||
)
|
||||
|
||||
const toAdd = files.filter((f) => !existing.has(f.name) && !inFlight.has(f.name))
|
||||
const skipped = files.length - toAdd.length
|
||||
|
||||
if (toAdd.length > 0) {
|
||||
const newItems: UploadItem[] = toAdd.map((file) => {
|
||||
const id = `u${++counter}`
|
||||
queue.push({ id, file })
|
||||
return { id, name: file.name, percent: 0, status: 'queued' }
|
||||
})
|
||||
set((s) => ({ items: [...s.items, ...newItems], minimized: false }))
|
||||
void pump()
|
||||
}
|
||||
if (skipped > 0) toast.message(i18n.t('admin.media.skippedDuplicates', { count: skipped }))
|
||||
},
|
||||
|
||||
toggleMinimize: () => set((s) => ({ minimized: !s.minimized })),
|
||||
dismiss: () => set({ items: [] }),
|
||||
}))
|
||||
Reference in New Issue
Block a user