refactor: re-use one component for screen picker

huddles and screen clips use same picker component now. We had to make
sure that tailwind works for both of them.
This commit is contained in:
talksik
2026-04-08 17:13:06 -07:00
parent 91269754f3
commit 8f68a81739
5 changed files with 30 additions and 128 deletions
@@ -1,23 +1,32 @@
import { useState, useEffect } from "react";
interface ScreenSourcePickerProps {
title?: string;
confirmLabel?: string;
getSources: () => Promise<ScreenSource[]>;
onSelect: (sourceId: string) => void;
onCancel: () => void;
}
export function ScreenSourcePicker({ onSelect, onCancel }: ScreenSourcePickerProps) {
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(() => {
window.electronScreen.getScreenSources().then((result) => {
getSources().then((result) => {
setSources(result);
setLoading(false);
});
}, []);
}, [getSources]);
// Auto-select if there's only one screen and no windows
// Auto-select if there's only one source
useEffect(() => {
if (!loading && sources.length === 1) {
setSelectedId(sources[0].id);
@@ -31,9 +40,7 @@ export function ScreenSourcePicker({ onSelect, onCancel }: ScreenSourcePickerPro
<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">
Record your screen
</h2>
<h2 className="text-base font-medium text-zinc-100">{title}</h2>
<button
onClick={onCancel}
className="text-zinc-400 hover:text-zinc-200"
@@ -81,7 +88,7 @@ export function ScreenSourcePicker({ onSelect, onCancel }: ScreenSourcePickerPro
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"
>
Record
{confirmLabel}
</button>
</div>
</div>
+4 -1
View File
@@ -7,7 +7,7 @@ import { useScreenRecorder } from "@/features/compose/use-screen-recorder";
import { particlePath, parseParticlePath } from "@/lib/particle-path";
import type { ParticlePath } from "@/lib/particle-path";
import { RecordingOverlay } from "@/features/compose/recording-overlay";
import { ScreenSourcePicker } from "@/features/compose/screen-source-picker";
import { ScreenSourcePicker } from "@/components/screen-source-picker";
import { TextComposeStep } from "@/features/compose/text-compose-step";
import { ConfigureStreamStep } from "@/features/compose/configure-stream-step";
import { apiClient } from "@/api/client";
@@ -485,6 +485,9 @@ export function ComposeOverlay({
<>
{step === "picking" && (
<ScreenSourcePicker
title="Record your screen"
confirmLabel="Record"
getSources={window.electronScreen.getScreenSources}
onSelect={handleScreenSourceSelected}
onCancel={cancel}
/>
+5 -2
View File
@@ -22,7 +22,7 @@ import {
} from '@livekit/components-react';
import { RoomEvent, Track } from 'livekit-client';
import { useState, useEffect, useRef, useCallback } from 'react';
import { ScreenPicker } from './ScreenPicker';
import { ScreenSourcePicker } from '@/components/screen-source-picker';
export function HuddleApp() {
const [connection, setConnection] = useState<{ token: string; serverUrl: string } | null>(null);
@@ -198,7 +198,10 @@ function HuddleContent() {
<RoomAudioRenderer />
<StartAudio label="Allow audio" />
{showPicker && (
<ScreenPicker
<ScreenSourcePicker
title="Share your screen"
confirmLabel="Share"
getSources={window.electronHuddle.getScreenSources}
onSelect={handleScreenShare}
onCancel={() => setShowPicker(false)}
/>
-117
View File
@@ -1,117 +0,0 @@
import { useState, useEffect } from 'react';
interface ScreenPickerProps {
onSelect: (sourceId: string) => void;
onCancel: () => void;
}
export function ScreenPicker({ onSelect, onCancel }: ScreenPickerProps) {
const [sources, setSources] = useState<ScreenSource[]>([]);
const [selectedId, setSelectedId] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
window.electronHuddle.getScreenSources().then((result) => {
setSources(result);
setLoading(false);
});
}, []);
const screens = sources.filter((s) => s.id.startsWith('screen:'));
const windows = sources.filter((s) => s.id.startsWith('window:'));
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
<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">Share your screen</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"
>
Share
</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>
);
}
+6
View File
@@ -2,6 +2,12 @@
@import "tw-animate-css";
@import "shadcn/tailwind.css";
/* Ensure Tailwind scans shared components/features used by secondary windows
(huddle, screen_record, autoplay) whose vite root is a subdirectory. */
@source "../components";
@source "../features";
@source "../lib";
@custom-variant dark (&:is(.dark *));
@theme inline {