todo
This commit is contained in:
parent
3052836f3b
commit
df8e4f747f
@ -206,7 +206,7 @@ func (d *dummyResponseWriter) WriteHeader(statusCode int) {}
|
||||
|
||||
var (
|
||||
jobs = map[string]*RecordJob{}
|
||||
jobsMu = sync.Mutex{}
|
||||
jobsMu = sync.RWMutex{}
|
||||
)
|
||||
|
||||
func startPreviewIdleKiller() {
|
||||
@ -214,21 +214,21 @@ func startPreviewIdleKiller() {
|
||||
go func() {
|
||||
defer t.Stop()
|
||||
for range t.C {
|
||||
jobsMu.Lock()
|
||||
jobsMu.RLock()
|
||||
list := make([]*RecordJob, 0, len(jobs))
|
||||
for _, j := range jobs {
|
||||
if j != nil {
|
||||
list = append(list, j)
|
||||
}
|
||||
}
|
||||
jobsMu.Unlock()
|
||||
jobsMu.RUnlock()
|
||||
|
||||
for _, j := range list {
|
||||
jobsMu.Lock()
|
||||
jobsMu.RLock()
|
||||
cmdRunning := j.previewCmd != nil
|
||||
last := j.previewLastHit
|
||||
st := j.Status
|
||||
jobsMu.Unlock()
|
||||
jobsMu.RUnlock()
|
||||
|
||||
if !cmdRunning {
|
||||
continue
|
||||
|
||||
@ -1068,20 +1068,70 @@ func applyFinishedPhaseTruthToRecordJobMeta(j *RecordJob) {
|
||||
|
||||
// ---------------- Handlers ----------------
|
||||
|
||||
type recordListItem struct {
|
||||
ID string `json:"id"`
|
||||
SourceURL string `json:"sourceUrl"`
|
||||
Status string `json:"status"`
|
||||
StartedAt time.Time `json:"startedAt"`
|
||||
StartedAtMs int64 `json:"startedAtMs"`
|
||||
EndedAt *time.Time `json:"endedAt,omitempty"`
|
||||
EndedAtMs int64 `json:"endedAtMs,omitempty"`
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Phase string `json:"phase,omitempty"`
|
||||
Progress int `json:"progress,omitempty"`
|
||||
SizeBytes int64 `json:"sizeBytes,omitempty"`
|
||||
DurationSeconds float64 `json:"durationSeconds,omitempty"`
|
||||
PreviewState string `json:"previewState,omitempty"`
|
||||
RoomStatus string `json:"roomStatus,omitempty"`
|
||||
IsOnline bool `json:"isOnline,omitempty"`
|
||||
ModelImageURL string `json:"modelImageUrl,omitempty"`
|
||||
ModelChatRoomURL string `json:"modelChatRoomUrl,omitempty"`
|
||||
PostWorkKey string `json:"postWorkKey,omitempty"`
|
||||
PostWork *PostWorkKeyStatus `json:"postWork,omitempty"`
|
||||
}
|
||||
|
||||
func snapshotRecordJobForList(j *RecordJob) recordListItem {
|
||||
var postWorkCopy *PostWorkKeyStatus
|
||||
if j.PostWork != nil {
|
||||
pw := *j.PostWork
|
||||
postWorkCopy = &pw
|
||||
}
|
||||
|
||||
return recordListItem{
|
||||
ID: j.ID,
|
||||
SourceURL: j.SourceURL,
|
||||
Status: string(j.Status),
|
||||
StartedAt: j.StartedAt,
|
||||
StartedAtMs: j.StartedAtMs,
|
||||
EndedAt: j.EndedAt,
|
||||
EndedAtMs: j.EndedAtMs,
|
||||
Output: j.Output,
|
||||
Error: j.Error,
|
||||
Phase: j.Phase,
|
||||
Progress: j.Progress,
|
||||
SizeBytes: j.SizeBytes,
|
||||
DurationSeconds: j.DurationSeconds,
|
||||
PreviewState: j.PreviewState,
|
||||
PostWorkKey: j.PostWorkKey,
|
||||
PostWork: postWorkCopy,
|
||||
}
|
||||
}
|
||||
|
||||
func recordList(w http.ResponseWriter, r *http.Request) {
|
||||
if !mustMethod(w, r, http.MethodGet) {
|
||||
return
|
||||
}
|
||||
|
||||
jobsMu.Lock()
|
||||
list := make([]*RecordJob, 0, len(jobs))
|
||||
jobsMu.RLock()
|
||||
list := make([]recordListItem, 0, len(jobs))
|
||||
for _, j := range jobs {
|
||||
if j == nil || j.Hidden {
|
||||
continue
|
||||
}
|
||||
list = append(list, j)
|
||||
list = append(list, snapshotRecordJobForList(j))
|
||||
}
|
||||
jobsMu.Unlock()
|
||||
jobsMu.RUnlock()
|
||||
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
return list[i].StartedAt.After(list[j].StartedAt)
|
||||
@ -1267,9 +1317,9 @@ func recordRemoveQueuedPostwork(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
jobsMu.Lock()
|
||||
jobsMu.RLock()
|
||||
job, ok := jobs[id]
|
||||
jobsMu.Unlock()
|
||||
jobsMu.RUnlock()
|
||||
if !ok || job == nil {
|
||||
http.Error(w, "job nicht gefunden", http.StatusNotFound)
|
||||
return
|
||||
@ -1411,9 +1461,9 @@ func recordStatus(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
jobsMu.Lock()
|
||||
jobsMu.RLock()
|
||||
job, ok := jobs[id]
|
||||
jobsMu.Unlock()
|
||||
jobsMu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
http.Error(w, "job nicht gefunden", http.StatusNotFound)
|
||||
@ -1431,9 +1481,9 @@ func recordStop(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
id := q(r, "id")
|
||||
|
||||
jobsMu.Lock()
|
||||
jobsMu.RLock()
|
||||
job, ok := jobs[id]
|
||||
jobsMu.Unlock()
|
||||
jobsMu.RUnlock()
|
||||
if !ok {
|
||||
http.Error(w, "job nicht gefunden", http.StatusNotFound)
|
||||
return
|
||||
@ -1754,6 +1804,33 @@ func buildDoneIndex(doneAbs string) ([]doneIndexItem, map[string][]int) {
|
||||
|
||||
// ---------------- Done meta + list ----------------
|
||||
|
||||
func activePostworkBasenameSet() map[string]struct{} {
|
||||
out := make(map[string]struct{})
|
||||
|
||||
jobsMu.RLock()
|
||||
defer jobsMu.RUnlock()
|
||||
|
||||
for _, j := range jobs {
|
||||
if j == nil {
|
||||
continue
|
||||
}
|
||||
if !isPostworkJob(j) {
|
||||
continue
|
||||
}
|
||||
if isTerminalJobStatus(j.Status) {
|
||||
continue
|
||||
}
|
||||
|
||||
base := strings.TrimSpace(filepath.Base(j.Output))
|
||||
if base == "" {
|
||||
continue
|
||||
}
|
||||
out[strings.ToLower(base)] = struct{}{}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func recordDoneMeta(w http.ResponseWriter, r *http.Request) {
|
||||
if !mustMethod(w, r, http.MethodGet) {
|
||||
return
|
||||
@ -1884,34 +1961,7 @@ func recordDoneMeta(w http.ResponseWriter, r *http.Request) {
|
||||
sortedAll := doneCache.sortedIdx
|
||||
doneCache.mu.Unlock()
|
||||
|
||||
isActivePostworkOutput := func(fullPath string) bool {
|
||||
base := strings.TrimSpace(filepath.Base(fullPath))
|
||||
if base == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
jobsMu.Lock()
|
||||
defer jobsMu.Unlock()
|
||||
|
||||
for _, j := range jobs {
|
||||
if j == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !isPostworkJob(j) {
|
||||
continue
|
||||
}
|
||||
if isTerminalJobStatus(j.Status) {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.EqualFold(filepath.Base(strings.TrimSpace(j.Output)), base) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
activePostwork := activePostworkBasenameSet()
|
||||
|
||||
count := 0
|
||||
if qModel == "" {
|
||||
@ -1922,7 +1972,7 @@ func recordDoneMeta(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
for _, idx := range sortedAll[incKey+"|completed_desc"] {
|
||||
it := items[idx]
|
||||
if isActivePostworkOutput(it.job.Output) {
|
||||
if _, exists := activePostwork[strings.ToLower(filepath.Base(it.job.Output))]; exists {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
@ -1935,7 +1985,7 @@ func recordDoneMeta(w http.ResponseWriter, r *http.Request) {
|
||||
if it.modelKey != qModel {
|
||||
continue
|
||||
}
|
||||
if isActivePostworkOutput(it.job.Output) {
|
||||
if _, exists := activePostwork[strings.ToLower(filepath.Base(it.job.Output))]; exists {
|
||||
continue
|
||||
}
|
||||
count++
|
||||
@ -2157,34 +2207,7 @@ func recordDoneList(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
isActivePostworkOutput := func(fullPath string) bool {
|
||||
base := strings.TrimSpace(filepath.Base(fullPath))
|
||||
if base == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
jobsMu.Lock()
|
||||
defer jobsMu.Unlock()
|
||||
|
||||
for _, j := range jobs {
|
||||
if j == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if !isPostworkJob(j) {
|
||||
continue
|
||||
}
|
||||
if isTerminalJobStatus(j.Status) {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.EqualFold(filepath.Base(strings.TrimSpace(j.Output)), base) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
activePostwork := activePostworkBasenameSet()
|
||||
|
||||
filteredIdx := make([]int, 0, len(idx))
|
||||
for _, ii := range idx {
|
||||
@ -2192,7 +2215,7 @@ func recordDoneList(w http.ResponseWriter, r *http.Request) {
|
||||
if it.job == nil {
|
||||
continue
|
||||
}
|
||||
if isActivePostworkOutput(it.job.Output) {
|
||||
if _, exists := activePostwork[strings.ToLower(filepath.Base(it.job.Output))]; exists {
|
||||
continue
|
||||
}
|
||||
filteredIdx = append(filteredIdx, ii)
|
||||
|
||||
@ -54,6 +54,9 @@ func setJobProgress(job *RecordJob, phase string, pct int) {
|
||||
jobsMu.Lock()
|
||||
defer jobsMu.Unlock()
|
||||
|
||||
oldPhase := job.Phase
|
||||
oldProgress := job.Progress
|
||||
|
||||
inPostwork := job.EndedAt != nil || (strings.TrimSpace(job.Phase) != "" && strings.ToLower(strings.TrimSpace(job.Phase)) != "recording")
|
||||
if inPostwork {
|
||||
if phaseLower == "" || phaseLower == "recording" {
|
||||
@ -61,40 +64,45 @@ func setJobProgress(job *RecordJob, phase string, pct int) {
|
||||
}
|
||||
}
|
||||
|
||||
newPhase := job.Phase
|
||||
if phase != "" {
|
||||
job.Phase = phase
|
||||
newPhase = phase
|
||||
}
|
||||
|
||||
// recording = direkter Prozentwert
|
||||
newProgress := job.Progress
|
||||
|
||||
if !inPostwork {
|
||||
if pct < job.Progress {
|
||||
pct = job.Progress
|
||||
if pct < newProgress {
|
||||
pct = newProgress
|
||||
}
|
||||
job.Progress = pct
|
||||
newProgress = pct
|
||||
} else {
|
||||
r := rangeFor(phaseLower)
|
||||
width := float64(r.end - r.start)
|
||||
|
||||
mapped := r.start
|
||||
if width > 0 {
|
||||
mapped = r.start + int(math.Round((float64(pct)/100.0)*width))
|
||||
}
|
||||
|
||||
if mapped < r.start {
|
||||
mapped = r.start
|
||||
}
|
||||
if mapped > r.end {
|
||||
mapped = r.end
|
||||
}
|
||||
if mapped < newProgress {
|
||||
mapped = newProgress
|
||||
}
|
||||
newProgress = mapped
|
||||
}
|
||||
|
||||
if oldPhase == newPhase && oldProgress == newProgress {
|
||||
return
|
||||
}
|
||||
|
||||
// postwork-Phasen: pct ist IMMER lokal 0..100 innerhalb der Phase
|
||||
r := rangeFor(phaseLower)
|
||||
width := float64(r.end - r.start)
|
||||
|
||||
mapped := r.start
|
||||
if width > 0 {
|
||||
mapped = r.start + int(math.Round((float64(pct)/100.0)*width))
|
||||
}
|
||||
|
||||
if mapped < r.start {
|
||||
mapped = r.start
|
||||
}
|
||||
if mapped > r.end {
|
||||
mapped = r.end
|
||||
}
|
||||
|
||||
if mapped < job.Progress {
|
||||
mapped = job.Progress
|
||||
}
|
||||
|
||||
job.Progress = mapped
|
||||
job.Phase = newPhase
|
||||
job.Progress = newProgress
|
||||
}
|
||||
|
||||
// ---------------- Preview scrubber ----------------
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
{
|
||||
"databaseUrl": "postgres://postgres@127.0.0.1:5432/nsfwapp?sslmode=disable",
|
||||
"encryptedDbPassword": "vkNvHM+/AgEkRPW0X+rDDys4bPb5Q7ZPGqPSADCluIJ6GCDGNGlDy3s=",
|
||||
"recordDir": "C:\\Users\\Rother\\Desktop\\Privat\\records",
|
||||
"doneDir": "C:\\Users\\Rother\\Desktop\\Privat\\records\\done",
|
||||
"recordDir": "C:\\Users\\Chris\\Desktop\\privat\\records",
|
||||
"doneDir": "C:\\Users\\Chris\\Desktop\\privat\\records\\done",
|
||||
"ffmpegPath": "",
|
||||
"autoAddToDownloadList": true,
|
||||
"autoStartAddedDownloads": true,
|
||||
"enableConcurrentDownloadsLimit": true,
|
||||
"maxConcurrentDownloads": 40,
|
||||
"maxConcurrentDownloads": 80,
|
||||
"useChaturbateApi": true,
|
||||
"useMyFreeCamsWatcher": true,
|
||||
"autoDeleteSmallDownloads": true,
|
||||
@ -17,5 +17,5 @@
|
||||
"teaserPlayback": "all",
|
||||
"teaserAudio": true,
|
||||
"enableNotifications": true,
|
||||
"encryptedCookies": "tZzqhwM+6rrdV36edfxpkhfikSfzfs4VZESLJIpYw1U4Bwn7vo+P8Sq86M7nvXpvbQVg00QLl5Wo2dD2agojU58GYY5P5P6Wpc1ZPr2T409BaQXRA/WhHiZv7z/b4JoVYX7fScZ4FDtscQYuTdMpnU6Wr725j66Qg2x8VgXqUHb1qceUes9P3L2kc8MAvZHWnZHIEjbgDW9NAZOADlbBxOonmc5hMGpz5xzmpWWV7Gqg6n1i75U/XBDV6Ks1Jn5Ic9dwi0f1BH6zo56v5oDdPAxKPlw01TUsV9ZAA+u+TjWUiq52AJP/C20bujUEsmQupqKxMs3E4rs6DEb7SYfbQ6SSryKyYYLCv0T3+CbeUgDmgYd2n+yjatE8udHvmkCtZCWvBIK4lSglsVeFq1prgpiGBL2ItyLcR13musHGbZ7Whq/p1btJsFDTTyfGnILW+sRUcd1Y/17YM5zKAWYyi81qKop6DYqh9kBoM2y3DYVxsLkb9+lUmwN6N7oopqh/bJ8vxJKyC74="
|
||||
"encryptedCookies": "yDldmKNsPH20hdVRPLdr8nVj4CoIC/NHiXESzcqLLSsEcgi4P7V7+xcKw2d3I/GnGR6uVNQYBrc4c+YeUs9J7wbn9Ir+BdaFDAWZVWBoaHgdcgYLaKUxIMO273zfi+jwkPF4DHuSBw9gkAHgKeF15H+azEQEDjJc8VH1C+7/F6qhkHBtFKKDiAkj8a4+cqmzr6Cr+tdraCaTxu4YZmqA/3erMRl/HA6egQFAcfGB2KAcfBqQagvXZ+R8SgfTwJiLG7RkH5Xdh3fT34Cc03sbyXCJxMzFUd4RKgCRyM2oUfqXq/oOL8AbZxm0oi2Mh+8TaqbAfdu1CstnPlAFAk7s3gMvp4Ni5QVwtfKQYzSYThKyaZSV2wnWZIWOXh9Zb3SvmAQBANJUHG9WUr/nyxqDPj03bIKNWA8/ZflAyVtCQtlpQU4Nrqe3Fhl/ALBuhgTcwx8X77rs1Py7sR9SJ8Ks+bKf8ziE04oQem6HQsIf1TAZTpYbpd7mD7NrbUuDtbJ3RabZMppygQc="
|
||||
}
|
||||
|
||||
@ -2124,7 +2124,7 @@ export default function App() {
|
||||
}
|
||||
}, [applyPendingRoomSnapshot, fetchChaturbateCurrentShow])
|
||||
|
||||
// ✅ settings: nur einmal laden + nach Save-Event + optional bei focus/visibility
|
||||
// ✅ settings: initial + nach Save-Event + nur beim echten Wechsel hidden -> visible
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
@ -2138,19 +2138,23 @@ export default function App() {
|
||||
}
|
||||
|
||||
const onUpdated = () => void load()
|
||||
const onFocus = () => void load()
|
||||
|
||||
let wasHidden = document.hidden
|
||||
const onVisible = () => {
|
||||
const becameVisible = wasHidden && !document.hidden
|
||||
wasHidden = document.hidden
|
||||
if (becameVisible) void load()
|
||||
}
|
||||
|
||||
window.addEventListener('recorder-settings-updated', onUpdated as EventListener)
|
||||
window.addEventListener('focus', onFocus)
|
||||
document.addEventListener('visibilitychange', onFocus)
|
||||
document.addEventListener('visibilitychange', onVisible)
|
||||
|
||||
load()
|
||||
void load()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
window.removeEventListener('recorder-settings-updated', onUpdated as EventListener)
|
||||
window.removeEventListener('focus', onFocus)
|
||||
document.removeEventListener('visibilitychange', onFocus)
|
||||
document.removeEventListener('visibilitychange', onVisible)
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -2313,37 +2317,6 @@ export default function App() {
|
||||
localStorage.setItem(COOKIE_STORAGE_KEY, JSON.stringify(cookies))
|
||||
}, [cookies, cookiesLoaded])
|
||||
|
||||
useEffect(() => {
|
||||
// 1x initial / bei sort-wechsel (für Badge)
|
||||
void loadDoneCount()
|
||||
|
||||
const onVis = () => {
|
||||
if (!document.hidden) void loadDoneCount()
|
||||
}
|
||||
document.addEventListener('visibilitychange', onVis)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', onVis)
|
||||
}
|
||||
}, [loadDoneCount])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedTab !== 'finished') return
|
||||
|
||||
void loadDoneCount()
|
||||
|
||||
const onVis = () => {
|
||||
if (!document.hidden) {
|
||||
void loadDoneCount()
|
||||
}
|
||||
}
|
||||
document.addEventListener('visibilitychange', onVis)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', onVis)
|
||||
}
|
||||
}, [selectedTab, loadDoneCount])
|
||||
|
||||
useEffect(() => {
|
||||
if (!authed) return
|
||||
|
||||
|
||||
@ -18,6 +18,7 @@ import DownloadsCardRow, {
|
||||
} from './DownloadsCardRow'
|
||||
import SwipeActionRow from './SwipeActionRow'
|
||||
import RecordJobActions from './RecordJobActions'
|
||||
import LazyMountWhenVisible from './LazyMountWhenVisible'
|
||||
|
||||
type AutostartState = {
|
||||
paused?: boolean
|
||||
@ -564,46 +565,6 @@ 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 = [],
|
||||
@ -994,9 +955,8 @@ export default function Downloads({
|
||||
return (
|
||||
<div className="grid w-[96px] h-[60px] overflow-hidden rounded-md">
|
||||
<LazyMountWhenVisible
|
||||
placeholder={
|
||||
<div className="h-full w-full bg-gray-100 dark:bg-white/10" />
|
||||
}
|
||||
className="w-full h-full"
|
||||
placeholder={<div className="h-full w-full bg-gray-100 dark:bg-white/10" />}
|
||||
>
|
||||
<ModelPreview
|
||||
jobId={j.id}
|
||||
|
||||
@ -2,11 +2,13 @@
|
||||
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
import Button from './Button'
|
||||
import ModelPreview from './ModelPreview'
|
||||
import ProgressBar from './ProgressBar'
|
||||
import RecordJobActions from './RecordJobActions'
|
||||
import type { RecordJob } from '../../types'
|
||||
import LazyMountWhenVisible from './LazyMountWhenVisible'
|
||||
|
||||
export type PendingWatchedRoom = WaitingModelRow & {
|
||||
currentShow: string
|
||||
@ -704,24 +706,29 @@ export default function DownloadsCardRow({
|
||||
dark:bg-white/10 dark:ring-white/10
|
||||
"
|
||||
>
|
||||
<ModelPreview
|
||||
jobId={j.id}
|
||||
blur={blurPreviews}
|
||||
roomStatus={previewRoomStatusOfJob(
|
||||
j,
|
||||
roomStatusByModelKey,
|
||||
modelsByKey,
|
||||
growingByJobId
|
||||
)}
|
||||
alignStartAt={j.startedAt}
|
||||
alignEndAt={previewAlignEndAtOfJob(j)}
|
||||
alignEveryMs={10_000}
|
||||
fastRetryMs={1000}
|
||||
fastRetryMax={25}
|
||||
fastRetryWindowMs={60_000}
|
||||
thumbsCandidates={jobThumbsJPGCandidates(j)}
|
||||
<LazyMountWhenVisible
|
||||
className="w-full h-full"
|
||||
/>
|
||||
placeholder={<div className="h-full w-full bg-gray-100 dark:bg-white/10" />}
|
||||
>
|
||||
<ModelPreview
|
||||
jobId={j.id}
|
||||
blur={blurPreviews}
|
||||
roomStatus={previewRoomStatusOfJob(
|
||||
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>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
|
||||
@ -814,7 +814,13 @@ export default function FinishedVideoPreview({
|
||||
teaserOk &&
|
||||
Boolean(teaserSrc) &&
|
||||
!showingInlineVideo &&
|
||||
shouldPreloadAnimatedAssets
|
||||
shouldPreloadAnimatedAssets &&
|
||||
animatedTrigger !== 'hover'
|
||||
|
||||
const showStillImage =
|
||||
shouldLoadStill &&
|
||||
!showingInlineVideo &&
|
||||
!(animatedMode === 'teaser' && teaserActive)
|
||||
|
||||
// ✅ Progress-Quelle: NUR das Element, das wirklich spielt (für "Sprünge" wichtig)
|
||||
const progressVideoRef =
|
||||
@ -1298,7 +1304,7 @@ export default function FinishedVideoPreview({
|
||||
data-fps={typeof effectiveFPS === 'number' ? String(effectiveFPS) : undefined}
|
||||
>
|
||||
{/* ✅ Thumb IMMER als Fallback/Background */}
|
||||
{shouldLoadStill && thumbSrc && thumbOk ? (
|
||||
{showStillImage && thumbSrc && thumbOk ? (
|
||||
<img
|
||||
src={thumbSrc}
|
||||
loading={alwaysLoadStill ? 'eager' : 'lazy'}
|
||||
|
||||
56
frontend/src/components/ui/LazyMountWhenVisible.tsx
Normal file
56
frontend/src/components/ui/LazyMountWhenVisible.tsx
Normal file
@ -0,0 +1,56 @@
|
||||
// frontend\src\components\ui\LazyMountWhenVisible.tsx
|
||||
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||
|
||||
type Props = {
|
||||
children: ReactNode
|
||||
placeholder?: ReactNode
|
||||
rootMargin?: string
|
||||
threshold?: number
|
||||
className?: string
|
||||
once?: boolean
|
||||
}
|
||||
|
||||
export default function LazyMountWhenVisible({
|
||||
children,
|
||||
placeholder = null,
|
||||
rootMargin = '300px',
|
||||
threshold = 0.01,
|
||||
className,
|
||||
once = true,
|
||||
}: Props) {
|
||||
const hostRef = useRef<HTMLDivElement | null>(null)
|
||||
const [visible, setVisible] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const el = hostRef.current
|
||||
if (!el) return
|
||||
if (visible && once) return
|
||||
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const entry = entries[0]
|
||||
if (!entry) return
|
||||
|
||||
if (entry.isIntersecting) {
|
||||
setVisible(true)
|
||||
if (once) io.disconnect()
|
||||
} else if (!once) {
|
||||
setVisible(false)
|
||||
}
|
||||
},
|
||||
{ rootMargin, threshold }
|
||||
)
|
||||
|
||||
io.observe(el)
|
||||
return () => io.disconnect()
|
||||
}, [visible, once, rootMargin, threshold])
|
||||
|
||||
return (
|
||||
<div ref={hostRef} className={className}>
|
||||
{visible ? children : placeholder}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user