This commit is contained in:
Linrador 2026-03-31 14:03:43 +02:00
parent 6337220de9
commit f5deb65f75
4 changed files with 134 additions and 20 deletions

View File

@ -745,7 +745,13 @@ export default function FinishedDownloadsCardsView({
{showMobileAudioButton ? (
<button
type="button"
className="absolute top-2 right-2 z-[35] inline-flex items-center gap-1.5 rounded-full bg-black/70 px-2.5 py-1.5 text-[11px] font-medium text-white backdrop-blur hover:bg-black/80"
className="
absolute top-2 right-2 z-[35] inline-flex items-center gap-1.5 rounded-full
px-2.5 py-1.5 text-[11px] font-medium backdrop-blur transition
bg-white/95 text-gray-900 ring-1 ring-gray-200 shadow-sm hover:bg-white
dark:bg-black/70 dark:text-white dark:ring-white/10 dark:hover:bg-black/80
focus:outline-none focus:ring-2 focus:ring-indigo-500
"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()

View File

@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type SyntheticEvent
import type { RecordJob } from '../../types'
import HoverPopover from './HoverPopover'
import { DEFAULT_INLINE_MUTED, applyInlineVideoPolicy } from './videoPolicy'
import { ForwardIcon } from '@heroicons/react/24/solid'
type Variant = 'thumb' | 'fill'
type InlineVideoMode = false | true | 'always' | 'hover'
@ -714,6 +715,21 @@ export default function FinishedVideoPreview({
const shouldPreloadAnimatedAssets =
forceActive || effectiveNearView || effectiveInView || effectiveEverInView || (wantsHover && hovered)
useEffect(() => {
if (animatedMode !== 'teaser' || showingInlineVideo || !teaserActive) {
setTeaserPlaybackRate(1)
return
}
setTeaserPlaybackRate(teaserSpeedHold ? 2 : 1)
}, [
teaserSpeedHold,
teaserActive,
animatedMode,
showingInlineVideo,
setTeaserPlaybackRate,
])
// ✅ Wenn meta.json schon alles hat: sofort Callbacks auslösen (kein hidden <video> nötig)
// aber pro Datei/Wert nur 1x (verhindert doppelte Parent-Requests)
useEffect(() => {
@ -1401,12 +1417,14 @@ export default function FinishedVideoPreview({
<div className="pointer-events-none absolute inset-x-0 bottom-3 z-40 flex justify-center">
<div
className="
inline-flex items-center gap-1.5
rounded-full bg-black/70 px-3 py-1
text-[11px] font-semibold text-white
shadow-lg ring-1 ring-white/10 backdrop-blur-sm
"
>
2x speed &gt;&gt;
<span>2x speed</span>
<ForwardIcon className="size-3.5 shrink-0" />
</div>
</div>
) : null}

View File

@ -13,6 +13,7 @@ import Pagination from './Pagination'
import TagBadge from './TagBadge'
import RecordJobActions from './RecordJobActions'
import type { RecordJob } from '../../types'
import TextInput from './TextInput'
type ParsedModel = {
input: string
@ -979,11 +980,12 @@ export default function ModelsTab({ onAddToDownloads }: ModelsTabProps) {
>
<div className="grid gap-2">
<div className="flex flex-col sm:flex-row gap-2">
<input
<TextInput
value={input}
size='sm'
onChange={(e) => setInput(e.target.value)}
placeholder="https://…"
className="flex-1 rounded-md px-3 py-2 text-sm bg-white text-gray-900 dark:bg-white/10 dark:text-white"
className="flex-1"
/>
<Button
className="px-3 py-2 text-sm"
@ -1115,17 +1117,11 @@ export default function ModelsTab({ onAddToDownloads }: ModelsTabProps) {
</div>
{/* Zeile 2: Suche volle Breite */}
<input
<TextInput
value={q}
size='sm'
onChange={(e) => setQ(e.target.value)}
placeholder="Suchen…"
className="
w-full min-w-0
rounded-md px-3 py-2 text-sm
bg-white text-gray-900 shadow-sm ring-1 ring-gray-200
focus:outline-none focus:ring-2 focus:ring-indigo-500
dark:bg-white/10 dark:text-white dark:ring-white/10
"
/>
</div>
@ -1186,17 +1182,12 @@ export default function ModelsTab({ onAddToDownloads }: ModelsTabProps) {
/>
</div>
<input
<TextInput
value={q}
size='sm'
onChange={(e) => setQ(e.target.value)}
placeholder="Suchen…"
className="
w-full sm:w-[260px]
rounded-md px-3 py-2 text-sm
bg-white text-gray-900 shadow-sm ring-1 ring-gray-200
focus:outline-none focus:ring-2 focus:ring-indigo-500
dark:bg-white/10 dark:text-white dark:ring-white/10
"
className="sm:w-[260px]"
/>
</div>
</div>

View File

@ -0,0 +1,99 @@
// frontend\src\components\ui\TextInput.tsx
'use client'
import * as React from 'react'
type TextInputSize = 'sm' | 'md' | 'lg'
export type TextInputProps = Omit<
React.InputHTMLAttributes<HTMLInputElement>,
'size'
> & {
size?: TextInputSize
selectAllOnFocus?: boolean
selectAllOnMouseDown?: boolean
}
const baseClassName = `
block w-full min-w-0
bg-white text-gray-900 shadow-sm ring-1 ring-gray-200
focus:outline-none focus:ring-2 focus:ring-indigo-500
dark:bg-white/10 dark:text-white dark:ring-white/10 dark:[color-scheme:dark]
`
const sizeClassName: Record<TextInputSize, string> = {
sm: 'rounded-md px-3 py-2 text-sm',
md: 'rounded-lg px-3 py-2.5 text-sm',
lg: 'rounded-lg px-4 py-3 text-base',
}
function assignRef<T>(ref: React.Ref<T> | undefined, value: T) {
if (!ref) return
if (typeof ref === 'function') {
ref(value)
return
}
;(ref as React.MutableRefObject<T | null>).current = value
}
const TextInput = React.forwardRef<HTMLInputElement, TextInputProps>(
(
{
className = '',
type = 'text',
size = 'md',
selectAllOnFocus = false,
selectAllOnMouseDown = false,
onFocus,
onMouseDown,
...props
},
forwardedRef
) => {
const innerRef = React.useRef<HTMLInputElement | null>(null)
const setRefs = React.useCallback(
(node: HTMLInputElement | null) => {
innerRef.current = node
assignRef(forwardedRef, node)
},
[forwardedRef]
)
const selectAll = React.useCallback(() => {
const el = innerRef.current
if (!el) return
el.focus()
requestAnimationFrame(() => el.select())
}, [])
return (
<input
{...props}
ref={setRefs}
type={type}
className={[baseClassName, sizeClassName[size], className].join(' ').trim()}
onFocus={(e) => {
onFocus?.(e)
if (selectAllOnFocus) {
selectAll()
}
}}
onMouseDown={(e) => {
onMouseDown?.(e)
if (!selectAllOnMouseDown) return
if (e.button !== 0) return
e.preventDefault()
selectAll()
}}
/>
)
}
)
TextInput.displayName = 'TextInput'
export default TextInput