updated
This commit is contained in:
parent
6337220de9
commit
f5deb65f75
@ -745,7 +745,13 @@ export default function FinishedDownloadsCardsView({
|
|||||||
{showMobileAudioButton ? (
|
{showMobileAudioButton ? (
|
||||||
<button
|
<button
|
||||||
type="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) => {
|
onClick={(e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, type SyntheticEvent
|
|||||||
import type { RecordJob } from '../../types'
|
import type { RecordJob } from '../../types'
|
||||||
import HoverPopover from './HoverPopover'
|
import HoverPopover from './HoverPopover'
|
||||||
import { DEFAULT_INLINE_MUTED, applyInlineVideoPolicy } from './videoPolicy'
|
import { DEFAULT_INLINE_MUTED, applyInlineVideoPolicy } from './videoPolicy'
|
||||||
|
import { ForwardIcon } from '@heroicons/react/24/solid'
|
||||||
|
|
||||||
type Variant = 'thumb' | 'fill'
|
type Variant = 'thumb' | 'fill'
|
||||||
type InlineVideoMode = false | true | 'always' | 'hover'
|
type InlineVideoMode = false | true | 'always' | 'hover'
|
||||||
@ -714,6 +715,21 @@ export default function FinishedVideoPreview({
|
|||||||
const shouldPreloadAnimatedAssets =
|
const shouldPreloadAnimatedAssets =
|
||||||
forceActive || effectiveNearView || effectiveInView || effectiveEverInView || (wantsHover && hovered)
|
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)
|
// ✅ 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)
|
// aber pro Datei/Wert nur 1x (verhindert doppelte Parent-Requests)
|
||||||
useEffect(() => {
|
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="pointer-events-none absolute inset-x-0 bottom-3 z-40 flex justify-center">
|
||||||
<div
|
<div
|
||||||
className="
|
className="
|
||||||
|
inline-flex items-center gap-1.5
|
||||||
rounded-full bg-black/70 px-3 py-1
|
rounded-full bg-black/70 px-3 py-1
|
||||||
text-[11px] font-semibold text-white
|
text-[11px] font-semibold text-white
|
||||||
shadow-lg ring-1 ring-white/10 backdrop-blur-sm
|
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>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import Pagination from './Pagination'
|
|||||||
import TagBadge from './TagBadge'
|
import TagBadge from './TagBadge'
|
||||||
import RecordJobActions from './RecordJobActions'
|
import RecordJobActions from './RecordJobActions'
|
||||||
import type { RecordJob } from '../../types'
|
import type { RecordJob } from '../../types'
|
||||||
|
import TextInput from './TextInput'
|
||||||
|
|
||||||
type ParsedModel = {
|
type ParsedModel = {
|
||||||
input: string
|
input: string
|
||||||
@ -979,11 +980,12 @@ export default function ModelsTab({ onAddToDownloads }: ModelsTabProps) {
|
|||||||
>
|
>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<div className="flex flex-col sm:flex-row gap-2">
|
<div className="flex flex-col sm:flex-row gap-2">
|
||||||
<input
|
<TextInput
|
||||||
value={input}
|
value={input}
|
||||||
|
size='sm'
|
||||||
onChange={(e) => setInput(e.target.value)}
|
onChange={(e) => setInput(e.target.value)}
|
||||||
placeholder="https://…"
|
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
|
<Button
|
||||||
className="px-3 py-2 text-sm"
|
className="px-3 py-2 text-sm"
|
||||||
@ -1115,17 +1117,11 @@ export default function ModelsTab({ onAddToDownloads }: ModelsTabProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Zeile 2: Suche volle Breite */}
|
{/* Zeile 2: Suche volle Breite */}
|
||||||
<input
|
<TextInput
|
||||||
value={q}
|
value={q}
|
||||||
|
size='sm'
|
||||||
onChange={(e) => setQ(e.target.value)}
|
onChange={(e) => setQ(e.target.value)}
|
||||||
placeholder="Suchen…"
|
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>
|
</div>
|
||||||
|
|
||||||
@ -1186,17 +1182,12 @@ export default function ModelsTab({ onAddToDownloads }: ModelsTabProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<TextInput
|
||||||
value={q}
|
value={q}
|
||||||
|
size='sm'
|
||||||
onChange={(e) => setQ(e.target.value)}
|
onChange={(e) => setQ(e.target.value)}
|
||||||
placeholder="Suchen…"
|
placeholder="Suchen…"
|
||||||
className="
|
className="sm:w-[260px]"
|
||||||
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
|
|
||||||
"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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