ui updated

This commit is contained in:
Linrador 2026-04-09 14:00:37 +02:00
parent e25aca59c3
commit ebd7c84a24
5 changed files with 317 additions and 71 deletions

View File

@ -0,0 +1,84 @@
{
"items": [
{
"modelKey": "sw33t_g1rl",
"url": "https://www.myfreecams.com/#sw33t_g1rl",
"mode": "probe_retry",
"nextProbeAtMs": 1775732773528,
"currentShow": "offline",
"source": "watched"
},
{
"modelKey": "sunshinegirlx",
"url": "https://chaturbate.com/sunshinegirlx/",
"mode": "wait_public",
"currentShow": "private",
"imageUrl": "https://thumb.live.mmcdn.com/ri/sunshinegirlx.jpg",
"source": "watched"
},
{
"modelKey": "yourhanna",
"url": "https://chaturbate.com/yourhanna/",
"mode": "wait_public",
"currentShow": "hidden",
"imageUrl": "https://thumb.live.mmcdn.com/ri/yourhanna.jpg",
"source": "watched"
},
{
"modelKey": "nataliecroft",
"url": "https://chaturbate.com/nataliecroft/",
"mode": "wait_public",
"currentShow": "hidden",
"imageUrl": "https://thumb.live.mmcdn.com/ri/nataliecroft.jpg",
"source": "watched"
},
{
"modelKey": "wasianbby",
"url": "https://chaturbate.com/wasianbby/",
"mode": "wait_public",
"currentShow": "private",
"imageUrl": "https://thumb.live.mmcdn.com/ri/wasianbby.jpg",
"source": "watched"
},
{
"modelKey": "love_vibe",
"url": "https://chaturbate.com/love_vibe/",
"mode": "wait_public",
"currentShow": "private",
"imageUrl": "https://thumb.live.mmcdn.com/ri/love_vibe.jpg",
"source": "watched"
},
{
"modelKey": "georgiannahearn",
"url": "https://chaturbate.com/georgiannahearn/",
"mode": "wait_public",
"currentShow": "private",
"imageUrl": "https://thumb.live.mmcdn.com/ri/georgiannahearn.jpg",
"source": "watched"
},
{
"modelKey": "libbyshepard",
"url": "https://chaturbate.com/libbyshepard/",
"mode": "wait_public",
"currentShow": "away",
"imageUrl": "https://thumb.live.mmcdn.com/ri/libbyshepard.jpg",
"source": "watched"
},
{
"modelKey": "minions_",
"url": "https://chaturbate.com/minions_/",
"mode": "wait_public",
"currentShow": "hidden",
"imageUrl": "https://thumb.live.mmcdn.com/ri/minions_.jpg",
"source": "watched"
},
{
"modelKey": "amanda_sayhey",
"url": "https://chaturbate.com/amanda_sayhey/",
"mode": "probe_retry",
"nextProbeAtMs": 1775734021567,
"currentShow": "unknown",
"source": "watched"
}
]
}

View File

@ -0,0 +1,3 @@
{
"items": []
}

View File

