8f68a81739
huddles and screen clips use same picker component now. We had to make sure that tailwind works for both of them.
140 lines
4.1 KiB
TypeScript
140 lines
4.1 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
|
|
interface ScreenSourcePickerProps {
|
|
title?: string;
|
|
confirmLabel?: string;
|
|
getSources: () => Promise<ScreenSource[]>;
|
|
onSelect: (sourceId: string) => void;
|
|
onCancel: () => void;
|
|
}
|
|
|
|
export function ScreenSourcePicker({
|
|
title = "Select a screen",
|
|
confirmLabel = "Select",
|
|
getSources,
|
|
onSelect,
|
|
onCancel,
|
|
}: ScreenSourcePickerProps) {
|
|
const [sources, setSources] = useState<ScreenSource[]>([]);
|
|
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
getSources().then((result) => {
|
|
setSources(result);
|
|
setLoading(false);
|
|
});
|
|
}, [getSources]);
|
|
|
|
// Auto-select if there's only one source
|
|
useEffect(() => {
|
|
if (!loading && sources.length === 1) {
|
|
setSelectedId(sources[0].id);
|
|
}
|
|
}, [loading, sources]);
|
|
|
|
const screens = sources.filter((s) => s.id.startsWith("screen:"));
|
|
const windows = sources.filter((s) => s.id.startsWith("window:"));
|
|
|
|
return (
|
|
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/90">
|
|
<div className="mx-4 flex max-h-[80vh] w-full max-w-2xl flex-col rounded-lg bg-zinc-900 shadow-xl">
|
|
<div className="flex items-center justify-between border-b border-zinc-700 px-5 py-4">
|
|
<h2 className="text-base font-medium text-zinc-100">{title}</h2>
|
|
<button
|
|
onClick={onCancel}
|
|
className="text-zinc-400 hover:text-zinc-200"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
|
|
<div className="flex-1 overflow-y-auto px-5 py-4">
|
|
{loading ? (
|
|
<p className="text-center text-sm text-zinc-400">
|
|
Loading sources...
|
|
</p>
|
|
) : (
|
|
<>
|
|
{screens.length > 0 && (
|
|
<SourceSection
|
|
title="Screens"
|
|
sources={screens}
|
|
selectedId={selectedId}
|
|
onSelect={setSelectedId}
|
|
/>
|
|
)}
|
|
{windows.length > 0 && (
|
|
<SourceSection
|
|
title="Windows"
|
|
sources={windows}
|
|
selectedId={selectedId}
|
|
onSelect={setSelectedId}
|
|
/>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-2 border-t border-zinc-700 px-5 py-3">
|
|
<button
|
|
onClick={onCancel}
|
|
className="rounded-md px-4 py-2 text-sm text-zinc-300 hover:bg-zinc-800"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
disabled={!selectedId}
|
|
onClick={() => selectedId && onSelect(selectedId)}
|
|
className="rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white hover:bg-blue-500 disabled:opacity-40 disabled:hover:bg-blue-600"
|
|
>
|
|
{confirmLabel}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SourceSection({
|
|
title,
|
|
sources,
|
|
selectedId,
|
|
onSelect,
|
|
}: {
|
|
title: string;
|
|
sources: ScreenSource[];
|
|
selectedId: string | null;
|
|
onSelect: (id: string) => void;
|
|
}) {
|
|
return (
|
|
<div className="mb-4">
|
|
<h3 className="mb-2 text-xs font-medium uppercase tracking-wide text-zinc-400">
|
|
{title}
|
|
</h3>
|
|
<div className="grid grid-cols-3 gap-3">
|
|
{sources.map((source) => (
|
|
<button
|
|
key={source.id}
|
|
onClick={() => onSelect(source.id)}
|
|
className={`overflow-hidden rounded-lg border-2 text-left transition-colors ${
|
|
selectedId === source.id
|
|
? "border-blue-500 bg-zinc-800"
|
|
: "border-transparent bg-zinc-800/50 hover:border-zinc-600"
|
|
}`}
|
|
>
|
|
<img
|
|
src={source.thumbnailDataUrl}
|
|
alt={source.name}
|
|
className="aspect-video w-full object-cover"
|
|
/>
|
|
<p className="truncate px-2 py-1.5 text-xs text-zinc-300">
|
|
{source.name}
|
|
</p>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|