updated
This commit is contained in:
parent
f5deb65f75
commit
3052836f3b
@ -751,7 +751,7 @@ export default function App() {
|
||||
} finally {
|
||||
donePrefetchInFlightRef.current = false
|
||||
}
|
||||
}, [doneSort, includeKeep])
|
||||
}, [doneSort, includeKeep, donePageSize])
|
||||
|
||||
const loadDoneCount = useCallback(async () => {
|
||||
const now = Date.now()
|
||||
@ -794,6 +794,11 @@ export default function App() {
|
||||
|
||||
donePrefetchRef.current = null
|
||||
|
||||
const totalPages = getDoneTotalPages(doneCount, donePageSize)
|
||||
if (pageToLoad < totalPages) {
|
||||
void prefetchDonePage(pageToLoad + 1)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@ -822,10 +827,15 @@ export default function App() {
|
||||
if (count !== doneCount) {
|
||||
setDoneCount(count)
|
||||
}
|
||||
|
||||
const totalPages = getDoneTotalPages(count, donePageSize)
|
||||
if (pageToLoad < totalPages) {
|
||||
void prefetchDonePage(pageToLoad + 1)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}, [donePage, doneSort, prefetchDonePage, includeKeep, doneCount])
|
||||
}, [donePage, doneSort, donePageSize, prefetchDonePage, includeKeep, doneCount])
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
@ -2767,6 +2777,47 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
const fillDonePageFromPrefetch = useCallback((opts?: { removedFile?: string }) => {
|
||||
const removedFile = String(opts?.removedFile ?? '').trim()
|
||||
|
||||
setDoneJobs((prev) => {
|
||||
const filtered = removedFile
|
||||
? prev.filter((j) => baseName(j.output || '') !== removedFile)
|
||||
: [...prev]
|
||||
|
||||
const need = Math.max(0, donePageSize - filtered.length)
|
||||
if (need <= 0) return filtered
|
||||
|
||||
const prefetchKey = makePrefetchKey(donePage + 1, doneSort)
|
||||
const buf = donePrefetchRef.current
|
||||
|
||||
if (!buf || buf.key !== prefetchKey || !Array.isArray(buf.items) || buf.items.length === 0) {
|
||||
return filtered
|
||||
}
|
||||
|
||||
const next: RecordJob[] = [...filtered]
|
||||
const used = new Set(
|
||||
next.map((x) => String(x.id || baseName(x.output || '')).trim())
|
||||
)
|
||||
|
||||
while (next.length < donePageSize && buf.items.length > 0) {
|
||||
const cand = buf.items.shift()!
|
||||
const id = String(cand.id || baseName(cand.output || '')).trim()
|
||||
if (!id || used.has(id)) continue
|
||||
used.add(id)
|
||||
next.push(cand)
|
||||
}
|
||||
|
||||
donePrefetchRef.current = {
|
||||
...buf,
|
||||
items: buf.items,
|
||||
ts: buf.ts,
|
||||
}
|
||||
|
||||
return next
|
||||
})
|
||||
}, [donePage, donePageSize, doneSort])
|
||||
|
||||
const handleDeleteJobWithUndo = useCallback(
|
||||
async (job: RecordJob): Promise<void | { undoToken?: string }> => {
|
||||
const file = baseName(job.output || '')
|
||||
@ -2783,45 +2834,14 @@ export default function App() {
|
||||
),
|
||||
onSuccess: async () => {
|
||||
window.setTimeout(() => {
|
||||
// ✅ Done-Liste lokal bereinigen + Seite direkt wieder auffüllen (aus Prefetch)
|
||||
setDoneJobs((prev) => {
|
||||
const filtered = prev.filter((j) => baseName(j.output || '') !== file)
|
||||
fillDonePageFromPrefetch({ removedFile: file })
|
||||
|
||||
const need = donePageSize - filtered.length
|
||||
if (need <= 0) return filtered
|
||||
|
||||
const prefetchKey = makePrefetchKey(donePage + 1, doneSort)
|
||||
const buf = donePrefetchRef.current
|
||||
|
||||
if (!buf || buf.key !== prefetchKey || !Array.isArray(buf.items) || buf.items.length === 0) {
|
||||
return filtered
|
||||
}
|
||||
|
||||
const next: RecordJob[] = [...filtered]
|
||||
const used = new Set(next.map((x) => String(x.id || baseName(x.output || '')).trim()))
|
||||
|
||||
while (next.length < donePageSize && buf.items.length > 0) {
|
||||
const cand = buf.items.shift()!
|
||||
const id = String(cand.id || baseName(cand.output || '')).trim()
|
||||
if (!id || used.has(id)) continue
|
||||
used.add(id)
|
||||
next.push(cand)
|
||||
}
|
||||
|
||||
donePrefetchRef.current = { ...buf, items: buf.items, ts: buf.ts }
|
||||
return next
|
||||
})
|
||||
|
||||
// ✅ Count sofort optimistisch runter
|
||||
setDoneCount((c) => Math.max(0, c - 1))
|
||||
|
||||
// ✅ Running-/Player-State bereinigen (falls offen)
|
||||
setJobs((prev) => prev.filter((j) => baseName(j.output || '') !== file))
|
||||
setPlayerJob((prev) => (prev && baseName(prev.output || '') === file ? null : prev))
|
||||
|
||||
// ✅ Prefetch wieder nachfüllen – aber nur wenn es noch eine nächste Seite gibt
|
||||
const nextDoneCount = Math.max(0, doneCount - 1)
|
||||
const totalPagesAfterDelete = getDoneTotalPages(nextDoneCount)
|
||||
const totalPagesAfterDelete = getDoneTotalPages(nextDoneCount, donePageSize)
|
||||
|
||||
if (donePage < totalPagesAfterDelete) {
|
||||
void prefetchDonePage(donePage + 1)
|
||||
@ -2841,7 +2861,15 @@ export default function App() {
|
||||
return
|
||||
}
|
||||
},
|
||||
[runFinishedFileAction, notify, donePage, doneSort, prefetchDonePage, doneCount]
|
||||
[
|
||||
runFinishedFileAction,
|
||||
notify,
|
||||
fillDonePageFromPrefetch,
|
||||
donePage,
|
||||
prefetchDonePage,
|
||||
doneCount,
|
||||
loadDoneCount,
|
||||
]
|
||||
)
|
||||
|
||||
const handleKeepJob = useCallback(
|
||||
@ -2856,45 +2884,14 @@ export default function App() {
|
||||
run: () => apiJSON(`/api/record/keep?file=${encodeURIComponent(file)}`, { method: 'POST' }),
|
||||
onSuccess: async () => {
|
||||
window.setTimeout(() => {
|
||||
// ✅ Done-Liste lokal bereinigen + Seite direkt wieder auffüllen (aus Prefetch)
|
||||
setDoneJobs((prev) => {
|
||||
const filtered = prev.filter((j) => baseName(j.output || '') !== file)
|
||||
fillDonePageFromPrefetch({ removedFile: file })
|
||||
|
||||
const need = donePageSize - filtered.length
|
||||
if (need <= 0) return filtered
|
||||
|
||||
const prefetchKey = makePrefetchKey(donePage + 1, doneSort)
|
||||
const buf = donePrefetchRef.current
|
||||
|
||||
if (!buf || buf.key !== prefetchKey || !Array.isArray(buf.items) || buf.items.length === 0) {
|
||||
return filtered
|
||||
}
|
||||
|
||||
const next: RecordJob[] = [...filtered]
|
||||
const used = new Set(next.map((x) => String(x.id || baseName(x.output || '')).trim()))
|
||||
|
||||
while (next.length < donePageSize && buf.items.length > 0) {
|
||||
const cand = buf.items.shift()!
|
||||
const id = String(cand.id || baseName(cand.output || '')).trim()
|
||||
if (!id || used.has(id)) continue
|
||||
used.add(id)
|
||||
next.push(cand)
|
||||
}
|
||||
|
||||
donePrefetchRef.current = { ...buf, items: buf.items, ts: buf.ts }
|
||||
return next
|
||||
})
|
||||
|
||||
// ✅ Entfernt aus Running + Player
|
||||
setJobs((prev) => prev.filter((j) => baseName(j.output || '') !== file))
|
||||
setPlayerJob((prev) => (prev && baseName(prev.output || '') === file ? null : prev))
|
||||
|
||||
// ✅ Count runter
|
||||
setDoneCount((c) => Math.max(0, c - 1))
|
||||
|
||||
// ✅ Prefetch wieder nachfüllen – aber nur wenn es noch eine nächste Seite gibt
|
||||
const nextDoneCount = Math.max(0, doneCount - 1)
|
||||
const totalPagesAfterKeep = getDoneTotalPages(nextDoneCount)
|
||||
const totalPagesAfterKeep = getDoneTotalPages(nextDoneCount, donePageSize)
|
||||
|
||||
if (donePage < totalPagesAfterKeep) {
|
||||
void prefetchDonePage(donePage + 1)
|
||||
@ -2914,8 +2911,8 @@ export default function App() {
|
||||
[
|
||||
runFinishedFileAction,
|
||||
notify,
|
||||
fillDonePageFromPrefetch,
|
||||
donePage,
|
||||
doneSort,
|
||||
prefetchDonePage,
|
||||
loadDoneCount,
|
||||
doneCount,
|
||||
@ -3310,18 +3307,21 @@ export default function App() {
|
||||
}, ms)
|
||||
}
|
||||
|
||||
const kick = () => void checkClipboard()
|
||||
window.addEventListener('hover', kick)
|
||||
document.addEventListener('visibilitychange', kick)
|
||||
const kick = () => {
|
||||
void checkClipboard()
|
||||
}
|
||||
|
||||
schedule(0)
|
||||
window.addEventListener('focus', kick)
|
||||
document.addEventListener('visibilitychange', kick)
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (timer) window.clearTimeout(timer)
|
||||
window.removeEventListener('hover', kick)
|
||||
document.removeEventListener('visibilitychange', kick)
|
||||
}
|
||||
schedule(0)
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
if (timer) window.clearTimeout(timer)
|
||||
window.removeEventListener('focus', kick)
|
||||
document.removeEventListener('visibilitychange', kick)
|
||||
}
|
||||
}, [autoAddEnabled, autoStartEnabled, enqueueStart])
|
||||
|
||||
if (!authChecked) {
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useState, useCallback, useEffect, useRef } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import Table, { type Column } from './Table'
|
||||
import Card from './Card'
|
||||
import Button from './Button'
|
||||
@ -563,6 +564,46 @@ function DiskEmergencyBadge() {
|
||||
)
|
||||
}
|
||||
|
||||
function LazyMountWhenVisible({
|
||||
children,
|
||||
placeholder,
|
||||
rootMargin = '300px',
|
||||
}: {
|
||||
children: ReactNode
|
||||
placeholder: ReactNode
|
||||
rootMargin?: string
|
||||
}) {
|
||||
const hostRef = useRef<HTMLDivElement | null>(null)
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) return
|
||||
|
||||
const el = hostRef.current
|
||||
if (!el) return
|
||||
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0]
|
||||
if (entry?.isIntersecting) {
|
||||
setVisible(true)
|
||||
io.disconnect()
|
||||
}
|
||||
},
|
||||
{ rootMargin, threshold: 0.01 }
|
||||
)
|
||||
|
||||
io.observe(el)
|
||||
return () => io.disconnect()
|
||||
}, [visible, rootMargin])
|
||||
|
||||
return (
|
||||
<div ref={hostRef} className="w-full h-full">
|
||||
{visible ? children : placeholder}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function Downloads({
|
||||
jobs,
|
||||
pending = [],
|
||||
@ -952,24 +993,30 @@ export default function Downloads({
|
||||
const j = r.job
|
||||
return (
|
||||
<div className="grid w-[96px] h-[60px] overflow-hidden rounded-md">
|
||||
<ModelPreview
|
||||
jobId={j.id}
|
||||
blur={blurPreviews}
|
||||
roomStatus={effectiveRoomStatusOfJob(
|
||||
j,
|
||||
roomStatusByModelKey,
|
||||
modelsByKey,
|
||||
growingByJobId
|
||||
)}
|
||||
alignStartAt={j.startedAt}
|
||||
alignEndAt={previewAlignEndAtOfJob(j)}
|
||||
alignEveryMs={10_000}
|
||||
fastRetryMs={1000}
|
||||
fastRetryMax={25}
|
||||
fastRetryWindowMs={60_000}
|
||||
thumbsCandidates={jobThumbsJPGCandidates(j)}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
<LazyMountWhenVisible
|
||||
placeholder={
|
||||
<div className="h-full w-full bg-gray-100 dark:bg-white/10" />
|
||||
}
|
||||
>
|
||||
<ModelPreview
|
||||
jobId={j.id}
|
||||
blur={blurPreviews}
|
||||
roomStatus={effectiveRoomStatusOfJob(
|
||||
j,
|
||||
roomStatusByModelKey,
|
||||
modelsByKey,
|
||||
growingByJobId
|
||||
)}
|
||||
alignStartAt={j.startedAt}
|
||||
alignEndAt={previewAlignEndAtOfJob(j)}
|
||||
alignEveryMs={10_000}
|
||||
fastRetryMs={1000}
|
||||
fastRetryMax={25}
|
||||
fastRetryWindowMs={60_000}
|
||||
thumbsCandidates={jobThumbsJPGCandidates(j)}
|
||||
className="w-full h-full"
|
||||
/>
|
||||
</LazyMountWhenVisible>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1289,7 +1289,6 @@ export default function FinishedDownloads({
|
||||
const GALLERY_CARD_SCALE_KEY = 'finishedDownloads_galleryCardScale_v1'
|
||||
const LAST_UNDO_KEY = 'finishedDownloads_lastUndo_v1'
|
||||
|
||||
const allMode = loadMode === 'all'
|
||||
const requestedTeaserPlaybackMode: TeaserPlaybackMode = teaserPlayback ?? 'hover'
|
||||
const teaserPlaybackMode: TeaserPlaybackMode =
|
||||
pauseTeasers ? 'still' : requestedTeaserPlaybackMode
|
||||
@ -1327,7 +1326,6 @@ export default function FinishedDownloads({
|
||||
const [removingKeys, setRemovingKeys] = React.useState<Set<string>>(() => new Set())
|
||||
const [hiddenFiles, setHiddenFiles] = React.useState<Set<string>>(() => new Set())
|
||||
|
||||
const [isLoading, setIsLoading] = React.useState(false)
|
||||
const [undoing, setUndoing] = React.useState(false)
|
||||
const [bulkBusy, setBulkBusy] = React.useState(false)
|
||||
|
||||
@ -1339,11 +1337,6 @@ export default function FinishedDownloads({
|
||||
|
||||
const [renamedFiles, setRenamedFiles] = React.useState<Record<string, string>>({})
|
||||
|
||||
const [overrideDoneJobs, setOverrideDoneJobs] = React.useState<RecordJob[] | null>(null)
|
||||
const [overrideDoneJobsKey, setOverrideDoneJobsKey] = React.useState<string | null>(null)
|
||||
const [overrideDoneTotal, setOverrideDoneTotal] = React.useState<number | null>(null)
|
||||
const [refillTick, setRefillTick] = React.useState(0)
|
||||
|
||||
const [view, setView] = React.useState<ViewMode>('table')
|
||||
const [mobileOptionsOpen, setMobileOptionsOpen] = React.useState(false)
|
||||
const [selectionActionsOpen, setSelectionActionsOpen] = React.useState(false)
|
||||
@ -1396,12 +1389,6 @@ export default function FinishedDownloads({
|
||||
const teaserKeyRafRef = React.useRef<number | null>(null)
|
||||
const pendingTeaserKeyRef = React.useRef<string | null>(null)
|
||||
|
||||
const refillInFlightRef = React.useRef(false)
|
||||
const refillQueuedWhileInFlightRef = React.useRef(false)
|
||||
const refillSessionRef = React.useRef(0)
|
||||
const refillTimerRef = React.useRef<number | null>(null)
|
||||
const refillRetryRef = React.useRef(0)
|
||||
|
||||
const countHintRef = React.useRef({ pending: 0, t: 0, timer: 0 as any })
|
||||
|
||||
const scaleLiveRafRef = React.useRef<number | null>(null)
|
||||
@ -1418,8 +1405,6 @@ export default function FinishedDownloads({
|
||||
|
||||
const removeTimersRef = React.useRef<Map<string, number>>(new Map())
|
||||
|
||||
const galleryHydratedKeyRef = React.useRef<string | null>(null)
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// memoized derived data
|
||||
// -----------------------------------------------------------------------------
|
||||
@ -1467,39 +1452,17 @@ export default function FinishedDownloads({
|
||||
activeTagSet.size > 0 || searchTokens.some((t) => t.length >= 2)
|
||||
|
||||
const globalFilterActive = searchActiveForGlobalFetch
|
||||
const effectiveAllMode = globalFilterActive || allMode
|
||||
const effectiveAllMode = globalFilterActive || loadMode === 'all'
|
||||
|
||||
const galleryPageSize = Math.max(2, galleryColumnCount * 2)
|
||||
|
||||
const serverPageSize = pageSize
|
||||
const effectivePageSize =
|
||||
view === 'gallery'
|
||||
? galleryPageSize
|
||||
: pageSize
|
||||
|
||||
const galleryHydrationKey =
|
||||
view === 'gallery' && loadMode === 'paged' && !effectiveAllMode
|
||||
? `${page}__${sortMode}__${includeKeep ? 1 : 0}__${effectivePageSize}`
|
||||
: null
|
||||
|
||||
const hasMatchingGalleryOverride =
|
||||
view === 'gallery' &&
|
||||
galleryHydrationKey != null &&
|
||||
overrideDoneJobsKey === galleryHydrationKey
|
||||
|
||||
const doneJobsPage =
|
||||
effectiveAllMode
|
||||
? (overrideDoneJobs ?? doneJobs)
|
||||
: hasMatchingGalleryOverride && overrideDoneJobs
|
||||
? overrideDoneJobs
|
||||
: doneJobs
|
||||
|
||||
const doneTotalPage =
|
||||
effectiveAllMode
|
||||
? (overrideDoneTotal ?? doneTotal)
|
||||
: hasMatchingGalleryOverride && overrideDoneTotal != null
|
||||
? overrideDoneTotal
|
||||
: doneTotal
|
||||
const doneJobsPage = doneJobs
|
||||
const doneTotalPage = doneTotal
|
||||
|
||||
const applyRenamedOutputLocal = useCallback(
|
||||
(job: RecordJob): RecordJob => {
|
||||
@ -1701,9 +1664,7 @@ export default function FinishedDownloads({
|
||||
|
||||
const emptyFolder = !effectiveAllMode && totalItemsForPagination === 0
|
||||
const emptyByFilter = globalFilterActive && visibleRows.length === 0
|
||||
const emptyAll = allMode && visibleRows.length === 0
|
||||
const isEmptyState = emptyFolder || emptyAll || emptyByFilter
|
||||
const showLoadingCard = isLoading && !isEmptyState
|
||||
const isEmptyState = emptyFolder || emptyByFilter
|
||||
|
||||
const selectedCount = selectedFiles.length
|
||||
const allPageSelected =
|
||||
@ -1837,42 +1798,6 @@ export default function FinishedDownloads({
|
||||
|
||||
const clearTagFilter = useCallback(() => setTagFilter([]), [])
|
||||
|
||||
const fetchAllDoneJobs = useCallback(
|
||||
async (signal?: AbortSignal) => {
|
||||
setIsLoading(true)
|
||||
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/record/done?all=1&sort=${encodeURIComponent(sortMode)}&withCount=1${includeKeep ? '&includeKeep=1' : ''}`,
|
||||
{
|
||||
cache: 'no-store' as any,
|
||||
signal,
|
||||
}
|
||||
)
|
||||
if (!res.ok) return
|
||||
|
||||
const data = await res.json().catch(() => null)
|
||||
const items = Array.isArray(data?.items) ? (data.items as RecordJob[]) : []
|
||||
const count = Number(data?.count ?? data?.totalCount ?? items.length)
|
||||
|
||||
setOverrideDoneJobs(items)
|
||||
setOverrideDoneTotal(Number.isFinite(count) ? count : items.length)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
},
|
||||
[sortMode, includeKeep]
|
||||
)
|
||||
|
||||
const queueRefill = useCallback(() => {
|
||||
if (refillTimerRef.current != null) return
|
||||
|
||||
refillTimerRef.current = window.setTimeout(() => {
|
||||
refillTimerRef.current = null
|
||||
setRefillTick((n) => n + 1)
|
||||
}, 80)
|
||||
}, [])
|
||||
|
||||
const flushResolutionsSoon = useCallback(() => {
|
||||
if (resolutionsFlushTimerRef.current != null) return
|
||||
resolutionsFlushTimerRef.current = window.setTimeout(() => {
|
||||
@ -2273,28 +2198,16 @@ export default function FinishedDownloads({
|
||||
return (await r.json().catch(() => null)) as any
|
||||
},
|
||||
onSuccess: async (result: any) => {
|
||||
if (onDeleteJob) {
|
||||
const undoToken = typeof result?.undoToken === 'string' ? result.undoToken : ''
|
||||
if (undoToken) {
|
||||
setLastAction({ kind: 'delete', undoToken, originalFile: file, rowKey: key })
|
||||
} else {
|
||||
setLastAction(null)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const from = (result?.from === 'keep' ? 'keep' : 'done') as 'done' | 'keep'
|
||||
const undoToken = typeof result?.undoToken === 'string' ? result.undoToken : ''
|
||||
|
||||
if (undoToken) {
|
||||
setLastAction({ kind: 'delete', undoToken, originalFile: file, rowKey: key, from })
|
||||
setLastAction({ kind: 'delete', undoToken, originalFile: file, rowKey: key })
|
||||
} else {
|
||||
setLastAction(null)
|
||||
}
|
||||
|
||||
queueRefill()
|
||||
emitCountHint(-1)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return res.ok
|
||||
@ -2305,7 +2218,6 @@ export default function FinishedDownloads({
|
||||
onDeleteJob,
|
||||
withFileReleaseRetry,
|
||||
runFileMutation,
|
||||
queueRefill,
|
||||
emitCountHint,
|
||||
]
|
||||
)
|
||||
@ -2355,9 +2267,8 @@ export default function FinishedDownloads({
|
||||
|
||||
setLastAction({ kind: 'keep', keptFile, originalFile: file, rowKey: key })
|
||||
|
||||
queueRefill()
|
||||
emitCountHint(includeKeep ? 0 : -1)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
return res.ok
|
||||
@ -2368,7 +2279,6 @@ export default function FinishedDownloads({
|
||||
deletingKeys,
|
||||
withFileReleaseRetry,
|
||||
runFileMutation,
|
||||
queueRefill,
|
||||
emitCountHint,
|
||||
includeKeep,
|
||||
]
|
||||
@ -2468,7 +2378,6 @@ export default function FinishedDownloads({
|
||||
|
||||
const visibleDelta = lastAction.from === 'keep' && !includeKeep ? 0 : +1
|
||||
emitCountHint(visibleDelta)
|
||||
queueRefill()
|
||||
setLastAction(null)
|
||||
return
|
||||
}
|
||||
@ -2490,7 +2399,6 @@ export default function FinishedDownloads({
|
||||
unhideRow(restoredFile, restoredFile)
|
||||
|
||||
emitCountHint(+1)
|
||||
queueRefill()
|
||||
setLastAction(null)
|
||||
return
|
||||
}
|
||||
@ -2513,7 +2421,6 @@ export default function FinishedDownloads({
|
||||
applyRename(oldFile, newFile)
|
||||
}
|
||||
|
||||
queueRefill()
|
||||
setLastAction(null)
|
||||
return
|
||||
}
|
||||
@ -2526,7 +2433,6 @@ export default function FinishedDownloads({
|
||||
lastAction,
|
||||
undoing,
|
||||
notify,
|
||||
queueRefill,
|
||||
includeKeep,
|
||||
emitCountHint,
|
||||
applyRename,
|
||||
@ -2594,10 +2500,6 @@ export default function FinishedDownloads({
|
||||
}
|
||||
|
||||
setLastAction({ kind: 'hot', currentFile: apiNew })
|
||||
|
||||
if (!onToggleHot || sortMode === 'file_asc' || sortMode === 'file_desc') {
|
||||
queueRefill()
|
||||
}
|
||||
},
|
||||
onError: async () => {
|
||||
clearRenamePair(oldFile, optimisticNew)
|
||||
@ -2613,8 +2515,6 @@ export default function FinishedDownloads({
|
||||
clearRenamePair,
|
||||
onToggleHot,
|
||||
withFileReleaseRetry,
|
||||
queueRefill,
|
||||
sortMode,
|
||||
]
|
||||
)
|
||||
|
||||
@ -2807,7 +2707,6 @@ export default function FinishedDownloads({
|
||||
const deletedCount = Number(data?.deleted ?? results.filter((x: any) => x?.ok).length ?? 0)
|
||||
|
||||
clearSelection()
|
||||
queueRefill()
|
||||
emitCountHint(-deletedCount)
|
||||
|
||||
if (deletedCount > 0) {
|
||||
@ -2824,7 +2723,6 @@ export default function FinishedDownloads({
|
||||
selectedItems,
|
||||
animateRemove,
|
||||
clearSelection,
|
||||
queueRefill,
|
||||
emitCountHint,
|
||||
notify,
|
||||
])
|
||||
@ -2869,7 +2767,6 @@ export default function FinishedDownloads({
|
||||
const keptCount = Number(data?.kept ?? results.filter((x: any) => x?.ok).length ?? 0)
|
||||
|
||||
clearSelection()
|
||||
queueRefill()
|
||||
emitCountHint(includeKeep ? 0 : -keptCount)
|
||||
|
||||
if (keptCount > 0) {
|
||||
@ -2886,7 +2783,6 @@ export default function FinishedDownloads({
|
||||
selectedItems,
|
||||
animateRemove,
|
||||
clearSelection,
|
||||
queueRefill,
|
||||
emitCountHint,
|
||||
includeKeep,
|
||||
notify,
|
||||
@ -2905,21 +2801,6 @@ export default function FinishedDownloads({
|
||||
}
|
||||
}, [view, galleryColumnCount, pageSize, onPageSizeChange])
|
||||
|
||||
useEffect(() => {
|
||||
if (effectiveAllMode) return
|
||||
setOverrideDoneJobs(null)
|
||||
setOverrideDoneTotal(null)
|
||||
setOverrideDoneJobsKey(null)
|
||||
}, [effectiveAllMode])
|
||||
|
||||
useEffect(() => {
|
||||
if (view === 'gallery') return
|
||||
|
||||
setOverrideDoneJobs(null)
|
||||
setOverrideDoneTotal(null)
|
||||
setOverrideDoneJobsKey(null)
|
||||
}, [view])
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const raw = localStorage.getItem(GALLERY_CARD_SCALE_KEY)
|
||||
@ -3250,73 +3131,6 @@ export default function FinishedDownloads({
|
||||
}, [lastAction])
|
||||
|
||||
useEffect(() => {
|
||||
if (!effectiveAllMode) return
|
||||
|
||||
const ac = new AbortController()
|
||||
|
||||
const t = window.setTimeout(() => {
|
||||
fetchAllDoneJobs(ac.signal).catch(() => {})
|
||||
}, 250)
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(t)
|
||||
ac.abort()
|
||||
}
|
||||
}, [effectiveAllMode, fetchAllDoneJobs])
|
||||
|
||||
useEffect(() => {
|
||||
if (refillTick === 0) return
|
||||
|
||||
const ac = new AbortController()
|
||||
let alive = true
|
||||
let finished = false
|
||||
const mySession = ++refillSessionRef.current
|
||||
|
||||
const finishRefill = () => {
|
||||
if (finished) return
|
||||
finished = true
|
||||
|
||||
if (refillSessionRef.current !== mySession) return
|
||||
|
||||
refillInFlightRef.current = false
|
||||
|
||||
if (refillQueuedWhileInFlightRef.current) {
|
||||
refillQueuedWhileInFlightRef.current = false
|
||||
queueRefill()
|
||||
}
|
||||
}
|
||||
|
||||
refillInFlightRef.current = true
|
||||
|
||||
// ✅ paged-mode hier NICHT mehr laden
|
||||
if (!effectiveAllMode) {
|
||||
finishRefill()
|
||||
return () => {
|
||||
alive = false
|
||||
ac.abort()
|
||||
finishRefill()
|
||||
}
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
await fetchAllDoneJobs(ac.signal)
|
||||
if (alive) refillRetryRef.current = 0
|
||||
} catch {
|
||||
} finally {
|
||||
if (alive) finishRefill()
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
alive = false
|
||||
ac.abort()
|
||||
finishRefill()
|
||||
}
|
||||
}, [refillTick, effectiveAllMode, fetchAllDoneJobs, queueRefill])
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return
|
||||
if (effectiveAllMode) return
|
||||
if (totalItemsForPagination < 0) return
|
||||
|
||||
@ -3326,7 +3140,6 @@ export default function FinishedDownloads({
|
||||
onPageChange(totalPages)
|
||||
}
|
||||
}, [
|
||||
isLoading,
|
||||
effectiveAllMode,
|
||||
totalItemsForPagination,
|
||||
effectivePageSize,
|
||||
@ -3334,103 +3147,6 @@ export default function FinishedDownloads({
|
||||
onPageChange,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
if (loadMode !== 'paged') return
|
||||
if (effectiveAllMode) return
|
||||
if (view !== 'gallery') return
|
||||
if (!galleryHydrationKey) return
|
||||
|
||||
const currentCount = hasMatchingGalleryOverride
|
||||
? (overrideDoneJobs?.length ?? 0)
|
||||
: doneJobs.length
|
||||
|
||||
const wantedCount = effectivePageSize
|
||||
const totalCount = doneTotal
|
||||
|
||||
const pageStart = (page - 1) * wantedCount
|
||||
const maxItemsOnThisPage = Math.max(0, totalCount - pageStart)
|
||||
|
||||
if (maxItemsOnThisPage <= 0) {
|
||||
galleryHydratedKeyRef.current = galleryHydrationKey
|
||||
return
|
||||
}
|
||||
|
||||
const targetCount = Math.min(wantedCount, maxItemsOnThisPage)
|
||||
|
||||
if (currentCount >= targetCount) {
|
||||
galleryHydratedKeyRef.current = galleryHydrationKey
|
||||
return
|
||||
}
|
||||
|
||||
if (galleryHydratedKeyRef.current === galleryHydrationKey) {
|
||||
return
|
||||
}
|
||||
|
||||
const ac = new AbortController()
|
||||
let cancelled = false
|
||||
|
||||
setIsLoading(true)
|
||||
|
||||
;(async () => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/record/done?page=${page}&pageSize=${targetCount}&sort=${encodeURIComponent(sortMode)}${
|
||||
includeKeep ? '&includeKeep=1' : ''
|
||||
}`,
|
||||
{ cache: 'no-store' as any, signal: ac.signal }
|
||||
)
|
||||
|
||||
if (!res.ok || cancelled || ac.signal.aborted) return
|
||||
|
||||
const data = await res.json().catch(() => null)
|
||||
if (cancelled || ac.signal.aborted) return
|
||||
|
||||
const items = Array.isArray(data?.items)
|
||||
? (data.items as RecordJob[])
|
||||
: Array.isArray(data)
|
||||
? data
|
||||
: []
|
||||
|
||||
const countRaw = Number(data?.count ?? data?.totalCount ?? items.length)
|
||||
const count = Number.isFinite(countRaw) && countRaw >= 0 ? countRaw : 0
|
||||
|
||||
setOverrideDoneJobs(items)
|
||||
setOverrideDoneTotal(count)
|
||||
setOverrideDoneJobsKey(galleryHydrationKey)
|
||||
galleryHydratedKeyRef.current = galleryHydrationKey
|
||||
} catch (err: any) {
|
||||
if (err?.name !== 'AbortError') {
|
||||
console.error('gallery hydration failed', err)
|
||||
}
|
||||
} finally {
|
||||
if (!cancelled) {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
})()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
ac.abort()
|
||||
}
|
||||
}, [
|
||||
loadMode,
|
||||
effectiveAllMode,
|
||||
view,
|
||||
page,
|
||||
sortMode,
|
||||
includeKeep,
|
||||
doneJobs,
|
||||
doneTotal,
|
||||
effectivePageSize,
|
||||
serverPageSize,
|
||||
galleryHydrationKey,
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
galleryHydratedKeyRef.current = null
|
||||
}, [page, sortMode, includeKeep, effectivePageSize, view])
|
||||
|
||||
useEffect(() => {
|
||||
const m = new Map<string, string>()
|
||||
for (const j of viewRows) {
|
||||
@ -3516,37 +3232,12 @@ export default function FinishedDownloads({
|
||||
|
||||
if (detail.phase === 'success') {
|
||||
markDeleting(key, false)
|
||||
|
||||
if (refillInFlightRef.current) {
|
||||
refillQueuedWhileInFlightRef.current = true
|
||||
} else {
|
||||
queueRefill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('finished-downloads:delete', onExternalDelete as EventListener)
|
||||
return () => window.removeEventListener('finished-downloads:delete', onExternalDelete as EventListener)
|
||||
}, [animateRemove, markDeleting, queueRefill, restoreRow])
|
||||
|
||||
useEffect(() => {
|
||||
const onReload = () => {
|
||||
if (document.hidden) return
|
||||
|
||||
if (refillInFlightRef.current) {
|
||||
if (!refillQueuedWhileInFlightRef.current) {
|
||||
refillQueuedWhileInFlightRef.current = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (refillTimerRef.current != null) return
|
||||
queueRefill()
|
||||
}
|
||||
|
||||
window.addEventListener('finished-downloads:reload', onReload as EventListener)
|
||||
return () => window.removeEventListener('finished-downloads:reload', onReload as EventListener)
|
||||
}, [queueRefill])
|
||||
}, [animateRemove, markDeleting, restoreRow])
|
||||
|
||||
useEffect(() => {
|
||||
const onExternalRename = (ev: Event) => {
|
||||
@ -3706,8 +3397,7 @@ export default function FinishedDownloads({
|
||||
<LoadingSpinner
|
||||
size="lg"
|
||||
className={[
|
||||
'text-indigo-500 transition-opacity duration-150',
|
||||
isLoading ? 'opacity-100' : 'opacity-0 pointer-events-none',
|
||||
'text-indigo-500 transition-opacity duration-150 opacity-0 pointer-events-none',
|
||||
].join(' ')}
|
||||
srLabel="Lade Downloads…"
|
||||
/>
|
||||
@ -3866,8 +3556,7 @@ export default function FinishedDownloads({
|
||||
<LoadingSpinner
|
||||
size="lg"
|
||||
className={[
|
||||
'text-indigo-500 transition-opacity duration-150',
|
||||
isLoading ? 'opacity-100' : 'opacity-0 pointer-events-none',
|
||||
'text-indigo-500 transition-opacity duration-150 opacity-0 pointer-events-none',
|
||||
].join(' ')}
|
||||
srLabel="Lade Downloads…"
|
||||
/>
|
||||
@ -4253,26 +3942,7 @@ export default function FinishedDownloads({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showLoadingCard ? (
|
||||
<Card grayBody>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
Lade Downloads…
|
||||
</div>
|
||||
<div className="text-xs text-gray-600 dark:text-gray-300">
|
||||
Bitte warte einen Moment.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoadingSpinner
|
||||
size="lg"
|
||||
className="text-indigo-500"
|
||||
srLabel="Lade Downloads…"
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
) : (emptyFolder || emptyAll) ? (
|
||||
{emptyFolder ? (
|
||||
<Card grayBody>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="grid h-10 w-10 place-items-center rounded-xl bg-white/70 ring-1 ring-gray-200 dark:bg-white/5 dark:ring-white/10">
|
||||
@ -4332,7 +4002,7 @@ export default function FinishedDownloads({
|
||||
}}
|
||||
isSmall={isSmall}
|
||||
jobForDetails={jobForDetails}
|
||||
isLoading={isLoading}
|
||||
isLoading={false}
|
||||
blurPreviews={blurPreviews}
|
||||
durations={durations}
|
||||
teaserState={teaserState}
|
||||
@ -4394,7 +4064,7 @@ export default function FinishedDownloads({
|
||||
{view === 'table' && (
|
||||
<FinishedDownloadsTableView
|
||||
rows={pageRows}
|
||||
isLoading={isLoading}
|
||||
isLoading={false}
|
||||
selectedKeys={selectedKeys}
|
||||
onToggleSelected={toggleSelected}
|
||||
onToggleSelectAllPage={(checked) => {
|
||||
@ -4465,7 +4135,7 @@ export default function FinishedDownloads({
|
||||
if (checked) selectPageRows(pageRows)
|
||||
else deselectPageRows(pageRows)
|
||||
}}
|
||||
isLoading={isLoading}
|
||||
isLoading={false}
|
||||
jobForDetails={jobForDetails}
|
||||
blurPreviews={blurPreviews}
|
||||
durations={durations}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user