diff --git a/backend/record.go b/backend/record.go index 6aa3647..894f516 100644 --- a/backend/record.go +++ b/backend/record.go @@ -1,4 +1,4 @@ -// backend/record.go +// backend\record.go package main import ( diff --git a/backend/recorder_settings.json b/backend/recorder_settings.json index a6b46aa..36b4f31 100644 --- a/backend/recorder_settings.json +++ b/backend/recorder_settings.json @@ -13,7 +13,7 @@ "autoDeleteSmallDownloadsBelowMB": 300, "autoDeleteSmallDownloadsKeepFavorites": false, "lowDiskPauseBelowGB": 5, - "blurPreviews": false, + "blurPreviews": true, "teaserPlayback": "all", "teaserAudio": true, "enableNotifications": true, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 843a9c2..71a4842 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -957,6 +957,60 @@ export default function App() { } }, [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() + for (const item of incoming) { + incomingByKey.set(doneJobMergeKey(item), item) + } + + const seen = new Set() + 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 now = Date.now() @@ -986,16 +1040,22 @@ export default function App() { } }, [includeKeep]) - const loadDonePage = useCallback(async (pageToLoad = donePage, sortToLoad = doneSort) => { + const loadDonePage = useCallback(async ( + pageToLoad = donePage, + sortToLoad = doneSort, + opts?: { preserveOrder?: boolean } + ) => { try { + const preserveOrder = Boolean(opts?.preserveOrder) const key = makePrefetchKey(pageToLoad, sortToLoad) const prefetched = donePrefetchRef.current if (prefetched?.key === key && Array.isArray(prefetched.items)) { - setDoneJobs(prefetched.items) + setDoneJobs((prev) => + preserveOrder ? mergeDoneJobsStable(prev, prefetched.items) : prefetched.items + ) donePrefetchRef.current = null - return } @@ -1013,7 +1073,9 @@ export default function App() { ? (data.items as RecordJob[]) : [] - setDoneJobs(items) + setDoneJobs((prev) => + preserveOrder ? mergeDoneJobsStable(prev, items) : items + ) const countRaw = Number(data?.count ?? doneCount) const count = Number.isFinite(countRaw) && countRaw >= 0 ? countRaw : 0 @@ -2726,8 +2788,13 @@ export default function App() { } const onDoneChanged = () => { - if (selectedTabRef.current !== 'finished') return - void loadDonePage(donePageRef.current, doneSortRef.current) + void loadDoneCount() + + if (selectedTabRef.current === 'finished') { + void loadDonePage(donePageRef.current, doneSortRef.current, { + preserveOrder: true, + }) + } } const onAutostart = (ev: MessageEvent) => { @@ -2850,10 +2917,6 @@ export default function App() { if (state === 'done' || state === 'error') { bumpAssets() - - if (selectedTabRef.current === 'finished') { - void loadDonePage(donePageRef.current, doneSortRef.current) - } } } catch { // ignore diff --git a/frontend/src/components/ui/FinishedDownloads.tsx b/frontend/src/components/ui/FinishedDownloads.tsx index a885d55..c089cb6 100644 --- a/frontend/src/components/ui/FinishedDownloads.tsx +++ b/frontend/src/components/ui/FinishedDownloads.tsx @@ -2286,23 +2286,11 @@ export default function FinishedDownloads({ DEFAULT_GALLERY_PAGE_SIZE ) - const handleGalleryRecommendedPageSizeChange = useCallback((rawSize: number) => { - const rounded = Math.round(Number(rawSize)) - if (!Number.isFinite(rounded) || rounded <= 0) return - - const next = Math.max(1, Math.min(24, rounded)) - - setGalleryPageSize((current) => { - if (current === next) return current - return next - }) - - onPageSizeChange?.(next) - - if (page !== 1) { - onPageChange(1) - } - }, [onPageSizeChange, onPageChange, page]) + const handleGalleryRecommendedPageSizeChange = useCallback((size: number) => { + const next = Math.max(2, Math.min(24, Math.round(size))) + if (next === galleryPageSize) return + setGalleryPageSize(next) + }, [galleryPageSize]) const effectivePageSize = view === 'gallery' diff --git a/frontend/src/components/ui/FinishedDownloadsGalleryView.tsx b/frontend/src/components/ui/FinishedDownloadsGalleryView.tsx index d042e4b..f8875c6 100644 --- a/frontend/src/components/ui/FinishedDownloadsGalleryView.tsx +++ b/frontend/src/components/ui/FinishedDownloadsGalleryView.tsx @@ -177,6 +177,8 @@ function FinishedDownloadsGalleryView({ const rootRef = useRef(null) const lastRecommendedPageSizeRef = useRef(null) + const shrinkTimerRef = useRef(null) + const rafRef = useRef(null) const recommendPageSize = useCallback(() => { if (!onRecommendedPageSizeChange) return @@ -184,23 +186,72 @@ function FinishedDownloadsGalleryView({ const el = rootRef.current if (!el) return - const containerWidth = el.clientWidth - if (!Number.isFinite(containerWidth) || containerWidth <= 0) return + const measureAndApply = () => { + const containerWidth = el.clientWidth + if (!Number.isFinite(containerWidth) || containerWidth <= 0) return - const gapPx = 12 // entspricht Tailwind gap-3 - const columns = Math.max( - 1, - Math.floor((containerWidth + gapPx) / (minCardWidth + gapPx)) - ) + const gapPx = 12 - const rowsPerPage = 2 + const columns = Math.max( + 1, + Math.floor((containerWidth + gapPx) / (minCardWidth + gapPx)) + ) - const nextPageSize = clamp(columns * rowsPerPage, rowsPerPage, 24) + const rowsPerPage = 2 + const nextPageSize = clamp(columns * rowsPerPage, rowsPerPage, 24) + const currentPageSize = lastRecommendedPageSizeRef.current - if (lastRecommendedPageSizeRef.current === nextPageSize) return - lastRecommendedPageSizeRef.current = nextPageSize + if (currentPageSize === nextPageSize) return - onRecommendedPageSizeChange(nextPageSize) + // Größer werden: sofort übernehmen + if (currentPageSize == null || nextPageSize > currentPageSize) { + if (shrinkTimerRef.current !== null) { + window.clearTimeout(shrinkTimerRef.current) + shrinkTimerRef.current = null + } + + lastRecommendedPageSizeRef.current = 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]) useEffect(() => { @@ -219,6 +270,16 @@ function FinishedDownloadsGalleryView({ return () => { ro.disconnect() 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])