updated
This commit is contained in:
parent
6337220de9
commit
f5deb65f75
@ -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()
|
||||
|
||||
@ -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 >>
|
||||
<span>2x speed</span>
|
||||
<ForwardIcon className="size-3.5 shrink-0" />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
@ -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>
|
||||
|
||||
99
frontend/src/components/ui/TextInput.tsx
Normal file
99
frontend/src/components/ui/TextInput.tsx
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user