todo
This commit is contained in:
parent
3052836f3b
commit
df8e4f747f
@ -206,7 +206,7 @@ func (d *dummyResponseWriter) WriteHeader(statusCode int) {}
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
jobs = map[string]*RecordJob{}
|
jobs = map[string]*RecordJob{}
|
||||||
jobsMu = sync.Mutex{}
|
jobsMu = sync.RWMutex{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func startPreviewIdleKiller() {
|
func startPreviewIdleKiller() {
|
||||||
@ -214,21 +214,21 @@ func startPreviewIdleKiller() {
|
|||||||
go func() {
|
go func() {
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
for range t.C {
|
for range t.C {
|
||||||
jobsMu.Lock()
|
jobsMu.RLock()
|
||||||
list := make([]*RecordJob, 0, len(jobs))
|
list := make([]*RecordJob, 0, len(jobs))
|
||||||
for _, j := range jobs {
|
for _, j := range jobs {
|
||||||
if j != nil {
|
if j != nil {
|
||||||
list = append(list, j)
|
list = append(list, j)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jobsMu.Unlock()
|
jobsMu.RUnlock()
|
||||||
|
|
||||||
for _, j := range list {
|
for _, j := range list {
|
||||||
jobsMu.Lock()
|
jobsMu.RLock()
|
||||||
cmdRunning := j.previewCmd != nil
|
cmdRunning := j.previewCmd != nil
|
||||||
last := j.previewLastHit
|
last := j.previewLastHit
|
||||||
st := j.Status
|
st := j.Status
|
||||||
jobsMu.Unlock()
|
jobsMu.RUnlock()
|
||||||
|
|
||||||
if !cmdRunning {
|
if !cmdRunning {
|
||||||
continue
|
continue
|
||||||
|
|||||||
@ -1068,20 +1068,70 @@ func applyFinishedPhaseTruthToRecordJobMeta(j *RecordJob) {
|
|||||||
|
|
||||||
// ---------------- Handlers ----------------
|
// ---------------- 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) {
|
func recordList(w http.ResponseWriter, r *http.Request) {
|
||||||
if !mustMethod(w, r, http.MethodGet) {
|
if !mustMethod(w, r, http.MethodGet) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jobsMu.Lock()
|
jobsMu.RLock()
|
||||||
list := make([]*RecordJob, 0, len(jobs))
|
list := make([]recordListItem, 0, len(jobs))
|
||||||
for _, j := range jobs {
|
for _, j := range jobs {
|
||||||
if j == nil || j.Hidden {
|
if j == nil || j.Hidden {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
list = append(list, j)
|
list = append(list, snapshotRecordJobForList(j))
|
||||||
}
|
}
|
||||||
jobsMu.Unlock()
|
jobsMu.RUnlock()
|
||||||
|
|
||||||
sort.Slice(list, func(i, j int) bool {
|
sort.Slice(list, func(i, j int) bool {
|
||||||
return list[i].StartedAt.After(list[j].StartedAt)
|
return list[i].StartedAt.After(list[j].StartedAt)
|
||||||
@ -1267,9 +1317,9 @@ func recordRemoveQueuedPostwork(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jobsMu.Lock()
|
jobsMu.RLock()
|
||||||
job, ok := jobs[id]
|
job, ok := jobs[id]
|
||||||
jobsMu.Unlock()
|
jobsMu.RUnlock()
|
||||||
if !ok || job == nil {
|
if !ok || job == nil {
|
||||||
http.Error(w, "job nicht gefunden", http.StatusNotFound)
|
http.Error(w, "job nicht gefunden", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
@ -1411,9 +1461,9 @@ func recordStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
jobsMu.Lock()
|
jobsMu.RLock()
|
||||||
job, ok := jobs[id]
|
job, ok := jobs[id]
|
||||||
jobsMu.Unlock()
|
jobsMu.RUnlock()
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "job nicht gefunden", http.StatusNotFound)
|
http.Error(w, "job nicht gefunden", http.StatusNotFound)
|
||||||
@ -1431,9 +1481,9 @@ func recordStop(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
id := q(r, "id")
|
id := q(r, "id")
|
||||||
|
|
||||||
jobsMu.Lock()
|
jobsMu.RLock()
|
||||||
job, ok := jobs[id]
|
job, ok := jobs[id]
|
||||||
jobsMu.Unlock()
|
jobsMu.RUnlock()
|
||||||
if !ok {
|
if !ok {
|
||||||
http.Error(w, "job nicht gefunden", http.StatusNotFound)
|
http.Error(w, "job nicht gefunden", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
@ -1754,6 +1804,33 @@ func buildDoneIndex(doneAbs string) ([]doneIndexItem, map[string][]int) {
|
|||||||
|
|
||||||
// ---------------- Done meta + list ----------------
|
// ---------------- 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) {
|
func recordDoneMeta(w http.ResponseWriter, r *http.Request) {
|
||||||
if !mustMethod(w, r, http.MethodGet) {
|
if !mustMethod(w, r, http.MethodGet) {
|
||||||
return
|
return
|
||||||
@ -1884,34 +1961,7 @@ func recordDoneMeta(w http.ResponseWriter, r *http.Request) {
|
|||||||
sortedAll := doneCache.sortedIdx
|
sortedAll := doneCache.sortedIdx
|
||||||
doneCache.mu.Unlock()
|
doneCache.mu.Unlock()
|
||||||
|
|
||||||
isActivePostworkOutput := func(fullPath string) bool {
|
activePostwork := activePostworkBasenameSet()
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
count := 0
|
count := 0
|
||||||
if qModel == "" {
|
if qModel == "" {
|
||||||
@ -1922,7 +1972,7 @@ func recordDoneMeta(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
for _, idx := range sortedAll[incKey+"|completed_desc"] {
|
for _, idx := range sortedAll[incKey+"|completed_desc"] {
|
||||||
it := items[idx]
|
it := items[idx]
|
||||||
if isActivePostworkOutput(it.job.Output) {
|
if _, exists := activePostwork[strings.ToLower(filepath.Base(it.job.Output))]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
count++
|
count++
|
||||||
@ -1935,7 +1985,7 @@ func recordDoneMeta(w http.ResponseWriter, r *http.Request) {
|
|||||||
if it.modelKey != qModel {
|
if it.modelKey != qModel {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isActivePostworkOutput(it.job.Output) {
|
if _, exists := activePostwork[strings.ToLower(filepath.Base(it.job.Output))]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
count++
|
count++
|
||||||
@ -2157,34 +2207,7 @@ func recordDoneList(w http.ResponseWriter, r *http.Request) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
isActivePostworkOutput := func(fullPath string) bool {
|
activePostwork := activePostworkBasenameSet()
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
filteredIdx := make([]int, 0, len(idx))
|
filteredIdx := make([]int, 0, len(idx))
|
||||||
for _, ii := range idx {
|
for _, ii := range idx {
|
||||||
@ -2192,7 +2215,7 @@ func recordDoneList(w http.ResponseWriter, r *http.Request) {
|
|||||||
if it.job == nil {
|
if it.job == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if isActivePostworkOutput(it.job.Output) {
|
if _, exists := activePostwork[strings.ToLower(filepath.Base(it.job.Output))]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
filteredIdx = append(filteredIdx, ii)
|
filteredIdx = append(filteredIdx, ii)
|
||||||
|
|||||||
@ -54,6 +54,9 @@ func setJobProgress(job *RecordJob, phase string, pct int) {
|
|||||||
jobsMu.Lock()
|
jobsMu.Lock()
|
||||||
defer jobsMu.Unlock()
|
defer jobsMu.Unlock()
|
||||||
|
|
||||||
|
oldPhase := job.Phase
|
||||||
|
oldProgress := job.Progress
|
||||||
|
|
||||||
inPostwork := job.EndedAt != nil || (strings.TrimSpace(job.Phase) != "" && strings.ToLower(strings.TrimSpace(job.Phase)) != "recording")
|
inPostwork := job.EndedAt != nil || (strings.TrimSpace(job.Phase) != "" && strings.ToLower(strings.TrimSpace(job.Phase)) != "recording")
|
||||||
if inPostwork {
|
if inPostwork {
|
||||||
if phaseLower == "" || phaseLower == "recording" {
|
if phaseLower == "" || phaseLower == "recording" {
|
||||||
@ -61,40 +64,45 @@ func setJobProgress(job *RecordJob, phase string, pct int) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newPhase := job.Phase
|
||||||
if phase != "" {
|
if phase != "" {
|
||||||
job.Phase = phase
|
newPhase = phase
|
||||||
}
|
}
|
||||||
|
|
||||||
// recording = direkter Prozentwert
|
newProgress := job.Progress
|
||||||
|
|
||||||
if !inPostwork {
|
if !inPostwork {
|
||||||
if pct < job.Progress {
|
if pct < newProgress {
|
||||||
pct = job.Progress
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// postwork-Phasen: pct ist IMMER lokal 0..100 innerhalb der Phase
|
job.Phase = newPhase
|
||||||
r := rangeFor(phaseLower)
|
job.Progress = newProgress
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- Preview scrubber ----------------
|
// ---------------- Preview scrubber ----------------
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"databaseUrl": "postgres://postgres@127.0.0.1:5432/nsfwapp?sslmode=disable",
|
"databaseUrl": "postgres://postgres@127.0.0.1:5432/nsfwapp?sslmode=disable",
|
||||||
"encryptedDbPassword": "vkNvHM+/AgEkRPW0X+rDDys4bPb5Q7ZPGqPSADCluIJ6GCDGNGlDy3s=",
|
"encryptedDbPassword": "vkNvHM+/AgEkRPW0X+rDDys4bPb5Q7ZPGqPSADCluIJ6GCDGNGlDy3s=",
|
||||||
"recordDir": "C:\\Users\\Rother\\Desktop\\Privat\\records",
|
"recordDir": "C:\\Users\\Chris\\Desktop\\privat\\records",
|
||||||
"doneDir": "C:\\Users\\Rother\\Desktop\\Privat\\records\\done",
|
"doneDir": "C:\\Users\\Chris\\Desktop\\privat\\records\\done",
|
||||||
"ffmpegPath": "",
|
"ffmpegPath": "",
|
||||||
"autoAddToDownloadList": true,
|
"autoAddToDownloadList": true,
|
||||||
"autoStartAddedDownloads": true,
|
"autoStartAddedDownloads": true,
|
||||||
"enableConcurrentDownloadsLimit": true,
|
"enableConcurrentDownloadsLimit": true,
|
||||||
"maxConcurrentDownloads": 40,
|
"maxConcurrentDownloads": 80,
|
||||||
"useChaturbateApi": true,
|
"useChaturbateApi": true,
|
||||||
"useMyFreeCamsWatcher": true,
|
"useMyFreeCamsWatcher": true,
|
||||||
"autoDeleteSmallDownloads": true,
|
"autoDeleteSmallDownloads": true,
|
||||||
@ -17,5 +17,5 @@
|
|||||||
"teaserPlayback": "all",
|
"teaserPlayback": "all",
|
||||||
"teaserAudio": true,
|
"teaserAudio": true,
|
||||||
"enableNotifications": 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])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
|
|
||||||
@ -2138,19 +2138,23 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onUpdated = () => void load()
|
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('recorder-settings-updated', onUpdated as EventListener)
|
||||||
window.addEventListener('focus', onFocus)
|
document.addEventListener('visibilitychange', onVisible)
|
||||||
document.addEventListener('visibilitychange', onFocus)
|
|
||||||
|
|
||||||
load()
|
void load()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
window.removeEventListener('recorder-settings-updated', onUpdated as EventListener)
|
window.removeEventListener('recorder-settings-updated', onUpdated as EventListener)
|
||||||
window.removeEventListener('focus', onFocus)
|
document.removeEventListener('visibilitychange', onVisible)
|
||||||
document.removeEventListener('visibilitychange', onFocus)
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@ -2313,37 +2317,6 @@ export default function App() {
|
|||||||
localStorage.setItem(COOKIE_STORAGE_KEY, JSON.stringify(cookies))
|
localStorage.setItem(COOKIE_STORAGE_KEY, JSON.stringify(cookies))
|
||||||
}, [cookies, cookiesLoaded])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!authed) return
|
if (!authed) return
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import DownloadsCardRow, {
|
|||||||
} from './DownloadsCardRow'
|
} from './DownloadsCardRow'
|
||||||
import SwipeActionRow from './SwipeActionRow'
|
import SwipeActionRow from './SwipeActionRow'
|
||||||
import RecordJobActions from './RecordJobActions'
|
import RecordJobActions from './RecordJobActions'
|
||||||
|
import LazyMountWhenVisible from './LazyMountWhenVisible'
|
||||||
|
|
||||||
type AutostartState = {
|
type AutostartState = {
|
||||||
paused?: boolean
|
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({
|
export default function Downloads({
|
||||||
jobs,
|
jobs,
|
||||||
pending = [],
|
pending = [],
|
||||||
@ -994,9 +955,8 @@ export default function Downloads({
|
|||||||
return (
|
return (
|
||||||
<div className="grid w-[96px] h-[60px] overflow-hidden rounded-md">
|
<div className="grid w-[96px] h-[60px] overflow-hidden rounded-md">
|
||||||
<LazyMountWhenVisible
|
<LazyMountWhenVisible
|
||||||
placeholder={
|
className="w-full h-full"
|
||||||
<div className="h-full w-full bg-gray-100 dark:bg-white/10" />
|
placeholder={<div className="h-full w-full bg-gray-100 dark:bg-white/10" />}
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<ModelPreview
|
<ModelPreview
|
||||||
jobId={j.id}
|
jobId={j.id}
|
||||||
|
|||||||
@ -2,11 +2,13 @@
|
|||||||
|
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||||
import Button from './Button'
|
import Button from './Button'
|
||||||
import ModelPreview from './ModelPreview'
|
import ModelPreview from './ModelPreview'
|
||||||
import ProgressBar from './ProgressBar'
|
import ProgressBar from './ProgressBar'
|
||||||
import RecordJobActions from './RecordJobActions'
|
import RecordJobActions from './RecordJobActions'
|
||||||
import type { RecordJob } from '../../types'
|
import type { RecordJob } from '../../types'
|
||||||
|
import LazyMountWhenVisible from './LazyMountWhenVisible'
|
||||||
|
|
||||||
export type PendingWatchedRoom = WaitingModelRow & {
|
export type PendingWatchedRoom = WaitingModelRow & {
|
||||||
currentShow: string
|
currentShow: string
|
||||||
@ -704,24 +706,29 @@ export default function DownloadsCardRow({
|
|||||||
dark:bg-white/10 dark:ring-white/10
|
dark:bg-white/10 dark:ring-white/10
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<ModelPreview
|
<LazyMountWhenVisible
|
||||||
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"
|
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>
|
||||||
|
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
|
|||||||
@ -814,7 +814,13 @@ export default function FinishedVideoPreview({
|
|||||||
teaserOk &&
|
teaserOk &&
|
||||||
Boolean(teaserSrc) &&
|
Boolean(teaserSrc) &&
|
||||||
!showingInlineVideo &&
|
!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)
|
// ✅ Progress-Quelle: NUR das Element, das wirklich spielt (für "Sprünge" wichtig)
|
||||||
const progressVideoRef =
|
const progressVideoRef =
|
||||||
@ -1298,7 +1304,7 @@ export default function FinishedVideoPreview({
|
|||||||
data-fps={typeof effectiveFPS === 'number' ? String(effectiveFPS) : undefined}
|
data-fps={typeof effectiveFPS === 'number' ? String(effectiveFPS) : undefined}
|
||||||
>
|
>
|
||||||
{/* ✅ Thumb IMMER als Fallback/Background */}
|
{/* ✅ Thumb IMMER als Fallback/Background */}
|
||||||
{shouldLoadStill && thumbSrc && thumbOk ? (
|
{showStillImage && thumbSrc && thumbOk ? (
|
||||||
<img
|
<img
|
||||||
src={thumbSrc}
|
src={thumbSrc}
|
||||||
loading={alwaysLoadStill ? 'eager' : 'lazy'}
|
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