updated
This commit is contained in:
parent
0fac07f620
commit
b17a45e1d3
@ -452,7 +452,16 @@ func startChaturbateOnlinePoller(store *ModelStore) {
|
||||
continue
|
||||
}
|
||||
|
||||
_ = cbApplySnapshot(rooms)
|
||||
fetchedAtNow := cbApplySnapshot(rooms)
|
||||
|
||||
// ✅ Alle bekannten Chaturbate-Models in der DB mit aktuellem Online-Snapshot synchronisieren
|
||||
if cbModelStore != nil {
|
||||
go func(roomsCopy []ChaturbateRoom, fetchedAt time.Time) {
|
||||
if err := cbModelStore.SyncChaturbateOnlineForKnownModels(roomsCopy, fetchedAt); err != nil && verboseLogs() {
|
||||
fmt.Println("⚠️ [chaturbate] sync known models failed:", err)
|
||||
}
|
||||
}(append([]ChaturbateRoom(nil), rooms...), fetchedAtNow)
|
||||
}
|
||||
|
||||
// Tags übernehmen ist teuer -> nur selten + im Hintergrund
|
||||
if cbModelStore != nil && len(rooms) > 0 {
|
||||
@ -941,10 +950,14 @@ func chaturbateOnlineHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
_ = cbApplySnapshot(rooms)
|
||||
fetchedAtNow := cbApplySnapshot(rooms)
|
||||
|
||||
if cbModelStore != nil && len(rooms) > 0 {
|
||||
cbModelStore.FillMissingTagsFromChaturbateOnline(rooms)
|
||||
if cbModelStore != nil {
|
||||
_ = cbModelStore.SyncChaturbateOnlineForKnownModels(rooms, fetchedAtNow)
|
||||
|
||||
if len(rooms) > 0 {
|
||||
cbModelStore.FillMissingTagsFromChaturbateOnline(rooms)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@ -1036,6 +1036,176 @@ WHERE lower(trim(host)) = lower(trim($1));
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *ModelStore) SyncChaturbateOnlineForKnownModels(rooms []ChaturbateRoom, fetchedAt time.Time) error {
|
||||
if err := s.ensureInit(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
const host = "chaturbate.com"
|
||||
fetchedAt = fetchedAt.UTC()
|
||||
|
||||
knownKeys, err := s.ListModelKeysByHost(host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(knownKeys) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
roomsByUser := indexRoomsByUser(rooms)
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = tx.Rollback() }()
|
||||
|
||||
// q1 mit cb_online_last_error
|
||||
stmt1, err1 := tx.Prepare(`
|
||||
UPDATE models
|
||||
SET
|
||||
last_seen_online = $1,
|
||||
last_seen_online_at = $2,
|
||||
cb_online_json = $3,
|
||||
cb_online_fetched_at = $4,
|
||||
cb_online_last_error = $5,
|
||||
profile_image_url = CASE
|
||||
WHEN COALESCE(trim($6), '') <> '' THEN $6
|
||||
ELSE profile_image_url
|
||||
END,
|
||||
profile_image_updated_at = CASE
|
||||
WHEN COALESCE(trim($6), '') <> '' THEN $4
|
||||
ELSE profile_image_updated_at
|
||||
END,
|
||||
updated_at = $7
|
||||
WHERE lower(trim(host)) = lower(trim($8))
|
||||
AND lower(trim(model_key)) = lower(trim($9));
|
||||
`)
|
||||
|
||||
// fallback ohne cb_online_last_error
|
||||
var stmt2 *sql.Stmt
|
||||
if err1 != nil {
|
||||
stmt2, err = tx.Prepare(`
|
||||
UPDATE models
|
||||
SET
|
||||
last_seen_online = $1,
|
||||
last_seen_online_at = $2,
|
||||
cb_online_json = $3,
|
||||
cb_online_fetched_at = $4,
|
||||
profile_image_url = CASE
|
||||
WHEN COALESCE(trim($5), '') <> '' THEN $5
|
||||
ELSE profile_image_url
|
||||
END,
|
||||
profile_image_updated_at = CASE
|
||||
WHEN COALESCE(trim($5), '') <> '' THEN $4
|
||||
ELSE profile_image_updated_at
|
||||
END,
|
||||
updated_at = $6
|
||||
WHERE lower(trim(host)) = lower(trim($7))
|
||||
AND lower(trim(model_key)) = lower(trim($8));
|
||||
`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt2.Close()
|
||||
} else {
|
||||
defer stmt1.Close()
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
fetchedAtStr := fetchedAt.Format(time.RFC3339Nano)
|
||||
|
||||
for _, key := range knownKeys {
|
||||
key = strings.TrimSpace(key)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
online bool
|
||||
snap *ChaturbateOnlineSnapshot
|
||||
jsonStr string
|
||||
imgURL string
|
||||
)
|
||||
|
||||
if rm, ok := roomsByUser[strings.ToLower(key)]; ok {
|
||||
online = true
|
||||
imgURL = strings.TrimSpace(selectBestRoomImageURL(rm))
|
||||
|
||||
snap = &ChaturbateOnlineSnapshot{
|
||||
Username: rm.Username,
|
||||
DisplayName: rm.DisplayName,
|
||||
CurrentShow: strings.TrimSpace(rm.CurrentShow),
|
||||
RoomSubject: rm.RoomSubject,
|
||||
Location: rm.Location,
|
||||
Country: rm.Country,
|
||||
SpokenLanguages: rm.SpokenLanguages,
|
||||
Gender: rm.Gender,
|
||||
NumUsers: rm.NumUsers,
|
||||
NumFollowers: rm.NumFollowers,
|
||||
IsHD: rm.IsHD,
|
||||
IsNew: rm.IsNew,
|
||||
Age: rm.Age,
|
||||
SecondsOnline: rm.SecondsOnline,
|
||||
ImageURL: rm.ImageURL,
|
||||
ImageURL360: rm.ImageURL360,
|
||||
ChatRoomURL: rm.ChatRoomURL,
|
||||
ChatRoomURLRS: rm.ChatRoomURLRS,
|
||||
Tags: rm.Tags,
|
||||
}
|
||||
} else {
|
||||
online = false
|
||||
|
||||
snap = &ChaturbateOnlineSnapshot{
|
||||
Username: key,
|
||||
CurrentShow: "offline",
|
||||
}
|
||||
}
|
||||
|
||||
if snap != nil {
|
||||
if b, err := json.Marshal(snap); err == nil {
|
||||
jsonStr = strings.TrimSpace(string(b))
|
||||
}
|
||||
}
|
||||
|
||||
if stmt1 != nil {
|
||||
if _, err := stmt1.Exec(
|
||||
online,
|
||||
fetchedAt,
|
||||
nullableStringArg(jsonStr),
|
||||
fetchedAt,
|
||||
"",
|
||||
imgURL,
|
||||
now,
|
||||
host,
|
||||
key,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if _, err := stmt2.Exec(
|
||||
online,
|
||||
fetchedAt,
|
||||
nullableStringArg(jsonStr),
|
||||
fetchedAt,
|
||||
imgURL,
|
||||
now,
|
||||
host,
|
||||
key,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
_ = fetchedAtStr // falls du später Logging willst
|
||||
}
|
||||
|
||||
return tx.Commit()
|
||||
}
|
||||
|
||||
func (s *ModelStore) SetChaturbateOnlineSnapshot(host, modelKey string, snap *ChaturbateOnlineSnapshot, fetchedAt string, lastErr string) error {
|
||||
if err := s.ensureInit(); err != nil {
|
||||
return err
|
||||
|
||||
Binary file not shown.
1
backend/web/dist/assets/index-3IFBscEU.css
vendored
1
backend/web/dist/assets/index-3IFBscEU.css
vendored
File diff suppressed because one or more lines are too long
465
backend/web/dist/assets/index-C4whm-WW.js
vendored
465
backend/web/dist/assets/index-C4whm-WW.js
vendored
File diff suppressed because one or more lines are too long
1
backend/web/dist/assets/index-D0pbgV48.css
vendored
Normal file
1
backend/web/dist/assets/index-D0pbgV48.css
vendored
Normal file
File diff suppressed because one or more lines are too long
449
backend/web/dist/assets/index-jlVIND2Y.js
vendored
Normal file
449
backend/web/dist/assets/index-jlVIND2Y.js
vendored
Normal file
File diff suppressed because one or more lines are too long
4
backend/web/dist/index.html
vendored
4
backend/web/dist/index.html
vendored
@ -5,8 +5,8 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<title>App</title>
|
||||
<script type="module" crossorigin src="/assets/index-C4whm-WW.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-3IFBscEU.css">
|
||||
<script type="module" crossorigin src="/assets/index-jlVIND2Y.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-D0pbgV48.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@ -107,12 +107,6 @@ type ChaturbateOnlineRoom = {
|
||||
image_url?: string
|
||||
}
|
||||
|
||||
type ChaturbateOnlineResponse = {
|
||||
enabled: boolean
|
||||
rooms: ChaturbateOnlineRoom[]
|
||||
total?: number
|
||||
}
|
||||
|
||||
function normalizeHttpUrl(raw: string): string | null {
|
||||
let v = (raw ?? '').trim()
|
||||
if (!v) return null
|
||||
@ -943,17 +937,6 @@ export default function App() {
|
||||
const [cbOnlineByKeyLower, setCbOnlineByKeyLower] = useState<Record<string, ChaturbateOnlineRoom>>({})
|
||||
const cbOnlineByKeyLowerRef = useRef<Record<string, ChaturbateOnlineRoom>>({})
|
||||
|
||||
const lastCbShowByKeyLowerRef = useRef<Record<string, string>>({})
|
||||
|
||||
// ✅ merkt sich, ob ein Model im letzten Snapshot überhaupt online war
|
||||
const lastCbOnlineByKeyLowerRef = useRef<Record<string, true>>({})
|
||||
|
||||
// ✅ verhindert Toast-Spam direkt beim ersten Poll (Startup)
|
||||
const cbOnlineInitDoneRef = useRef(false)
|
||||
|
||||
// ✅ merkt sich, ob ein Model seit App-Start schon einmal online war
|
||||
const everCbOnlineByKeyLowerRef = useRef<Record<string, true>>({})
|
||||
|
||||
useEffect(() => {
|
||||
cbOnlineByKeyLowerRef.current = cbOnlineByKeyLower
|
||||
}, [cbOnlineByKeyLower])
|
||||
|
||||
@ -76,6 +76,60 @@ const pendingRowKey = (p: PendingWatchedRoom) => {
|
||||
return String(anyP.key ?? anyP.id ?? pendingModelName(p))
|
||||
}
|
||||
|
||||
const normalizeRoomStatus = (v: unknown): string => {
|
||||
const s = String(v ?? '').trim().toLowerCase()
|
||||
|
||||
switch (s) {
|
||||
case 'public':
|
||||
return 'Public'
|
||||
case 'private':
|
||||
return 'Private'
|
||||
case 'hidden':
|
||||
return 'Hidden'
|
||||
case 'away':
|
||||
return 'Away'
|
||||
case 'offline':
|
||||
return 'Offline'
|
||||
default:
|
||||
return 'Offline'
|
||||
}
|
||||
}
|
||||
|
||||
const roomStatusOfJob = (job: RecordJob): string => {
|
||||
const j = job as any
|
||||
|
||||
const raw =
|
||||
j.currentShow ??
|
||||
j.roomStatus ??
|
||||
j.modelStatus ??
|
||||
j.show ??
|
||||
j.statusText ??
|
||||
j.model?.currentShow ??
|
||||
j.model?.roomStatus ??
|
||||
j.model?.status ??
|
||||
j.room?.currentShow ??
|
||||
j.room?.status ??
|
||||
''
|
||||
|
||||
return normalizeRoomStatus(raw)
|
||||
}
|
||||
|
||||
const roomStatusTone = (status: string): string => {
|
||||
switch (status) {
|
||||
case 'Public':
|
||||
return 'bg-emerald-500/15 text-emerald-900 ring-emerald-500/30 dark:bg-emerald-400/10 dark:text-emerald-200 dark:ring-emerald-400/25'
|
||||
case 'Private':
|
||||
return 'bg-fuchsia-500/15 text-fuchsia-900 ring-fuchsia-500/30 dark:bg-fuchsia-400/10 dark:text-fuchsia-200 dark:ring-fuchsia-400/25'
|
||||
case 'Hidden':
|
||||
return 'bg-slate-500/15 text-slate-900 ring-slate-500/30 dark:bg-slate-400/10 dark:text-slate-200 dark:ring-slate-400/25'
|
||||
case 'Away':
|
||||
return 'bg-amber-500/15 text-amber-900 ring-amber-500/30 dark:bg-amber-400/10 dark:text-amber-200 dark:ring-amber-400/25'
|
||||
case 'Offline':
|
||||
default:
|
||||
return 'bg-gray-900/5 text-gray-800 ring-gray-900/10 dark:bg-white/10 dark:text-gray-200 dark:ring-white/10'
|
||||
}
|
||||
}
|
||||
|
||||
const toMs = (v: unknown): number => {
|
||||
if (typeof v === 'number' && Number.isFinite(v)) {
|
||||
// Heuristik: 10-stellige Unix-Sekunden -> ms
|
||||
@ -317,7 +371,7 @@ function DownloadsCardRow({
|
||||
const p = r.pending
|
||||
const name = pendingModelName(p)
|
||||
const url = pendingUrl(p)
|
||||
const show = (p.currentShow || 'unknown').toLowerCase()
|
||||
const show = normalizeRoomStatus(p.currentShow)
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -410,12 +464,14 @@ function DownloadsCardRow({
|
||||
const phaseLower = phase.toLowerCase()
|
||||
const isRecording = phaseLower === 'recording'
|
||||
|
||||
const isStopRequested = Boolean(stopRequestedIds[j.id]) // nur UI-zwischenzustand
|
||||
const isStopRequested = Boolean(stopRequestedIds[j.id]) // nur für Stop-Button/UI
|
||||
const rawStatus = String(j.status ?? '').toLowerCase()
|
||||
|
||||
const isBusyPhase = phaseLower !== '' && phaseLower !== 'recording'
|
||||
const isStopping = isBusyPhase || rawStatus !== 'running' || isStopRequested
|
||||
|
||||
const roomStatus = roomStatusOfJob(j)
|
||||
|
||||
let phaseText = phase ? (phaseLabel(phase) || phase) : ''
|
||||
|
||||
if (phaseLower === 'recording') {
|
||||
@ -424,8 +480,7 @@ function DownloadsCardRow({
|
||||
phaseText = postWorkLabel(j, postworkInfoOf(j))
|
||||
}
|
||||
|
||||
const statusText = rawStatus || 'unknown'
|
||||
const progressLabel = phaseText || statusText
|
||||
const progressLabel = phaseText || roomStatus
|
||||
|
||||
const progress = Number((j as any).progress ?? 0)
|
||||
const showBar =
|
||||
@ -504,13 +559,12 @@ function DownloadsCardRow({
|
||||
{ /* Status-Badge */}
|
||||
<span
|
||||
className={[
|
||||
'shrink-0 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold',
|
||||
'bg-gray-900/5 text-gray-800 dark:bg-white/10 dark:text-gray-200',
|
||||
isStopping ? 'ring-1 ring-amber-500/30' : 'ring-1 ring-emerald-500/25',
|
||||
'shrink-0 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold ring-1',
|
||||
roomStatusTone(roomStatus),
|
||||
].join(' ')}
|
||||
title={statusText}
|
||||
title={roomStatus}
|
||||
>
|
||||
{statusText}
|
||||
{roomStatus}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-0.5 truncate text-xs text-gray-600 dark:text-gray-300" title={j.output}>
|
||||
@ -1052,21 +1106,7 @@ export default function Downloads({
|
||||
const f = baseName(j.output || '')
|
||||
const name = modelNameFromOutput(j.output)
|
||||
|
||||
const rawStatus = String(j.status ?? '').toLowerCase()
|
||||
|
||||
// Final "stopped" sauber erkennen (inkl. UI-Stop)
|
||||
const isStopRequested = Boolean(stopRequestedIds[j.id])
|
||||
const stopInitiated = Boolean(stopInitiatedIds[j.id])
|
||||
const isStoppedFinal = rawStatus === 'stopped' || (stopInitiated && Boolean(j.endedAt))
|
||||
|
||||
// ✅ Status-Text neben dem Modelname: NUR Job-Status
|
||||
// (keine phase wie assets/moving/remuxing)
|
||||
const statusText = isStoppedFinal ? 'stopped' : (rawStatus || 'unknown')
|
||||
|
||||
// Optional: "Stoppe…" rein UI-seitig anzeigen, aber ohne phase
|
||||
const showStoppingUI = !isStoppedFinal && isStopRequested
|
||||
const badgeText = showStoppingUI ? 'stopping' : statusText
|
||||
|
||||
const roomStatus = roomStatusOfJob(j)
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -1077,21 +1117,11 @@ export default function Downloads({
|
||||
<span
|
||||
className={[
|
||||
'shrink-0 inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-semibold ring-1',
|
||||
rawStatus === 'running'
|
||||
? 'bg-emerald-500/15 text-emerald-900 ring-emerald-500/30 dark:bg-emerald-400/10 dark:text-emerald-200 dark:ring-emerald-400/25'
|
||||
: isStoppedFinal
|
||||
? 'bg-slate-500/15 text-slate-900 ring-slate-500/30 dark:bg-slate-400/10 dark:text-slate-200 dark:ring-slate-400/25'
|
||||
: rawStatus === 'failed'
|
||||
? 'bg-red-500/15 text-red-900 ring-red-500/30 dark:bg-red-400/10 dark:text-red-200 dark:ring-red-400/25'
|
||||
: rawStatus === 'finished'
|
||||
? 'bg-emerald-500/15 text-emerald-900 ring-emerald-500/30 dark:bg-emerald-400/10 dark:text-emerald-200 dark:ring-emerald-400/25'
|
||||
: showStoppingUI
|
||||
? 'bg-amber-500/15 text-amber-900 ring-amber-500/30 dark:bg-amber-400/10 dark:text-amber-200 dark:ring-amber-400/25'
|
||||
: 'bg-gray-900/5 text-gray-800 ring-gray-900/10 dark:bg-white/10 dark:text-gray-200 dark:ring-white/10',
|
||||
roomStatusTone(roomStatus),
|
||||
].join(' ')}
|
||||
title={badgeText}
|
||||
title={roomStatus}
|
||||
>
|
||||
{badgeText}
|
||||
{roomStatus}
|
||||
</span>
|
||||
</div>
|
||||
<span className="block max-w-[220px] truncate" title={j.output}>
|
||||
@ -1157,7 +1187,7 @@ export default function Downloads({
|
||||
}
|
||||
|
||||
const p = r.pending
|
||||
const show = (p.currentShow || 'unknown').toLowerCase()
|
||||
const show = normalizeRoomStatus(p.currentShow)
|
||||
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
|
||||
@ -13,6 +13,7 @@ type ModalProps = {
|
||||
open: boolean
|
||||
onClose: () => void
|
||||
title?: string
|
||||
titleRight?: ReactNode
|
||||
children?: ReactNode
|
||||
footer?: ReactNode
|
||||
icon?: ReactNode
|
||||
@ -81,6 +82,7 @@ export default function Modal({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
titleRight,
|
||||
children,
|
||||
footer,
|
||||
icon,
|
||||
@ -212,7 +214,7 @@ export default function Modal({
|
||||
layout === 'split' ? 'hidden lg:flex' : 'flex'
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
{title ? (
|
||||
<Dialog.Title className="hidden sm:block text-base font-semibold text-gray-900 dark:text-white truncate">
|
||||
{title}
|
||||
@ -220,20 +222,24 @@ export default function Modal({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'inline-flex shrink-0 items-center justify-center rounded-lg p-1.5',
|
||||
'text-gray-500 hover:text-gray-900 hover:bg-black/5',
|
||||
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600',
|
||||
'dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 dark:focus-visible:outline-indigo-500'
|
||||
)}
|
||||
aria-label="Schließen"
|
||||
title="Schließen"
|
||||
>
|
||||
<XMarkIcon className="size-5" />
|
||||
</button>
|
||||
<div className="shrink-0 flex items-center gap-2">
|
||||
{titleRight ? <div className="hidden sm:block">{titleRight}</div> : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'inline-flex shrink-0 items-center justify-center rounded-lg p-1.5',
|
||||
'text-gray-500 hover:text-gray-900 hover:bg-black/5',
|
||||
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600',
|
||||
'dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 dark:focus-visible:outline-indigo-500'
|
||||
)}
|
||||
aria-label="Schließen"
|
||||
title="Schließen"
|
||||
>
|
||||
<XMarkIcon className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
@ -286,7 +292,7 @@ export default function Modal({
|
||||
mobileCollapsed ? 'py-2' : 'py-3'
|
||||
)}
|
||||
>
|
||||
<div className="min-w-0 flex items-center gap-2">
|
||||
<div className="min-w-0 flex items-center gap-2 flex-1">
|
||||
{mobileCollapsedImageSrc ? (
|
||||
<img
|
||||
src={mobileCollapsedImageSrc}
|
||||
@ -300,7 +306,7 @@ export default function Modal({
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="min-w-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
{title ? (
|
||||
<div
|
||||
className={cn(
|
||||
@ -314,20 +320,24 @@ export default function Modal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'inline-flex shrink-0 items-center justify-center rounded-lg p-1.5',
|
||||
'text-gray-500 hover:text-gray-900 hover:bg-black/5',
|
||||
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600',
|
||||
'dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 dark:focus-visible:outline-indigo-500'
|
||||
)}
|
||||
aria-label="Schließen"
|
||||
title="Schließen"
|
||||
>
|
||||
<XMarkIcon className="size-5" />
|
||||
</button>
|
||||
<div className="shrink-0 flex items-center gap-2">
|
||||
{titleRight ? <div>{titleRight}</div> : null}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
'inline-flex shrink-0 items-center justify-center rounded-lg p-1.5',
|
||||
'text-gray-500 hover:text-gray-900 hover:bg-black/5',
|
||||
'focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600',
|
||||
'dark:text-gray-400 dark:hover:text-white dark:hover:bg-white/10 dark:focus-visible:outline-indigo-500'
|
||||
)}
|
||||
aria-label="Schließen"
|
||||
title="Schließen"
|
||||
>
|
||||
<XMarkIcon className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sticky tabs/actions (always sticky because in this sticky wrapper) */}
|
||||
|
||||
@ -170,7 +170,7 @@ export default function Pagination({
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex items-center justify-between bg-white dark:border-white/10 dark:bg-transparent',
|
||||
'flex items-center justify-between bg-transparent dark:border-white/10',
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@ -417,7 +417,7 @@ export default function Player({
|
||||
|
||||
// ✅ Live-Stream URL (Playback) -> play=1 hält Preview sicher am Leben
|
||||
const liveHlsSrc = React.useMemo(
|
||||
() => apiUrl(`/api/preview?id=${encodeURIComponent(previewId)}&play=1&file=index_hq.m3u8`),
|
||||
() => apiUrl(`/api/preview/live?id=${encodeURIComponent(previewId)}&play=1`),
|
||||
[previewId]
|
||||
)
|
||||
|
||||
@ -1213,7 +1213,7 @@ export default function Player({
|
||||
if (typeof window === 'undefined') return r
|
||||
|
||||
const ratio = getVideoAspectRatio()
|
||||
const BAR_H = 30 // gewünschter fixer Platz unter dem Video für die Controlbar
|
||||
const BAR_H = isRunning ? 0 : 30 // gewünschter fixer Platz unter dem Video für die Controlbar
|
||||
|
||||
const { w: vw, h: vh } = getViewport()
|
||||
const maxW = vw - MARGIN * 2
|
||||
@ -1248,7 +1248,7 @@ export default function Player({
|
||||
|
||||
return { x, y, w: Math.round(w), h }
|
||||
},
|
||||
[getVideoAspectRatio]
|
||||
[getVideoAspectRatio, isRunning]
|
||||
)
|
||||
|
||||
const loadRect = React.useCallback(() => {
|
||||
|
||||
@ -10,7 +10,6 @@ import TaskList from './TaskList'
|
||||
import type { TaskItem } from './TaskList'
|
||||
import PostgresUrlModal from './PostgresUrlModal'
|
||||
import { CheckIcon, XMarkIcon } from '@heroicons/react/24/solid'
|
||||
import { ArrowDownTrayIcon } from '@heroicons/react/24/outline'
|
||||
|
||||
type RecorderSettings = {
|
||||
databaseUrl?: string
|
||||
|
||||
@ -422,12 +422,14 @@ const SwipeCard = React.forwardRef<SwipeCardHandle, SwipeCardProps>(function Swi
|
||||
transition: animMs ? `transform ${animMs}ms ease` : undefined,
|
||||
touchAction: 'pan-y',
|
||||
willChange: dx !== 0 ? 'transform' : undefined,
|
||||
/*
|
||||
boxShadow:
|
||||
dx !== 0
|
||||
? swipeDir === 'right'
|
||||
? `0 16px 34px rgba(0,0,0,0.28), 0 0 0 1px rgba(16,185,129,${0.08 + reveal * 0.12})`
|
||||
: `0 16px 34px rgba(0,0,0,0.28), 0 0 0 1px rgba(244,63,94,${0.08 + reveal * 0.12})`
|
||||
: undefined,
|
||||
*/
|
||||
borderRadius: dx !== 0 ? '12px' : undefined,
|
||||
filter:
|
||||
dx !== 0
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user