This commit is contained in:
Linrador 2026-06-04 20:34:52 +02:00
parent 1631ff4f18
commit eeda6976ec
5 changed files with 153 additions and 41 deletions

View File

@ -1,4 +1,4 @@
// backend/record.go // backend\record.go
package main package main
import ( import (

View File

@ -13,7 +13,7 @@
"autoDeleteSmallDownloadsBelowMB": 300, "autoDeleteSmallDownloadsBelowMB": 300,
"autoDeleteSmallDownloadsKeepFavorites": false, "autoDeleteSmallDownloadsKeepFavorites": false,
"lowDiskPauseBelowGB": 5, "lowDiskPauseBelowGB": 5,
"blurPreviews": false, "blurPreviews": true,
"teaserPlayback": "all", "teaserPlayback": "all",
"teaserAudio": true, "teaserAudio": true,
"enableNotifications": true, "enableNotifications": true,

View File

@ -957,6 +957,60 @@ export default function App() {
} }
}, [doneSort, includeKeep, donePageSize]) }, [doneSort, includeKeep, donePageSize])
function doneJobMergeKey(job: RecordJob): string {
const output = String(job?.output ?? '').trim()
const id = String((job as any)?.id ?? '').trim()
if (output) return output
if (id) return id
return JSON.stringify([
String((job as any)?.sourceUrl ?? ''),
String((job as any)?.startedAt ?? ''),
String((job as any)?.endedAt ?? ''),
])
}
function mergeDoneJobsStable(
prev: RecordJob[],
incoming: RecordJob[]
): RecordJob[] {
if (!Array.isArray(prev) || prev.length === 0) return incoming
if (!Array.isArray(incoming) || incoming.length === 0) return prev
const incomingByKey = new Map<string, RecordJob>()
for (const item of incoming) {
incomingByKey.set(doneJobMergeKey(item), item)
}
const seen = new Set<string>()
const merged: RecordJob[] = []
// 1) Bestehende sichtbare Reihenfolge beibehalten,
// aber Daten der vorhandenen Elemente aktualisieren.
for (const oldItem of prev) {
const key = doneJobMergeKey(oldItem)
const nextItem = incomingByKey.get(key)
if (nextItem) {
merged.push(nextItem)
seen.add(key)
} else {
merged.push(oldItem)
}
}
// 2) Wirklich neue Finished-Downloads hinten anhängen,
// ohne bestehende Karten umzuschichten.
for (const item of incoming) {
const key = doneJobMergeKey(item)
if (seen.has(key)) continue
merged.push(item)
}
return merged
}
const loadDoneCount = useCallback(async () => { const loadDoneCount = useCallback(async () => {
const now = Date.now() const now = Date.now()
@ -986,16 +1040,22 @@ export default function App() {
} }
}, [includeKeep]) }, [includeKeep])
const loadDonePage = useCallback(async (pageToLoad = donePage, sortToLoad = doneSort) => { const loadDonePage = useCallback(async (
pageToLoad = donePage,
sortToLoad = doneSort,
opts?: { preserveOrder?: boolean }
) => {
try { try {
const preserveOrder = Boolean(opts?.preserveOrder)
const key = makePrefetchKey(pageToLoad, sortToLoad) const key = makePrefetchKey(pageToLoad, sortToLoad)
const prefetched = donePrefetchRef.current const prefetched = donePrefetchRef.current
if (prefetched?.key === key && Array.isArray(prefetched.items)) { if (prefetched?.key === key && Array.isArray(prefetched.items)) {
setDoneJobs(prefetched.items) setDoneJobs((prev) =>
preserveOrder ? mergeDoneJobsStable(prev, prefetched.items) : prefetched.items
)
donePrefetchRef.current = null donePrefetchRef.current = null
return return
} }
@ -1013,7 +1073,9 @@ export default function App() {
? (data.items as RecordJob[]) ? (data.items as RecordJob[])
: [] : []
setDoneJobs(items) setDoneJobs((prev) =>
preserveOrder ? mergeDoneJobsStable(prev, items) : items
)
const countRaw = Number(data?.count ?? doneCount) const countRaw = Number(data?.count ?? doneCount)
const count = Number.isFinite(countRaw) && countRaw >= 0 ? countRaw : 0 const count = Number.isFinite(countRaw) && countRaw >= 0 ? countRaw : 0
@ -2726,8 +2788,13 @@ export default function App() {
} }
const onDoneChanged = () => { const onDoneChanged = () => {
if (selectedTabRef.current !== 'finished') return void loadDoneCount()
void loadDonePage(donePageRef.current, doneSortRef.current)
if (selectedTabRef.current === 'finished') {
void loadDonePage(donePageRef.current, doneSortRef.current, {
preserveOrder: true,
})
}
} }
const onAutostart = (ev: MessageEvent) => { const onAutostart = (ev: MessageEvent) => {
@ -2850,10 +2917,6 @@ export default function App() {
if (state === 'done' || state === 'error') { if (state === 'done' || state === 'error') {
bumpAssets() bumpAssets()
if (selectedTabRef.current === 'finished') {
void loadDonePage(donePageRef.current, doneSortRef.current)
}
} }
} catch { } catch {
// ignore // ignore

View File

@ -2286,23 +2286,11 @@ export default function FinishedDownloads({
DEFAULT_GALLERY_PAGE_SIZE DEFAULT_GALLERY_PAGE_SIZE
) )
const handleGalleryRecommendedPageSizeChange = useCallback((rawSize: number) => { const handleGalleryRecommendedPageSizeChange = useCallback((size: number) => {
const rounded = Math.round(Number(rawSize)) const next = Math.max(2, Math.min(24, Math.round(size)))
if (!Number.isFinite(rounded) || rounded <= 0) return if (next === galleryPageSize) return
setGalleryPageSize(next)
const next = Math.max(1, Math.min(24, rounded)) }, [galleryPageSize])
setGalleryPageSize((current) => {
if (current === next) return current
return next
})
onPageSizeChange?.(next)
if (page !== 1) {
onPageChange(1)
}
}, [onPageSizeChange, onPageChange, page])
const effectivePageSize = const effectivePageSize =
view === 'gallery' view === 'gallery'

View File

@ -177,6 +177,8 @@ function FinishedDownloadsGalleryView({
const rootRef = useRef<HTMLDivElement | null>(null) const rootRef = useRef<HTMLDivElement | null>(null)
const lastRecommendedPageSizeRef = useRef<number | null>(null) const lastRecommendedPageSizeRef = useRef<number | null>(null)
const shrinkTimerRef = useRef<number | null>(null)
const rafRef = useRef<number | null>(null)
const recommendPageSize = useCallback(() => { const recommendPageSize = useCallback(() => {
if (!onRecommendedPageSizeChange) return if (!onRecommendedPageSizeChange) return
@ -184,23 +186,72 @@ function FinishedDownloadsGalleryView({
const el = rootRef.current const el = rootRef.current
if (!el) return if (!el) return
const measureAndApply = () => {
const containerWidth = el.clientWidth const containerWidth = el.clientWidth
if (!Number.isFinite(containerWidth) || containerWidth <= 0) return if (!Number.isFinite(containerWidth) || containerWidth <= 0) return
const gapPx = 12 // entspricht Tailwind gap-3 const gapPx = 12
const columns = Math.max( const columns = Math.max(
1, 1,
Math.floor((containerWidth + gapPx) / (minCardWidth + gapPx)) Math.floor((containerWidth + gapPx) / (minCardWidth + gapPx))
) )
const rowsPerPage = 2 const rowsPerPage = 2
const nextPageSize = clamp(columns * rowsPerPage, rowsPerPage, 24) const nextPageSize = clamp(columns * rowsPerPage, rowsPerPage, 24)
const currentPageSize = lastRecommendedPageSizeRef.current
if (currentPageSize === nextPageSize) return
// Größer werden: sofort übernehmen
if (currentPageSize == null || nextPageSize > currentPageSize) {
if (shrinkTimerRef.current !== null) {
window.clearTimeout(shrinkTimerRef.current)
shrinkTimerRef.current = null
}
if (lastRecommendedPageSizeRef.current === nextPageSize) return
lastRecommendedPageSizeRef.current = nextPageSize lastRecommendedPageSizeRef.current = nextPageSize
onRecommendedPageSizeChange(nextPageSize) onRecommendedPageSizeChange(nextPageSize)
return
}
// Kleiner werden: erst kurz stabilisieren lassen,
// damit kurze Reflows / Scrollbars / Sidebar-Änderungen
// nicht sofort 12 -> 8 zurückdrücken.
if (shrinkTimerRef.current !== null) {
window.clearTimeout(shrinkTimerRef.current)
}
shrinkTimerRef.current = window.setTimeout(() => {
shrinkTimerRef.current = null
const stableWidth = el.clientWidth
if (!Number.isFinite(stableWidth) || stableWidth <= 0) return
const stableColumns = Math.max(
1,
Math.floor((stableWidth + gapPx) / (minCardWidth + gapPx))
)
const stablePageSize = clamp(stableColumns * rowsPerPage, rowsPerPage, 24)
if (stablePageSize >= (lastRecommendedPageSizeRef.current ?? 0)) {
return
}
lastRecommendedPageSizeRef.current = stablePageSize
onRecommendedPageSizeChange(stablePageSize)
}, 220)
}
if (rafRef.current !== null) {
window.cancelAnimationFrame(rafRef.current)
}
rafRef.current = window.requestAnimationFrame(() => {
rafRef.current = null
measureAndApply()
})
}, [minCardWidth, onRecommendedPageSizeChange]) }, [minCardWidth, onRecommendedPageSizeChange])
useEffect(() => { useEffect(() => {
@ -219,6 +270,16 @@ function FinishedDownloadsGalleryView({
return () => { return () => {
ro.disconnect() ro.disconnect()
window.removeEventListener('resize', recommendPageSize) window.removeEventListener('resize', recommendPageSize)
if (shrinkTimerRef.current !== null) {
window.clearTimeout(shrinkTimerRef.current)
shrinkTimerRef.current = null
}
if (rafRef.current !== null) {
window.cancelAnimationFrame(rafRef.current)
rafRef.current = null
}
} }
}, [recommendPageSize]) }, [recommendPageSize])