updated finisheddownloads
This commit is contained in:
parent
dffe5dde16
commit
6682b51f76
@ -695,7 +695,7 @@ func recordPreviewLiveFMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ffmpeg args: input = m3u8, output = fragmented mp4 to stdout
|
// ffmpeg args: input = m3u8, output = fragmented mp4 to stdout
|
||||||
// ✅ nur Video (kein Audio), damit MSE codecs stabil sind
|
// ✅ Video + Audio für Browser-Playback
|
||||||
args := []string{"-hide_banner", "-loglevel", "error"}
|
args := []string{"-hide_banner", "-loglevel", "error"}
|
||||||
if ua != "" {
|
if ua != "" {
|
||||||
args = append(args, "-user_agent", ua)
|
args = append(args, "-user_agent", ua)
|
||||||
@ -707,9 +707,10 @@ func recordPreviewLiveFMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Input
|
// Input
|
||||||
args = append(args, "-i", m3u8)
|
args = append(args, "-i", m3u8)
|
||||||
|
|
||||||
// Video encode (low-latency-ish)
|
// Video + Audio encode (low-latency-ish)
|
||||||
args = append(args,
|
args = append(args,
|
||||||
"-an",
|
"-map", "0:v:0",
|
||||||
|
"-map", "0:a:0?",
|
||||||
"-vf", "scale=480:-2",
|
"-vf", "scale=480:-2",
|
||||||
"-c:v", "libx264",
|
"-c:v", "libx264",
|
||||||
"-preset", "veryfast",
|
"-preset", "veryfast",
|
||||||
@ -720,6 +721,10 @@ func recordPreviewLiveFMP4(w http.ResponseWriter, r *http.Request) {
|
|||||||
"-g", "48",
|
"-g", "48",
|
||||||
"-keyint_min", "48",
|
"-keyint_min", "48",
|
||||||
"-sc_threshold", "0",
|
"-sc_threshold", "0",
|
||||||
|
"-c:a", "aac",
|
||||||
|
"-b:a", "128k",
|
||||||
|
"-ac", "2",
|
||||||
|
"-ar", "48000",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Output: fMP4 fragmented to stdout (single HTTP response)
|
// Output: fMP4 fragmented to stdout (single HTTP response)
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -60,22 +59,6 @@ func RecordStream(
|
|||||||
jobsMu.Unlock()
|
jobsMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
if job != nil && strings.TrimSpace(job.PreviewDir) == "" {
|
|
||||||
assetID := assetIDForJob(job)
|
|
||||||
if strings.TrimSpace(assetID) == "" {
|
|
||||||
assetID = job.ID
|
|
||||||
}
|
|
||||||
previewDir := filepath.Join(os.TempDir(), "rec_preview", assetID)
|
|
||||||
|
|
||||||
jobsMu.Lock()
|
|
||||||
job.PreviewDir = previewDir
|
|
||||||
jobsMu.Unlock()
|
|
||||||
|
|
||||||
if err := startPreviewHLS(ctx, job, playlist.PlaylistURL, previewDir, httpCookie, hc.userAgent); err != nil {
|
|
||||||
fmt.Println("⚠️ preview start fehlgeschlagen:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4) Datei öffnen
|
// 4) Datei öffnen
|
||||||
file, err := os.Create(outputPath)
|
file, err := os.Create(outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -89,22 +88,6 @@ func RecordStreamMFC(
|
|||||||
_ = publishJob(job.ID)
|
_ = publishJob(job.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Preview starten
|
|
||||||
if job != nil && job.PreviewDir == "" {
|
|
||||||
assetID := assetIDForJob(job)
|
|
||||||
if strings.TrimSpace(assetID) == "" {
|
|
||||||
assetID = job.ID
|
|
||||||
}
|
|
||||||
previewDir := filepath.Join(os.TempDir(), "rec_preview", assetID)
|
|
||||||
|
|
||||||
job.PreviewDir = previewDir
|
|
||||||
|
|
||||||
if err := startPreviewHLS(ctx, job, m3u8URL, previewDir, "", hc.userAgent); err != nil {
|
|
||||||
fmt.Println("⚠️ preview start fehlgeschlagen:", err)
|
|
||||||
job.PreviewDir = "" // rollback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aufnahme starten
|
// Aufnahme starten
|
||||||
return handleM3U8Mode(ctx, m3u8URL, outputPath, job)
|
return handleM3U8Mode(ctx, m3u8URL, outputPath, job)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
"lowDiskPauseBelowGB": 5,
|
"lowDiskPauseBelowGB": 5,
|
||||||
"blurPreviews": false,
|
"blurPreviews": false,
|
||||||
"teaserPlayback": "hover",
|
"teaserPlayback": "hover",
|
||||||
"teaserAudio": false,
|
"teaserAudio": true,
|
||||||
"enableNotifications": true,
|
"enableNotifications": true,
|
||||||
"encryptedCookies": ""
|
"encryptedCookies": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@ -171,10 +171,10 @@ func loadSettings() {
|
|||||||
|
|
||||||
// ffmpeg-Pfad anhand Settings/Env/PATH bestimmen
|
// ffmpeg-Pfad anhand Settings/Env/PATH bestimmen
|
||||||
ffmpegPath = detectFFmpegPath()
|
ffmpegPath = detectFFmpegPath()
|
||||||
//fmt.Println("🔍 ffmpegPath:", ffmpegPath)
|
fmt.Println("🔍 ffmpegPath:", ffmpegPath)
|
||||||
|
|
||||||
ffprobePath = detectFFprobePath()
|
ffprobePath = detectFFprobePath()
|
||||||
//fmt.Println("🔍 ffprobePath:", ffprobePath)
|
fmt.Println("🔍 ffprobePath:", ffprobePath)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -403,10 +403,10 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
ffmpegPath = detectFFmpegPath()
|
ffmpegPath = detectFFmpegPath()
|
||||||
}
|
}
|
||||||
fmt.Println("🔍 ffmpegPath:", ffmpegPath)
|
//fmt.Println("🔍 ffmpegPath:", ffmpegPath)
|
||||||
|
|
||||||
ffprobePath = detectFFprobePath()
|
ffprobePath = detectFFprobePath()
|
||||||
fmt.Println("🔍 ffprobePath:", ffprobePath)
|
//fmt.Println("🔍 ffprobePath:", ffprobePath)
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import { isHotName, stripHotPrefix } from './hotName'
|
|||||||
import LabeledSwitch from './LabeledSwitch'
|
import LabeledSwitch from './LabeledSwitch'
|
||||||
import Switch from './Switch'
|
import Switch from './Switch'
|
||||||
import LoadingSpinner from './LoadingSpinner'
|
import LoadingSpinner from './LoadingSpinner'
|
||||||
|
import type { FinishedDownloadsTeaserState, TeaserPlaybackMode } from './teaserPlayback'
|
||||||
|
|
||||||
type SortMode =
|
type SortMode =
|
||||||
| 'completed_desc'
|
| 'completed_desc'
|
||||||
@ -38,8 +39,6 @@ type SortMode =
|
|||||||
| 'size_desc'
|
| 'size_desc'
|
||||||
| 'size_asc'
|
| 'size_asc'
|
||||||
|
|
||||||
type TeaserPlaybackMode = 'still' | 'hover' | 'all'
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
jobs: RecordJob[]
|
jobs: RecordJob[]
|
||||||
doneJobs: RecordJob[]
|
doneJobs: RecordJob[]
|
||||||
@ -357,6 +356,16 @@ export default function FinishedDownloads({
|
|||||||
// ✅ schützt gegen alte Effect-Instanzen / StrictMode-Cleanup / Race Conditions
|
// ✅ schützt gegen alte Effect-Instanzen / StrictMode-Cleanup / Race Conditions
|
||||||
const refillSessionRef = React.useRef(0)
|
const refillSessionRef = React.useRef(0)
|
||||||
|
|
||||||
|
const teaserState = React.useMemo<FinishedDownloadsTeaserState>(
|
||||||
|
() => ({
|
||||||
|
mode: teaserPlaybackMode,
|
||||||
|
activeKey: teaserKey,
|
||||||
|
hoverKey: hoverTeaserKey,
|
||||||
|
audioEnabled: Boolean(teaserAudio),
|
||||||
|
}),
|
||||||
|
[teaserPlaybackMode, teaserKey, hoverTeaserKey, teaserAudio]
|
||||||
|
)
|
||||||
|
|
||||||
type UndoAction =
|
type UndoAction =
|
||||||
| { kind: 'delete'; undoToken: string; originalFile: string; rowKey?: string; from?: 'done' | 'keep' }
|
| { kind: 'delete'; undoToken: string; originalFile: string; rowKey?: string; from?: 'done' | 'keep' }
|
||||||
| { kind: 'keep'; keptFile: string; originalFile: string; rowKey?: string }
|
| { kind: 'keep'; keptFile: string; originalFile: string; rowKey?: string }
|
||||||
@ -1928,7 +1937,7 @@ export default function FinishedDownloads({
|
|||||||
// 🖱️ Desktop: Teaser nur bei Hover (Settings: 'hover' = Standard). Mobile: weiterhin Viewport-Fokus (Effect darunter)
|
// 🖱️ Desktop: Teaser nur bei Hover (Settings: 'hover' = Standard). Mobile: weiterhin Viewport-Fokus (Effect darunter)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const active =
|
const active =
|
||||||
teaserPlaybackMode === 'hover' &&
|
teaserState.mode === 'hover' &&
|
||||||
!canHover &&
|
!canHover &&
|
||||||
(view === 'cards' || view === 'gallery' || view === 'table')
|
(view === 'cards' || view === 'gallery' || view === 'table')
|
||||||
|
|
||||||
@ -1985,7 +1994,7 @@ export default function FinishedDownloads({
|
|||||||
io.disconnect()
|
io.disconnect()
|
||||||
if (teaserIORef.current === io) teaserIORef.current = null
|
if (teaserIORef.current === io) teaserIORef.current = null
|
||||||
}
|
}
|
||||||
}, [view, teaserPlaybackMode, canHover, inlinePlay?.key])
|
}, [view, teaserState.mode, canHover, inlinePlay?.key])
|
||||||
|
|
||||||
// 🧠 Laufzeit-Anzeige: bevorzugt Videodauer, sonst Fallback auf startedAt/endedAt
|
// 🧠 Laufzeit-Anzeige: bevorzugt Videodauer, sonst Fallback auf startedAt/endedAt
|
||||||
const runtimeOf = (job: RecordJob): string => {
|
const runtimeOf = (job: RecordJob): string => {
|
||||||
@ -2456,10 +2465,7 @@ export default function FinishedDownloads({
|
|||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
blurPreviews={blurPreviews}
|
blurPreviews={blurPreviews}
|
||||||
durations={durations}
|
durations={durations}
|
||||||
teaserKey={teaserKey}
|
teaserState={teaserState}
|
||||||
teaserPlayback={teaserPlaybackMode}
|
|
||||||
teaserAudio={teaserAudio}
|
|
||||||
hoverTeaserKey={hoverTeaserKey}
|
|
||||||
inlinePlay={inlinePlay}
|
inlinePlay={inlinePlay}
|
||||||
deletingKeys={deletingKeys}
|
deletingKeys={deletingKeys}
|
||||||
keepingKeys={keepingKeys}
|
keepingKeys={keepingKeys}
|
||||||
@ -2515,11 +2521,8 @@ export default function FinishedDownloads({
|
|||||||
resolutions={resolutions}
|
resolutions={resolutions}
|
||||||
durations={durations}
|
durations={durations}
|
||||||
canHover={canHover}
|
canHover={canHover}
|
||||||
teaserAudio={teaserAudio}
|
teaserState={teaserState}
|
||||||
hoverTeaserKey={hoverTeaserKey}
|
|
||||||
setHoverTeaserKey={setHoverTeaserKey}
|
setHoverTeaserKey={setHoverTeaserKey}
|
||||||
teaserPlayback={teaserPlaybackMode}
|
|
||||||
teaserKey={teaserKey}
|
|
||||||
registerTeaserHost={registerTeaserHost}
|
registerTeaserHost={registerTeaserHost}
|
||||||
handleDuration={handleDuration}
|
handleDuration={handleDuration}
|
||||||
handleResolution={handleResolution}
|
handleResolution={handleResolution}
|
||||||
@ -2556,10 +2559,7 @@ export default function FinishedDownloads({
|
|||||||
blurPreviews={blurPreviews}
|
blurPreviews={blurPreviews}
|
||||||
durations={durations}
|
durations={durations}
|
||||||
handleDuration={handleDuration}
|
handleDuration={handleDuration}
|
||||||
teaserKey={teaserKey}
|
teaserState={teaserState}
|
||||||
teaserPlayback={teaserPlaybackMode}
|
|
||||||
teaserAudio={teaserAudio}
|
|
||||||
hoverTeaserKey={hoverTeaserKey}
|
|
||||||
keyFor={keyFor}
|
keyFor={keyFor}
|
||||||
baseName={baseName}
|
baseName={baseName}
|
||||||
modelNameFromOutput={modelNameFromOutput}
|
modelNameFromOutput={modelNameFromOutput}
|
||||||
|
|||||||
@ -11,6 +11,11 @@ import TagOverflowRow from './TagOverflowRow'
|
|||||||
import PreviewScrubber from './PreviewScrubber'
|
import PreviewScrubber from './PreviewScrubber'
|
||||||
import { isHotName, stripHotPrefix } from './hotName'
|
import { isHotName, stripHotPrefix } from './hotName'
|
||||||
import { formatResolution } from './formatters'
|
import { formatResolution } from './formatters'
|
||||||
|
import type { FinishedDownloadsTeaserState } from './teaserPlayback'
|
||||||
|
import {
|
||||||
|
shouldAnimateTeaser,
|
||||||
|
shouldEnableTeaserAudio,
|
||||||
|
} from './teaserPlayback'
|
||||||
|
|
||||||
type InlinePlayState = { key: string; nonce: number } | null
|
type InlinePlayState = { key: string; nonce: number } | null
|
||||||
|
|
||||||
@ -18,14 +23,11 @@ type Props = {
|
|||||||
rows: RecordJob[]
|
rows: RecordJob[]
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
isSmall: boolean
|
isSmall: boolean
|
||||||
teaserPlayback: 'still' | 'hover' | 'all'
|
|
||||||
teaserAudio?: boolean
|
|
||||||
hoverTeaserKey?: string | null
|
|
||||||
|
|
||||||
blurPreviews?: boolean
|
blurPreviews?: boolean
|
||||||
durations: Record<string, number>
|
durations: Record<string, number>
|
||||||
teaserKey: string | null
|
|
||||||
inlinePlay: InlinePlayState
|
inlinePlay: InlinePlayState
|
||||||
|
teaserState: FinishedDownloadsTeaserState
|
||||||
|
|
||||||
deletingKeys: Set<string>
|
deletingKeys: Set<string>
|
||||||
keepingKeys: Set<string>
|
keepingKeys: Set<string>
|
||||||
@ -524,12 +526,9 @@ export default function FinishedDownloadsCardsView({
|
|||||||
rows,
|
rows,
|
||||||
isSmall,
|
isSmall,
|
||||||
isLoading,
|
isLoading,
|
||||||
teaserPlayback,
|
|
||||||
teaserAudio,
|
|
||||||
hoverTeaserKey,
|
|
||||||
blurPreviews,
|
blurPreviews,
|
||||||
durations,
|
durations,
|
||||||
teaserKey,
|
teaserState,
|
||||||
inlinePlay,
|
inlinePlay,
|
||||||
deletingKeys,
|
deletingKeys,
|
||||||
keepingKeys,
|
keepingKeys,
|
||||||
@ -704,10 +703,12 @@ export default function FinishedDownloadsCardsView({
|
|||||||
const realInlineActive = inlinePlay?.key === k
|
const realInlineActive = inlinePlay?.key === k
|
||||||
const inlineActive = opts?.disableInline ? false : realInlineActive
|
const inlineActive = opts?.disableInline ? false : realInlineActive
|
||||||
|
|
||||||
const allowSound =
|
const allowSound = shouldEnableTeaserAudio({
|
||||||
!opts?.forceStill &&
|
state: teaserState,
|
||||||
Boolean(teaserAudio) &&
|
itemKey: k,
|
||||||
(inlineActive || hoverTeaserKey === k)
|
inlineActive,
|
||||||
|
disabled: Boolean(opts?.forceStill),
|
||||||
|
})
|
||||||
|
|
||||||
const previewMuted = !allowSound
|
const previewMuted = !allowSound
|
||||||
const inlineNonce = inlineActive ? inlinePlay?.nonce ?? 0 : 0
|
const inlineNonce = inlineActive ? inlinePlay?.nonce ?? 0 : 0
|
||||||
@ -717,15 +718,13 @@ export default function FinishedDownloadsCardsView({
|
|||||||
// ✅ Im Mobile-Stack soll nur die Top-Card Teaser-Video bekommen.
|
// ✅ Im Mobile-Stack soll nur die Top-Card Teaser-Video bekommen.
|
||||||
// Untere Karten zeigen immer nur Bild (Still), selbst wenn teaserKey mal matcht.
|
// Untere Karten zeigen immer nur Bild (Still), selbst wenn teaserKey mal matcht.
|
||||||
const allowTeaserAnimation =
|
const allowTeaserAnimation =
|
||||||
opts?.forceStill
|
opts?.mobileStackTopOnlyVideo
|
||||||
? false
|
? teaserState.mode !== 'still'
|
||||||
: opts?.mobileStackTopOnlyVideo
|
: shouldAnimateTeaser({
|
||||||
? (teaserPlayback === 'still' ? false : true)
|
state: teaserState,
|
||||||
: (teaserPlayback === 'all'
|
itemKey: k,
|
||||||
? true
|
disabled: Boolean(opts?.forceStill),
|
||||||
: teaserPlayback === 'hover'
|
})
|
||||||
? teaserKey === k
|
|
||||||
: false)
|
|
||||||
|
|
||||||
const busy = deletingKeys.has(k) || keepingKeys.has(k) || removingKeys.has(k)
|
const busy = deletingKeys.has(k) || keepingKeys.has(k) || removingKeys.has(k)
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,12 @@ import TagOverflowRow from './TagOverflowRow'
|
|||||||
import { isHotName, stripHotPrefix } from './hotName'
|
import { isHotName, stripHotPrefix } from './hotName'
|
||||||
import { formatResolution } from './formatters'
|
import { formatResolution } from './formatters'
|
||||||
import PreviewScrubber from './PreviewScrubber'
|
import PreviewScrubber from './PreviewScrubber'
|
||||||
|
import type { FinishedDownloadsTeaserState } from './teaserPlayback'
|
||||||
|
import {
|
||||||
|
shouldAnimateTeaser,
|
||||||
|
shouldEnableTeaserAudio,
|
||||||
|
shouldObserveTeasers,
|
||||||
|
} from './teaserPlayback'
|
||||||
|
|
||||||
type ModelFlags = {
|
type ModelFlags = {
|
||||||
favorite?: boolean
|
favorite?: boolean
|
||||||
@ -35,10 +41,7 @@ type Props = {
|
|||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
blurPreviews?: boolean
|
blurPreviews?: boolean
|
||||||
durations: Record<string, number>
|
durations: Record<string, number>
|
||||||
teaserPlayback: 'still' | 'hover' | 'all'
|
teaserState: FinishedDownloadsTeaserState
|
||||||
teaserAudio?: boolean
|
|
||||||
hoverTeaserKey?: string | null
|
|
||||||
teaserKey: string | null
|
|
||||||
|
|
||||||
handleDuration: (job: RecordJob, seconds: number) => void
|
handleDuration: (job: RecordJob, seconds: number) => void
|
||||||
|
|
||||||
@ -150,10 +153,7 @@ export default function FinishedDownloadsGalleryView({
|
|||||||
isLoading,
|
isLoading,
|
||||||
blurPreviews,
|
blurPreviews,
|
||||||
durations,
|
durations,
|
||||||
teaserPlayback,
|
teaserState,
|
||||||
teaserAudio,
|
|
||||||
hoverTeaserKey,
|
|
||||||
teaserKey,
|
|
||||||
|
|
||||||
handleDuration,
|
handleDuration,
|
||||||
handleScrubberClickIndex,
|
handleScrubberClickIndex,
|
||||||
@ -189,18 +189,18 @@ export default function FinishedDownloadsGalleryView({
|
|||||||
enqueueToggleHot,
|
enqueueToggleHot,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
// ✅ Teaser-Observer nur aktiv, wenn Preview überhaupt "laufen" soll
|
// ✅ Teaser-Observer nur aktiv, wenn Preview überhaupt "laufen" soll
|
||||||
const shouldObserveTeasers = teaserPlayback === 'hover' || teaserPlayback === 'all'
|
const observeTeasers = shouldObserveTeasers(teaserState.mode)
|
||||||
|
|
||||||
// ✅ Wrapper: bei still unregistrieren / nicht registrieren
|
// ✅ Wrapper: bei still unregistrieren / nicht registrieren
|
||||||
const registerTeaserHostIfNeeded = React.useCallback(
|
const registerTeaserHostIfNeeded = React.useCallback(
|
||||||
(key: string) => (el: HTMLDivElement | null) => {
|
(key: string) => (el: HTMLDivElement | null) => {
|
||||||
if (!shouldObserveTeasers) {
|
if (!observeTeasers) {
|
||||||
registerTeaserHost(key)(null)
|
registerTeaserHost(key)(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
registerTeaserHost(key)(el)
|
registerTeaserHost(key)(el)
|
||||||
},
|
},
|
||||||
[registerTeaserHost, shouldObserveTeasers]
|
[registerTeaserHost, observeTeasers]
|
||||||
)
|
)
|
||||||
|
|
||||||
const parseTags = (raw?: string): string[] => {
|
const parseTags = (raw?: string): string[] => {
|
||||||
@ -276,7 +276,10 @@ export default function FinishedDownloadsGalleryView({
|
|||||||
const k = keyFor(j)
|
const k = keyFor(j)
|
||||||
|
|
||||||
// Sound nur bei Hover auf genau diesem Teaser
|
// Sound nur bei Hover auf genau diesem Teaser
|
||||||
const allowSound = Boolean(teaserAudio) && hoverTeaserKey === k
|
const allowSound = shouldEnableTeaserAudio({
|
||||||
|
state: teaserState,
|
||||||
|
itemKey: k,
|
||||||
|
})
|
||||||
const previewMuted = !allowSound
|
const previewMuted = !allowSound
|
||||||
|
|
||||||
const model = modelNameFromOutput(j.output)
|
const model = modelNameFromOutput(j.output)
|
||||||
@ -490,15 +493,11 @@ export default function FinishedDownloadsGalleryView({
|
|||||||
variant="fill"
|
variant="fill"
|
||||||
showPopover={false}
|
showPopover={false}
|
||||||
blur={blurPreviews}
|
blur={blurPreviews}
|
||||||
animated={
|
animated={shouldAnimateTeaser({
|
||||||
hideTeaserUnderOverlay
|
state: teaserState,
|
||||||
? false
|
itemKey: k,
|
||||||
: teaserPlayback === 'all'
|
disabled: hideTeaserUnderOverlay,
|
||||||
? true
|
})}
|
||||||
: teaserPlayback === 'hover'
|
|
||||||
? teaserKey === k
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
animatedMode="teaser"
|
animatedMode="teaser"
|
||||||
animatedTrigger="always"
|
animatedTrigger="always"
|
||||||
muted={previewMuted}
|
muted={previewMuted}
|
||||||
|
|||||||
@ -5,12 +5,16 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import Table, { type Column, type SortState } from './Table'
|
import Table, { type Column, type SortState } from './Table'
|
||||||
import type { RecordJob } from '../../types'
|
import type { RecordJob } from '../../types'
|
||||||
|
|
||||||
import FinishedVideoPreview from './FinishedVideoPreview'
|
import FinishedVideoPreview from './FinishedVideoPreview'
|
||||||
import RecordJobActions from './RecordJobActions'
|
import RecordJobActions from './RecordJobActions'
|
||||||
import TagOverflowRow from './TagOverflowRow'
|
import TagOverflowRow from './TagOverflowRow'
|
||||||
import { isHotName, stripHotPrefix } from './hotName'
|
import { isHotName, stripHotPrefix } from './hotName'
|
||||||
import { formatResolution } from './formatters'
|
import { formatResolution } from './formatters'
|
||||||
|
import type { FinishedDownloadsTeaserState } from './teaserPlayback'
|
||||||
|
import {
|
||||||
|
shouldAnimateTeaser,
|
||||||
|
shouldEnableTeaserAudio,
|
||||||
|
} from './teaserPlayback'
|
||||||
|
|
||||||
type SortMode =
|
type SortMode =
|
||||||
| 'completed_desc'
|
| 'completed_desc'
|
||||||
@ -39,11 +43,8 @@ type Props = {
|
|||||||
|
|
||||||
// teaser/preview
|
// teaser/preview
|
||||||
canHover: boolean
|
canHover: boolean
|
||||||
teaserAudio?: boolean
|
teaserState: FinishedDownloadsTeaserState
|
||||||
hoverTeaserKey: string | null
|
|
||||||
setHoverTeaserKey: React.Dispatch<React.SetStateAction<string | null>>
|
setHoverTeaserKey: React.Dispatch<React.SetStateAction<string | null>>
|
||||||
teaserPlayback?: 'still' | 'hover' | 'all'
|
|
||||||
teaserKey: string | null
|
|
||||||
registerTeaserHost: (key: string) => (el: HTMLDivElement | null) => void
|
registerTeaserHost: (key: string) => (el: HTMLDivElement | null) => void
|
||||||
handleDuration: (job: RecordJob, seconds: number) => void
|
handleDuration: (job: RecordJob, seconds: number) => void
|
||||||
handleResolution: (job: RecordJob, w: number, h: number) => void
|
handleResolution: (job: RecordJob, w: number, h: number) => void
|
||||||
@ -95,11 +96,8 @@ export default function FinishedDownloadsTableView({
|
|||||||
durations,
|
durations,
|
||||||
|
|
||||||
canHover,
|
canHover,
|
||||||
teaserAudio,
|
teaserState,
|
||||||
hoverTeaserKey,
|
|
||||||
setHoverTeaserKey,
|
setHoverTeaserKey,
|
||||||
teaserPlayback = 'hover',
|
|
||||||
teaserKey,
|
|
||||||
registerTeaserHost,
|
registerTeaserHost,
|
||||||
handleDuration,
|
handleDuration,
|
||||||
handleResolution,
|
handleResolution,
|
||||||
@ -225,7 +223,10 @@ export default function FinishedDownloadsTableView({
|
|||||||
widthClassName: 'w-[140px]',
|
widthClassName: 'w-[140px]',
|
||||||
cell: (j) => {
|
cell: (j) => {
|
||||||
const k = keyFor(j)
|
const k = keyFor(j)
|
||||||
const allowSound = Boolean(teaserAudio) && hoverTeaserKey === k
|
const allowSound = shouldEnableTeaserAudio({
|
||||||
|
state: teaserState,
|
||||||
|
itemKey: k,
|
||||||
|
})
|
||||||
const previewMuted = !allowSound
|
const previewMuted = !allowSound
|
||||||
const spriteInfo = previewSpriteInfoOf(j)
|
const spriteInfo = previewSpriteInfoOf(j)
|
||||||
const scrubActiveIndex = scrubActiveByKey[k]
|
const scrubActiveIndex = scrubActiveByKey[k]
|
||||||
@ -268,7 +269,10 @@ export default function FinishedDownloadsTableView({
|
|||||||
className="w-28 h-16 rounded-md ring-1 ring-black/5 dark:ring-white/10"
|
className="w-28 h-16 rounded-md ring-1 ring-black/5 dark:ring-white/10"
|
||||||
showPopover={false}
|
showPopover={false}
|
||||||
blur={blurPreviews}
|
blur={blurPreviews}
|
||||||
animated={teaserPlayback === 'all' ? true : teaserPlayback === 'hover' ? teaserKey === k : false}
|
animated={shouldAnimateTeaser({
|
||||||
|
state: teaserState,
|
||||||
|
itemKey: k,
|
||||||
|
})}
|
||||||
animatedMode="teaser"
|
animatedMode="teaser"
|
||||||
animatedTrigger="always"
|
animatedTrigger="always"
|
||||||
assetNonce={assetNonce}
|
assetNonce={assetNonce}
|
||||||
@ -474,16 +478,13 @@ export default function FinishedDownloadsTableView({
|
|||||||
keyFor,
|
keyFor,
|
||||||
baseName,
|
baseName,
|
||||||
durations,
|
durations,
|
||||||
teaserAudio,
|
|
||||||
hoverTeaserKey,
|
|
||||||
registerTeaserHost,
|
registerTeaserHost,
|
||||||
canHover,
|
canHover,
|
||||||
setHoverTeaserKey,
|
setHoverTeaserKey,
|
||||||
handleDuration,
|
handleDuration,
|
||||||
handleResolution,
|
handleResolution,
|
||||||
blurPreviews,
|
blurPreviews,
|
||||||
teaserPlayback,
|
teaserState,
|
||||||
teaserKey,
|
|
||||||
assetNonce,
|
assetNonce,
|
||||||
lower,
|
lower,
|
||||||
modelNameFromOutput,
|
modelNameFromOutput,
|
||||||
|
|||||||
69
frontend/src/components/ui/teaserPlayback.ts
Normal file
69
frontend/src/components/ui/teaserPlayback.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
// frontend\src\components\ui\teaserPlayback.ts
|
||||||
|
|
||||||
|
export type TeaserPlaybackMode = 'still' | 'hover' | 'all'
|
||||||
|
|
||||||
|
export type FinishedDownloadsTeaserState = {
|
||||||
|
mode: TeaserPlaybackMode
|
||||||
|
activeKey: string | null
|
||||||
|
hoverKey: string | null
|
||||||
|
audioEnabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type TeaserAnimationOptions = {
|
||||||
|
state: FinishedDownloadsTeaserState
|
||||||
|
itemKey: string
|
||||||
|
disabled?: boolean
|
||||||
|
forceAll?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
type TeaserAudioOptions = {
|
||||||
|
state: FinishedDownloadsTeaserState
|
||||||
|
itemKey: string
|
||||||
|
inlineActive?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldObserveTeasers(mode: TeaserPlaybackMode): boolean {
|
||||||
|
return mode === 'hover' || mode === 'all'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldAnimateTeaser({
|
||||||
|
state,
|
||||||
|
itemKey,
|
||||||
|
disabled = false,
|
||||||
|
forceAll = false,
|
||||||
|
}: TeaserAnimationOptions): boolean {
|
||||||
|
if (disabled) return false
|
||||||
|
if (forceAll) return true
|
||||||
|
|
||||||
|
switch (state.mode) {
|
||||||
|
case 'all':
|
||||||
|
return true
|
||||||
|
case 'hover':
|
||||||
|
return state.hoverKey === itemKey
|
||||||
|
case 'still':
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldEnableTeaserAudio({
|
||||||
|
state,
|
||||||
|
itemKey,
|
||||||
|
inlineActive = false,
|
||||||
|
disabled = false,
|
||||||
|
}: TeaserAudioOptions): boolean {
|
||||||
|
if (disabled) return false
|
||||||
|
if (!state.audioEnabled) return false
|
||||||
|
|
||||||
|
// Inline immer bevorzugen
|
||||||
|
if (inlineActive) return true
|
||||||
|
|
||||||
|
// Desktop-Hover
|
||||||
|
if (state.hoverKey === itemKey) return true
|
||||||
|
|
||||||
|
// Mobile / aktiver Teaser im Viewport
|
||||||
|
if (state.activeKey === itemKey) return true
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user