From ae957af7b2e1b53150146edf3f05e6f4afd9bd87 Mon Sep 17 00:00:00 2001 From: Linrador <68631622+Linrador@users.noreply.github.com> Date: Mon, 1 Jun 2026 09:16:21 +0200 Subject: [PATCH] bugfixes and optimizations --- backend/assets_generate.go | 2 +- backend/postwork.go | 52 +++++ backend/preview.go | 6 - backend/recorder_settings.json | 1 + backend/settings.go | 19 ++ backend/startup_leftovers.go | 179 +++++++++++++++++- backend/tasks_cleanup.go | 101 +++++++--- frontend/src/App.tsx | 128 +++++++++---- frontend/src/components/ui/Downloads.tsx | 2 +- .../src/components/ui/FinishedDownloads.tsx | 14 +- .../ui/FinishedDownloadsCardsView.tsx | 13 +- .../ui/FinishedDownloadsDesktopCard.tsx | 113 +++++++---- .../ui/FinishedDownloadsGalleryCard.tsx | 57 +++++- .../src/components/ui/PerformanceMonitor.tsx | 10 +- .../src/components/ui/RecorderSettings.tsx | 18 ++ frontend/src/components/ui/Tabs.tsx | 29 ++- frontend/src/components/ui/TrainingTab.tsx | 2 +- frontend/src/index.css | 31 +++ frontend/src/types.ts | 12 ++ 19 files changed, 626 insertions(+), 163 deletions(-) diff --git a/backend/assets_generate.go b/backend/assets_generate.go index 15d78b8..1110eb2 100644 --- a/backend/assets_generate.go +++ b/backend/assets_generate.go @@ -992,7 +992,7 @@ func enqueueDeferredAssetsAndAI(job *RecordJob, outPath string, sourceURL string sortBucket, sortName := postWorkSortForVideoPath(outPath) - ok := enrichQ.Enqueue(PostWorkTask{ + ok := enqueueEnrichPostwork(PostWorkTask{ Key: "enrich:" + assetID, Added: time.Now(), SortBucket: sortBucket, diff --git a/backend/postwork.go b/backend/postwork.go index a06dc2c..7643508 100644 --- a/backend/postwork.go +++ b/backend/postwork.go @@ -252,6 +252,45 @@ func (pq *PostWorkQueue) RemoveQueued(key string) bool { return true } +func (pq *PostWorkQueue) CancelAll() (queuedRemoved int, runningCancelled int) { + pq.mu.Lock() + + waiting := append([]string(nil), pq.waitingKeys...) + runningCancels := make([]context.CancelFunc, 0, len(pq.cancelByKey)) + + for key, cancel := range pq.cancelByKey { + if _, running := pq.runningKeys[key]; running && cancel != nil { + runningCancels = append(runningCancels, cancel) + } + } + + for _, key := range waiting { + delete(pq.inflight, key) + pq.unlockFileLocked(key) + + if pq.queued > 0 { + pq.queued-- + } + + queuedRemoved++ + } + + pq.waitingKeys = nil + pq.queue = nil + + runningCancelled = len(runningCancels) + + pq.mu.Unlock() + + for _, cancel := range runningCancels { + cancel() + } + + publishTaskState() + + return queuedRemoved, runningCancelled +} + func (pq *PostWorkQueue) workerLoop(id int) { for { pq.mu.Lock() @@ -393,6 +432,19 @@ func (pq *PostWorkQueue) StatusForKey(key string) PostWorkKeyStatus { var postWorkQ = NewPostWorkQueue(1024, 1) // schneller kritischer Pfad var enrichQ = NewPostWorkQueue(128, 1) // langsame Hintergrundaufgaben +func enrichPostworkEnabled() bool { + return getSettings().EnrichPostworkEnabled +} + +func enqueueEnrichPostwork(task PostWorkTask) bool { + if !enrichPostworkEnabled() { + appLogln("⏭️ enrich postwork deaktiviert, überspringe:", task.Key) + return false + } + + return enrichQ.Enqueue(task) +} + // --- Status Refresher (ehemals postwork_refresh.go) --- type enrichStatusSnapshot struct { diff --git a/backend/preview.go b/backend/preview.go index c1bbdb9..5d2ce22 100644 --- a/backend/preview.go +++ b/backend/preview.go @@ -1274,9 +1274,6 @@ func extractLastFrameJPG(path string) ([]byte, error) { } b := out.Bytes() - if len(b) == 0 { - return nil, appErrorf("ffmpeg last-frame jpg: empty output") - } return b, nil } @@ -1314,9 +1311,6 @@ func extractFrameAtTimeJPG(path string, seconds float64) ([]byte, error) { return nil, ffmpegPreviewError("ffmpeg frame-at-time jpg", err, stderr.String()) } b := out.Bytes() - if len(b) == 0 { - return nil, appErrorf("ffmpeg frame-at-time jpg: empty output") - } return b, nil } diff --git a/backend/recorder_settings.json b/backend/recorder_settings.json index c602d34..a6b46aa 100644 --- a/backend/recorder_settings.json +++ b/backend/recorder_settings.json @@ -22,6 +22,7 @@ "generateAssetsTeaser": true, "generateAssetsSprites": false, "generateAssetsAnalyze": false, + "enrichPostworkEnabled": false, "trainingRecognitionEnabled": true, "trainingDetectorEpochs": 60, "encryptedCookies": "FEPj/igIPaDy8xr709QjHKOqXWVGesegmwzhTP1Whnusetrl1WenN7t0V1Rm/lw2nLkyis78kz+4yjsDtVgO3MXLU4Uq85AhAJQOE7SMVD+YMNenCS8Qr0JLkmI6cr/kR6sKrx7Jm6x6DY1BolFFMX1Ii3EO+8b4Re2GYaFi1iuh97oI8Zg4r5sMYmt7FIMRkr1uy0u0ToH8hsO0iDoExcxubQNHIZTQ07kkWP70Up78tYs59INxzosijATbiVhOT1H0YwS23iWC3bwLgeo2lOzMejJWpEG16CN55CoEAymFd2ktlQZ4Lmv0FlnBJ48H27Cj0TKR1yRbBxpGjgch9x0421C5qYGeXNB9jgczEKh28oYdt8ljXejoDsV7/oKYNaAFfIlICsC/Zz97ZpPRrKiRTSTkTLmIXjnWtVdwGRfCvJrYjJpoe3ZOaubulqCQs6Ug501b6JTzdZujKAl68tL5stZD7hH9XGQSpquZWGeSUGOrI5jU8dULbxCLpkGtiOEmxGb93OLYdHQ8JJg6zHeExVtPSqANHoKX3NpFc33fqOX1vOQVeg8Ei4ymxntIMOwQWs8xnbuKXmnNx6GTBSqHiD7X/a8oeEkMSw1+bCODvwuXlNBcdxHlf3/wkvHb9lhnh231BD1+Ufrnij65Apko8A==" diff --git a/backend/settings.go b/backend/settings.go index daa7b63..870797b 100644 --- a/backend/settings.go +++ b/backend/settings.go @@ -44,6 +44,7 @@ type RecorderSettings struct { GenerateAssetsTeaser bool `json:"generateAssetsTeaser"` GenerateAssetsSprites bool `json:"generateAssetsSprites"` GenerateAssetsAnalyze bool `json:"generateAssetsAnalyze"` + EnrichPostworkEnabled bool `json:"enrichPostworkEnabled"` TrainingRecognitionEnabled bool `json:"trainingRecognitionEnabled"` TrainingDetectorEpochs int `json:"trainingDetectorEpochs"` @@ -84,6 +85,7 @@ var ( GenerateAssetsTeaser: true, GenerateAssetsSprites: false, GenerateAssetsAnalyze: false, + EnrichPostworkEnabled: true, TrainingRecognitionEnabled: true, TrainingDetectorEpochs: 60, @@ -256,6 +258,7 @@ type RecorderSettingsPublic struct { GenerateAssetsTeaser bool `json:"generateAssetsTeaser"` GenerateAssetsSprites bool `json:"generateAssetsSprites"` GenerateAssetsAnalyze bool `json:"generateAssetsAnalyze"` + EnrichPostworkEnabled bool `json:"enrichPostworkEnabled"` TrainingRecognitionEnabled bool `json:"trainingRecognitionEnabled"` TrainingDetectorEpochs int `json:"trainingDetectorEpochs"` @@ -293,6 +296,7 @@ func toPublicSettings(s RecorderSettings) RecorderSettingsPublic { GenerateAssetsTeaser: s.GenerateAssetsTeaser, GenerateAssetsSprites: s.GenerateAssetsSprites, GenerateAssetsAnalyze: s.GenerateAssetsAnalyze, + EnrichPostworkEnabled: s.EnrichPostworkEnabled, TrainingRecognitionEnabled: s.TrainingRecognitionEnabled, TrainingDetectorEpochs: s.TrainingDetectorEpochs, @@ -455,11 +459,14 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) { next.EnableNotifications = in.EnableNotifications + oldEnrichPostworkEnabled := current.EnrichPostworkEnabled + next.GenerateAssetsMeta = in.GenerateAssetsMeta next.GenerateAssetsThumb = in.GenerateAssetsThumb next.GenerateAssetsTeaser = in.GenerateAssetsTeaser next.GenerateAssetsSprites = in.GenerateAssetsSprites next.GenerateAssetsAnalyze = in.GenerateAssetsAnalyze + next.EnrichPostworkEnabled = in.EnrichPostworkEnabled next.TrainingRecognitionEnabled = in.TrainingRecognitionEnabled next.TrainingDetectorEpochs = in.TrainingDetectorEpochs @@ -492,6 +499,18 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) { saveSettingsToDisk() + if oldEnrichPostworkEnabled && !next.EnrichPostworkEnabled { + cancelledQueued, cancelledRunning := enrichQ.CancelAll() + + if cancelledQueued > 0 || cancelledRunning > 0 { + appLogln( + "🛑 enrich postwork deaktiviert:", + "queuedRemoved=", cancelledQueued, + "runningCancelled=", cancelledRunning, + ) + } + } + if newStore != nil { setModelStore(newStore) setChaturbateOnlineModelStore(newStore) diff --git a/backend/startup_leftovers.go b/backend/startup_leftovers.go index 35e72a7..d3c602e 100644 --- a/backend/startup_leftovers.go +++ b/backend/startup_leftovers.go @@ -11,24 +11,185 @@ import ( "github.com/google/uuid" ) -const startupLeftoverMinAge = 60 * time.Second +const ( + startupLeftoverMinAge = 60 * time.Second + + // .muxing.ts sind temporäre Zwischenstände. + // Nur alte Dateien löschen, damit ein sehr frischer/noch schreibender Prozess + // nicht versehentlich getroffen wird. + startupMuxingTempMinAge = 10 * time.Minute + + postworkLeftoverInitialDelay = 8 * time.Second + postworkLeftoverScanInterval = 1 * time.Hour +) func startPostworkLeftoverScanOnStartup() { go func() { // Kurz warten, bis Settings, Queues und UI/SSE bereit sind. - time.Sleep(8 * time.Second) + time.Sleep(postworkLeftoverInitialDelay) - queued, skipped := enqueuePostworkLeftoversFromRecordDir() - if queued > 0 || skipped > 0 { - appLogln( - "🧩 [startup-postwork] leftover scan fertig:", - "queued=", queued, - "skipped=", skipped, - ) + runPostworkLeftoverScan := func(reason string) { + removedMuxing, skippedMuxing := cleanupStaleMuxingTempFilesFromRecordDir() + + queued, skipped := enqueuePostworkLeftoversFromRecordDir() + if queued > 0 || skipped > 0 || removedMuxing > 0 || skippedMuxing > 0 { + appLogln( + "🧩 [startup-postwork] leftover scan fertig:", + "reason=", reason, + "queued=", queued, + "skipped=", skipped, + "removedMuxing=", removedMuxing, + "skippedMuxing=", skippedMuxing, + ) + } + } + + runPostworkLeftoverScan("startup") + + ticker := time.NewTicker(postworkLeftoverScanInterval) + defer ticker.Stop() + + for range ticker.C { + runPostworkLeftoverScan("interval") } }() } +func cleanupStaleMuxingTempFilesFromRecordDir() (removed int, skipped int) { + s := getSettings() + + recordAbs, err := resolvePathRelativeToApp(s.RecordDir) + if err != nil || strings.TrimSpace(recordAbs) == "" { + recordAbs = strings.TrimSpace(s.RecordDir) + } + + recordAbs = strings.TrimSpace(recordAbs) + if recordAbs == "" { + appLogln("⚠️ [startup-postwork] recordDir ist leer") + return 0, 0 + } + + entries, err := os.ReadDir(recordAbs) + if err != nil { + appLogln("⚠️ [startup-postwork] recordDir lesen fehlgeschlagen:", err) + return 0, 0 + } + + now := time.Now() + + for _, e := range entries { + if e == nil || e.IsDir() { + continue + } + + name := strings.TrimSpace(e.Name()) + if name == "" { + continue + } + + if !isMuxingTempTSName(name) { + continue + } + + path := filepath.Join(recordAbs, name) + + fi, err := os.Stat(path) + if err != nil || fi == nil || fi.IsDir() { + skipped++ + continue + } + + if fi.Size() <= 0 { + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + skipped++ + appLogln("⚠️ [startup-postwork] .muxing.ts leer, aber löschen fehlgeschlagen:", name, err) + continue + } + + removed++ + appLogln("🗑️ [startup-postwork] leere .muxing.ts gelöscht:", name) + continue + } + + age := now.Sub(fi.ModTime()) + if age < startupMuxingTempMinAge { + skipped++ + appLogln( + "⏭️ [startup-postwork] .muxing.ts zu frisch:", + name, + "age=", age.Round(time.Second), + "min=", startupMuxingTempMinAge, + ) + continue + } + + if !singleFileStable(path) { + skipped++ + appLogln("⏭️ [startup-postwork] .muxing.ts nicht stabil:", name) + continue + } + + // Falls für diesen temporären Namen ein Audio-Sidecar existiert, direkt mit entfernen. + removedAudioSidecars := removePostworkAudioSidecarsForOutput(path) + + if err := os.Remove(path); err != nil && !os.IsNotExist(err) { + skipped++ + appLogln("⚠️ [startup-postwork] .muxing.ts löschen fehlgeschlagen:", name, err) + continue + } + + removed++ + appLogln( + "🗑️ [startup-postwork] alte .muxing.ts gelöscht:", + name, + "audioSidecars=", len(removedAudioSidecars), + ) + } + + return removed, skipped +} + +func isMuxingTempTSName(name string) bool { + name = strings.TrimSpace(filepath.Base(name)) + if name == "" { + return false + } + + lowerName := strings.ToLower(name) + + return strings.HasSuffix(lowerName, ".muxing.ts") +} + +func singleFileStable(path string) bool { + path = strings.TrimSpace(path) + if path == "" { + return false + } + + sizeOf := func(p string) (int64, bool) { + fi, err := os.Stat(p) + if err != nil || fi == nil || fi.IsDir() { + return 0, false + } + + return fi.Size(), true + } + + size1, ok1 := sizeOf(path) + if !ok1 { + return false + } + + time.Sleep(1500 * time.Millisecond) + + size2, ok2 := sizeOf(path) + if !ok2 { + return false + } + + return size1 == size2 +} + func enqueuePostworkLeftoversFromRecordDir() (queued int, skipped int) { s := getSettings() diff --git a/backend/tasks_cleanup.go b/backend/tasks_cleanup.go index 0f375ad..b621e72 100644 --- a/backend/tasks_cleanup.go +++ b/backend/tasks_cleanup.go @@ -33,7 +33,12 @@ type cleanupResp struct { DeletedBytesHuman string `json:"deletedBytesHuman"` DeletedRecordOrphans int `json:"deletedRecordOrphans"` DeletedRecordOrphanBytes int64 `json:"deletedRecordOrphanBytes"` - ErrorCount int `json:"errorCount"` + + DeletedMuxingTemps int `json:"deletedMuxingTemps"` + DeletedAudioOrphans int `json:"deletedAudioOrphans"` + DeletedTSOrphans int `json:"deletedTSOrphans"` + + ErrorCount int `json:"errorCount"` // Generated-GC GeneratedOrphansChecked int `json:"generatedOrphansChecked"` @@ -246,7 +251,7 @@ func settingsCleanupHandler(w http.ResponseWriter, r *http.Request) { finishCleanupJob( jobID, - cleanupProgressText(resp.DeletedFiles, resp.DeletedBrokenFiles, resp.ScannedFiles, resp.DeletedBytes), + cleanupProgressText(resp), nil, ) }(jobID, ctx, doneAbs, recordAbs, mb) @@ -278,23 +283,42 @@ func settingsCleanupHandler(w http.ResponseWriter, r *http.Request) { } } -func cleanupProgressText(deleted int, broken int, total int, freedBytes int64) string { - if broken > 0 { - return fmt.Sprintf( - "gelöscht: %d/%d · fehlerhaft: %d · freigegeben: %s", - deleted, - total, - broken, - formatBytesSI(freedBytes), - ) +func cleanupProgressText(resp cleanupResp) string { + parts := []string{ + fmt.Sprintf("gelöscht: %d/%d", resp.DeletedFiles, resp.ScannedFiles), } - return fmt.Sprintf( - "gelöscht: %d/%d · freigegeben: %s", - deleted, - total, - formatBytesSI(freedBytes), - ) + if resp.DeletedBrokenFiles > 0 { + parts = append(parts, fmt.Sprintf("defekt: %d", resp.DeletedBrokenFiles)) + } + + if resp.DeletedRecordOrphans > 0 { + parts = append(parts, fmt.Sprintf("Records: %d", resp.DeletedRecordOrphans)) + } + + if resp.DeletedTSOrphans > 0 { + parts = append(parts, fmt.Sprintf("TS ohne Audio: %d", resp.DeletedTSOrphans)) + } + + if resp.DeletedAudioOrphans > 0 { + parts = append(parts, fmt.Sprintf("Audio ohne TS: %d", resp.DeletedAudioOrphans)) + } + + if resp.DeletedMuxingTemps > 0 { + parts = append(parts, fmt.Sprintf("Muxing-Temp: %d", resp.DeletedMuxingTemps)) + } + + if resp.GeneratedOrphansRemoved > 0 { + parts = append(parts, fmt.Sprintf("Generated: %d", resp.GeneratedOrphansRemoved)) + } + + if resp.ErrorCount > 0 { + parts = append(parts, fmt.Sprintf("Fehler: %d", resp.ErrorCount)) + } + + parts = append(parts, fmt.Sprintf("freigegeben: %s", formatBytesSI(resp.DeletedBytes))) + + return strings.Join(parts, " · ") } func cleanupVideoLooksBroken(ctx context.Context, path string) (bool, string) { @@ -444,10 +468,14 @@ func cleanupDeleteRecordOrphanFile(ctx context.Context, jobID string, path strin } fi, err := os.Stat(path) - if err != nil || fi == nil || fi.IsDir() || fi.Size() <= 0 { + if err != nil || fi == nil || fi.IsDir() { return } + size := fi.Size() + name := filepath.Base(path) + lowName := strings.ToLower(name) + if time.Since(fi.ModTime()) < cleanupRecordOrphanMinAge { resp.SkippedFiles++ return @@ -458,38 +486,49 @@ func cleanupDeleteRecordOrphanFile(ctx context.Context, jobID string, path strin return } - if !cleanupFileStable(path) { + // 0-KB-Dateien sind stabil genug zum Löschen. + // Für >0 Bytes weiter die Stabilitätsprüfung behalten. + if size > 0 && !cleanupFileStable(path) { resp.SkippedFiles++ return } updateCleanupJobState(jobID, func(st *CleanupTaskState) { - st.CurrentFile = filepath.Base(path) - st.Text = "Räume verwaiste Record-Dateien auf…" + st.CurrentFile = name + st.Text = "Räume Record-Reste auf…" }) if err := removeWithRetry(path); err != nil && !os.IsNotExist(err) { resp.ErrorCount++ - appLogln("⚠️ cleanup record orphan konnte nicht gelöscht werden:", filepath.Base(path), reason, err) + appLogln("⚠️ cleanup record orphan konnte nicht gelöscht werden:", name, reason, err) return } resp.DeletedFiles++ resp.DeletedRecordOrphans++ - resp.DeletedBytes += fi.Size() - resp.DeletedRecordOrphanBytes += fi.Size() + resp.DeletedBytes += size + resp.DeletedRecordOrphanBytes += size + + if strings.HasSuffix(lowName, ".muxing.ts") { + resp.DeletedMuxingTemps++ + } else if strings.HasPrefix(name, ".") && strings.HasSuffix(lowName, ".audio.m4s") { + resp.DeletedAudioOrphans++ + } else if !strings.HasPrefix(name, ".") && strings.EqualFold(filepath.Ext(name), ".ts") { + resp.DeletedTSOrphans++ + } purgeDurationCacheForPath(path) base := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)) base = strings.TrimPrefix(base, ".") base = strings.TrimSuffix(base, ".audio") + base = strings.TrimSuffix(base, ".muxing") if strings.TrimSpace(base) != "" { removeGeneratedForID(stripHotPrefix(base)) } - appLogln("🧹 cleanup deleted record orphan:", filepath.Base(path), "reason:", reason) + appLogln("🧹 cleanup deleted record orphan:", name, "reason:", reason, "size=", size) } func cleanupRecordDirOrphanAVFiles(ctx context.Context, jobID string, recordAbs string, resp *cleanupResp) error { @@ -522,6 +561,14 @@ func cleanupRecordDirOrphanAVFiles(ctx context.Context, jobID string, recordAbs full := filepath.Join(recordAbs, name) low := strings.ToLower(name) + // Fall 0: + // alte .muxing.ts sind temporäre Remux-/Muxing-Reste. + if strings.HasSuffix(low, ".muxing.ts") { + resp.ScannedFiles++ + cleanupDeleteRecordOrphanFile(ctx, jobID, full, resp, "stale muxing temp") + continue + } + // Fall 1: // foo.ts existiert, aber .foo.audio.m4s fehlt. if !strings.HasPrefix(name, ".") && strings.EqualFold(filepath.Ext(name), ".ts") { @@ -658,7 +705,7 @@ func cleanupSmallFiles(ctx context.Context, jobID string, doneAbs string, thresh st.Done = 0 st.Total = resp.ScannedFiles st.CurrentFile = "" - st.Text = cleanupProgressText(resp.DeletedFiles, resp.DeletedBrokenFiles, resp.ScannedFiles, resp.DeletedBytes) + st.Text = cleanupProgressText(*resp) st.Error = "" st.FinishedAt = nil }) @@ -716,7 +763,7 @@ func cleanupSmallFiles(ctx context.Context, jobID string, doneAbs string, thresh st.Done = i + 1 st.Total = resp.ScannedFiles st.CurrentFile = c.name - st.Text = cleanupProgressText(resp.DeletedFiles, resp.DeletedBrokenFiles, resp.ScannedFiles, resp.DeletedBytes) + st.Text = cleanupProgressText(*resp) st.Error = "" st.FinishedAt = nil }) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f4f58b9..843a9c2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -69,11 +69,23 @@ const DEFAULT_RECORDER_SETTINGS: RecorderSettingsState = { useMyFreeCamsWatcher: false, autoDeleteSmallDownloads: false, autoDeleteSmallDownloadsBelowMB: 200, + autoDeleteSmallDownloadsKeepFavorites: true, blurPreviews: false, teaserPlayback: 'hover', teaserAudio: false, lowDiskPauseBelowGB: 5, enableNotifications: true, + + generateAssetsMeta: true, + generateAssetsThumb: true, + generateAssetsTeaser: true, + generateAssetsSprites: true, + generateAssetsAnalyze: true, + + enrichPostworkEnabled: true, + + trainingRecognitionEnabled: true, + trainingDetectorEpochs: 60, } type StoredModel = { @@ -3812,14 +3824,30 @@ export default function App() { return } + const formatHeaderStatValue = (value: unknown) => { + const n = Number(value) + + if (!Number.isFinite(n)) return '0' + if (n >= 10000) return `${Math.round(n / 1000)}k` + if (n >= 1000) return `${Number((n / 1000).toFixed(1))}k` + + return String(Math.max(0, Math.floor(n))) + } + const headerShellClass = - 'rounded-2xl border border-gray-200/70 bg-white/65 p-3 shadow-sm backdrop-blur-xl dark:border-white/10 dark:bg-white/[0.06]' + 'rounded-2xl border border-gray-200/70 bg-white/65 p-3 shadow-sm backdrop-blur-xl dark:border-white/10 dark:bg-white/[0.06] max-lg:[@media_(orientation:landscape)]:rounded-xl max-lg:[@media_(orientation:landscape)]:p-2' const headerStatClass = - 'inline-flex h-8 items-center gap-1.5 rounded-xl border border-gray-200/70 bg-white/75 px-2.5 text-[11px] font-semibold text-gray-700 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/10 dark:text-gray-200' + 'inline-flex h-8 shrink-0 items-center justify-center gap-1.5 rounded-xl border border-gray-200/70 bg-white/75 px-2.5 text-[11px] font-semibold text-gray-700 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/10 dark:text-gray-200' + + const mobileHeaderStatClass = + 'flex h-8 min-w-0 items-center justify-center gap-1 rounded-lg border border-gray-200/70 bg-white/75 px-1.5 text-[10px] font-semibold text-gray-700 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/10 dark:text-gray-200' + + const headerStatValueClass = + 'inline-block min-w-[3ch] text-right tabular-nums text-gray-950 dark:text-white' const headerActionClass = - 'h-9 shrink-0 rounded-xl px-3 text-sm' + 'h-9 shrink-0 rounded-xl px-3 text-sm max-lg:[@media_(orientation:landscape)]:h-8 max-lg:[@media_(orientation:landscape)]:rounded-lg max-lg:[@media_(orientation:landscape)]:px-2.5 max-lg:[@media_(orientation:landscape)]:text-xs' const headerStatItems = [ { @@ -3858,60 +3886,79 @@ export default function App() {
-
+
-
-
+
+
-
-
-
-
+
+
+
-

- Recorder -

+

+ Recorder +

-
- {headerStatItems.map((item) => { - const Icon = item.icon +
+ {headerStatItems.map((item) => { + const Icon = item.icon - return ( - -
+ + ) + })}
+
-
- -
+
+ {headerStatItems.map((item) => { + const Icon = item.icon + + return ( + + + ) + })} +
+ +
+
-
+
{showPerfMon ? ( ) : null} -
+
-
-
+
+
diff --git a/frontend/src/components/ui/Downloads.tsx b/frontend/src/components/ui/Downloads.tsx index 0afc9a8..f8ea84c 100644 --- a/frontend/src/components/ui/Downloads.tsx +++ b/frontend/src/components/ui/Downloads.tsx @@ -640,7 +640,7 @@ export default function Downloads({ enableConcurrentDownloadsLimit = false, maxConcurrentDownloads = 20, }: Props) { - const isDesktop = useMediaQuery('(min-width: 640px)', true) + const isDesktop = useMediaQuery('(min-width: 1024px)', true) const [stopAllBusy, setStopAllBusy] = useState(false) diff --git a/frontend/src/components/ui/FinishedDownloads.tsx b/frontend/src/components/ui/FinishedDownloads.tsx index 989c575..1b73cf3 100644 --- a/frontend/src/components/ui/FinishedDownloads.tsx +++ b/frontend/src/components/ui/FinishedDownloads.tsx @@ -4725,7 +4725,7 @@ export default function FinishedDownloads({ <>
{/* Toolbar links neben dem Content, nicht per Transform aus dem Container geschoben */} -