@ -3580,6 +3580,12 @@ export default function App() {
return <LoginPage onLoggedIn={checkAuth} /> return <LoginPage onLoggedIn={checkAuth} />
} }
const headerInfoPanelClass =
'rounded-lg border border-gray-200/70 bg-white/45 px-2.5 py-1.5 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/5 sm:rounded-xl sm:px-3 sm:py-2'
const headerStatBadgeClass =
'inline-flex h-7 items-center gap-1 rounded-full border border-gray-200/70 bg-white/70 px-2 text-[10px] font-semibold text-gray-900 shadow-sm backdrop-blur dark:border-white/10 dark:bg-white/10 dark:text-gray-100 sm:h-8 sm:gap-1.5 sm:px-2.5 sm:text-[11px]'
return ( return (
<div className="min-h-[100dvh] bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100"> <div className="min-h-[100dvh] bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-100">
<div aria-hidden="true" className="pointer-events-none fixed inset-0 overflow-hidden"> <div aria-hidden="true" className="pointer-events-none fixed inset-0 overflow-hidden">
@ -3589,81 +3595,115 @@ export default function App() {
<div className="relative"> <div className="relative">
<header className="z-[70] bg-white/70 backdrop-blur dark:bg-gray-950/60 sm:sticky sm:top-0 sm:border-b sm:border-gray-200/70 sm:dark:border-white/10"> <header className="z-[70] bg-white/70 backdrop-blur dark:bg-gray-950/60 sm:sticky sm:top-0 sm:border-b sm:border-gray-200/70 sm:dark:border-white/10">
<div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pt-3 sm:py-4 space-y-2 sm:space-y-3"> <div className="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8 pt-2.5 sm:py-4 space-y-2 sm:space-y-3">
<div className="flex items-center sm:items-start justify-between gap-3 sm:gap-4"> <div className="flex items-center justify-between gap-3 sm:gap-4">
<div className="min-w-0"> <div className="min-w-0 flex-1">
<div className="min-w-0"> {/* Desktop */}
<div className="flex items-center gap-3 min-w-0"> <div className="hidden sm:flex min-h-[48px] items-center">
<h1 className="text-lg font-semibold tracking-tight text-gray-900 dark:text-white"> <div className={`min-w-0 ${headerInfoPanelClass}`}>
Recorder <div className="flex min-w-0 flex-wrap items-center gap-2.5">
</h1> <h1 className="shrink-0 text-lg font-semibold tracking-tight text-gray-900 dark:text-white">
Recorder
</h1>
{/* ✅ Mobile: Icons + Counts direkt rechts neben Recorder */} <div className="flex min-w-0 flex-wrap items-center gap-1.5">
<div className="flex items-center gap-1.5 shrink-0"> <span className={headerStatBadgeClass} title="online">
<span <SignalIcon className="size-4 opacity-80" />
className="inline-flex items-center gap-1 rounded-full bg-gray-100/80 px-2 py-1 text-[11px] font-semibold text-gray-900 ring-1 ring-gray-200/60 dark:bg-white/10 dark:text-gray-100 dark:ring-white/10" <span className="tabular-nums">{onlineModelsCount}</span>
title="online" </span>
>
<SignalIcon className="size-4 opacity-80" />
<span className="tabular-nums">{onlineModelsCount}</span>
</span>
<span <span className={headerStatBadgeClass} title="Watched online">
className="inline-flex items-center gap-1 rounded-full bg-gray-100/80 px-2 py-1 text-[11px] font-semibold text-gray-900 ring-1 ring-gray-200/60 dark:bg-white/10 dark:text-gray-100 dark:ring-white/10" <EyeIcon className="size-4 opacity-80" />
title="Watched online" <span className="tabular-nums">{onlineWatchedModelsCount}</span>
> </span>
<EyeIcon className="size-4 opacity-80" />
<span className="tabular-nums">{onlineWatchedModelsCount}</span>
</span>
<span <span className={headerStatBadgeClass} title="Fav online">
className="inline-flex items-center gap-1 rounded-full bg-gray-100/80 px-2 py-1 text-[11px] font-semibold text-gray-900 ring-1 ring-gray-200/60 dark:bg-white/10 dark:text-gray-100 dark:ring-white/10" <HeartIcon className="size-4 opacity-80" />
title="Fav online" <span className="tabular-nums">{onlineFavCount}</span>
> </span>
<HeartIcon className="size-4 opacity-80" />
<span className="tabular-nums">{onlineFavCount}</span>
</span>
<span <span className={headerStatBadgeClass} title="Like online">
className="inline-flex items-center gap-1 rounded-full bg-gray-100/80 px-2 py-1 text-[11px] font-semibold text-gray-900 ring-1 ring-gray-200/60 dark:bg-white/10 dark:text-gray-100 dark:ring-white/10" <HandThumbUpIcon className="size-4 opacity-80" />
title="Like online" <span className="tabular-nums">{onlineLikedCount}</span>
> </span>
<HandThumbUpIcon className="size-4 opacity-80" /> </div>
<span className="tabular-nums">{onlineLikedCount}</span>
</span> <div className="hidden lg:block h-4 w-px bg-gray-200/70 dark:bg-white/10" />
<div className="hidden lg:block whitespace-nowrap text-[11px] text-gray-500 dark:text-gray-400">
<LastUpdatedText
enabled={Boolean(recSettings.useChaturbateApi)}
fetchedAt={cbOnlineFetchedAt}
/>
</div>
</div> </div>
<div className="hidden sm:block text-[11px] text-gray-500 dark:text-gray-400"> </div>
<LastUpdatedText </div>
enabled={Boolean(recSettings.useChaturbateApi)}
fetchedAt={cbOnlineFetchedAt} {/* Mobile */}
/> <div className="sm:hidden space-y-1.5">
<div className={headerInfoPanelClass}>
<div className="flex min-w-0 flex-col gap-1.5">
<div className="flex min-w-0 items-center justify-between gap-2">
<h1 className="shrink-0 text-base font-semibold tracking-tight text-gray-900 dark:text-white">
Recorder
</h1>
<div className="min-w-0 flex max-w-[72%] flex-wrap justify-end gap-1">
<span className={headerStatBadgeClass} title="online">
<SignalIcon className="size-4 opacity-80" />
<span className="tabular-nums">{onlineModelsCount}</span>
</span>
<span className={headerStatBadgeClass} title="Watched online">
<EyeIcon className="size-4 opacity-80" />
<span className="tabular-nums">{onlineWatchedModelsCount}</span>
</span>
<span className={headerStatBadgeClass} title="Fav online">
<HeartIcon className="size-4 opacity-80" />
<span className="tabular-nums">{onlineFavCount}</span>
</span>
<span className={headerStatBadgeClass} title="Like online">
<HandThumbUpIcon className="size-4 opacity-80" />
<span className="tabular-nums">{onlineLikedCount}</span>
</span>
</div>
</div>
<div className="text-[10px] leading-tight text-gray-500 dark:text-gray-400">
<LastUpdatedText
enabled={Boolean(recSettings.useChaturbateApi)}
fetchedAt={cbOnlineFetchedAt}
/>
</div>
</div> </div>
</div> </div>
{/* ✅ Mobile: Status volle Breite + PerfMonitor + Cookies nebeneinander */} <div className="space-y-1.5">
<div className="sm:hidden mt-1 w-full"> {showPerfMon ? (
<div className="text-[11px] text-gray-500 dark:text-gray-400"> <PerformanceMonitor
<LastUpdatedText mode="inline"
enabled={Boolean(recSettings.useChaturbateApi)} compact
fetchedAt={cbOnlineFetchedAt} className="w-full"
/> />
</div> ) : null}
<div className="mt-2 flex items-stretch gap-2">
{showPerfMon ? <PerformanceMonitor mode="inline" className="flex-1" /> : <div className="flex-1" />}
<div className="grid grid-cols-2 gap-1.5">
<Button <Button
variant="secondary" variant="secondary"
onClick={() => setCookieModalOpen(true)} onClick={() => setCookieModalOpen(true)}
className="px-3 shrink-0" className="h-9 w-full px-2.5 text-sm"
> >
Cookies Cookies
</Button> </Button>
<Button <Button
variant="secondary" variant="soft"
color='red'
onClick={logout} onClick={logout}
className="px-3 shrink-0" className="h-9 w-full px-2.5 text-sm"
> >
Abmelden Abmelden
</Button> </Button>
@ -3672,12 +3712,29 @@ export default function App() {
</div> </div>
</div> </div>
<div className="hidden sm:flex items-center gap-2 h-full"> <div className="hidden sm:flex shrink-0 items-center gap-2 lg:gap-3">
{showPerfMon ? <PerformanceMonitor mode="inline" /> : null} {showPerfMon ? (
<Button variant="secondary" onClick={() => setCookieModalOpen(true)} className="h-9 px-3"> <PerformanceMonitor
mode="inline"
compact
className="w-[300px] lg:w-[360px]"
/>
) : null}
<Button
variant="secondary"
onClick={() => setCookieModalOpen(true)}
className="h-9 px-3 shrink-0"
>
Cookies Cookies
</Button> </Button>
<Button variant="secondary" onClick={logout} className="h-9 px-3">
<Button
variant="soft"
color='red'
onClick={logout}
className="h-9 px-3 shrink-0"
>
Abmelden Abmelden
</Button> </Button>
</div> </div>

View File

@ -3710,7 +3710,8 @@ export default function FinishedDownloads({
<Button <Button
size="md" size="md"
variant="secondary" variant={lastAction ? "soft" : "secondary"}
color="emerald"
disabled={!lastAction || undoing} disabled={!lastAction || undoing}
onClick={undoLastAction} onClick={undoLastAction}
leadingIcon={<ArrowUturnLeftIcon className="size-4 shrink-0" />} leadingIcon={<ArrowUturnLeftIcon className="size-4 shrink-0" />}
@ -3780,7 +3781,8 @@ export default function FinishedDownloads({
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<Button <Button
size="md" size="md"
variant="secondary" variant={lastAction ? "soft" : "secondary"}
color="emerald"
className="flex-1" className="flex-1"
disabled={!lastAction || undoing} disabled={!lastAction || undoing}
onClick={undoLastAction} onClick={undoLastAction}

View File

@ -8,6 +8,8 @@ type Props = {
className?: string className?: string
/** Server perf poll Intervall */ /** Server perf poll Intervall */
pollMs?: number pollMs?: number
/** kompakter, header-freundlicher Modus */
compact?: boolean
} }
const clamp01 = (v: number) => Math.max(0, Math.min(1, v)) const clamp01 = (v: number) => Math.max(0, Math.min(1, v))
@ -41,6 +43,26 @@ function formatBytes(bytes: number | null) {
return `${v.toFixed(digits)} ${units[i]}` return `${v.toFixed(digits)} ${units[i]}`
} }
function formatBytesCompact(bytes: number | null) {
if (bytes == null || !Number.isFinite(bytes) || bytes <= 0) return ''
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let v = bytes
let i = 0
while (v >= 1024 && i < units.length - 1) {
v /= 1024
i++
}
const rounded =
i === 0 ? Math.round(v) :
v >= 100 ? Math.round(v) :
v >= 10 ? Number(v.toFixed(0)) :
Number(v.toFixed(1))
return `${rounded} ${units[i]}`
}
function useFps(sampleMs = 1000) { function useFps(sampleMs = 1000) {
const [fps, setFps] = React.useState<number | null>(null) const [fps, setFps] = React.useState<number | null>(null)
@ -103,6 +125,7 @@ export default function PerformanceMonitor({
mode = 'inline', mode = 'inline',
className, className,
pollMs = 3000, pollMs = 3000,
compact = false,
}: Props) { }: Props) {
const fps = useFps(1000) const fps = useFps(1000)
@ -201,18 +224,95 @@ export default function PerformanceMonitor({
const wrapperClass = const wrapperClass =
mode === 'floating' mode === 'floating'
? 'fixed bottom-4 right-4 z-[80]' ? 'fixed bottom-4 right-4 z-[80]'
: 'flex items-center' // inline: sichtbar, Caller entscheidet per className : compact
? 'min-w-0'
: 'w-full min-w-0'
const panelClass = compact
? `
w-full min-w-0 rounded-lg border border-gray-200/70 bg-white/70 px-1.5 py-1 shadow-sm backdrop-blur
dark:border-white/10 dark:bg-white/5
sm:rounded-xl sm:px-2 sm:py-1.5
`
: `
rounded-lg border border-gray-200/70 bg-white/70 px-2.5 py-1.5 shadow-sm backdrop-blur
dark:border-white/10 dark:bg-white/5
grid grid-cols-2 gap-x-3 gap-y-2
sm:flex sm:items-center sm:gap-3
`
if (compact) {
const metrics = [
{
key: 'disk',
label: 'Disk',
value: formatBytesCompact(diskFreeBytes),
fill: usedFill,
tone: diskTone,
title: diskTitle,
},
{
key: 'ping',
label: 'Ping',
value: formatMs(ping),
fill: pingFill,
tone: pingTone,
title: `Ping: ${formatMs(ping)}`,
},
{
key: 'fps',
label: 'FPS',
value: fps != null ? String(fps) : '',
fill: fpsFill,
tone: fpsTone,
title: `FPS: ${fps != null ? fps : ''}`,
},
{
key: 'cpu',
label: 'CPU',
value: formatPct(cpu),
fill: cpuFill,
tone: cpuTone,
title: `CPU: ${formatPct(cpu)}`,
},
]
return (
<div className={`${wrapperClass} ${className ?? ''}`}>
<div className={panelClass}>
<div className="grid grid-cols-2 gap-1 md:grid-cols-4">
{metrics.map((item) => (
<div
key={item.key}
title={item.title}
className="min-w-0 rounded-md px-1.5 py-0.5 sm:rounded-lg sm:px-2 sm:py-1"
>
<div className="flex items-center justify-between gap-1.5 whitespace-nowrap">
<span className="min-w-0 truncate text-[9px] font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400 sm:text-[10px]">
{item.label}
</span>
<span className="shrink-0 whitespace-nowrap text-[10px] font-semibold leading-none tabular-nums text-gray-900 dark:text-gray-100 sm:text-[11px]">
{item.value}
</span>
</div>
<div className="mt-1 h-[3px] overflow-hidden rounded-full bg-gray-200/70 dark:bg-white/10 sm:mt-1.5 sm:h-1">
<div
className={`h-full rounded-full ${barTone(item.tone)}`}
style={{ width: `${Math.round(item.fill * 100)}%` }}
/>
</div>
</div>
))}
</div>
</div>
</div>
)
}
return ( return (
<div className={`${wrapperClass} ${className ?? ''}`}> <div className={`${wrapperClass} ${className ?? ''}`}>
<div <div className={panelClass}>
className="
rounded-lg border border-gray-200/70 bg-white/70 px-2.5 py-1.5 shadow-sm backdrop-blur
dark:border-white/10 dark:bg-white/5
grid grid-cols-2 gap-x-3 gap-y-2
sm:flex sm:items-center sm:gap-3
"
>
{/* DISK */} {/* DISK */}
<div className="flex items-center gap-2" title={diskTitle}> <div className="flex items-center gap-2" title={diskTitle}>
<span className="text-[11px] font-medium text-gray-600 dark:text-gray-300">Disk</span> <span className="text-[11px] font-medium text-gray-600 dark:text-gray-300">Disk</span>