import { ToggleButton } from "@heroui/react"; import { AppWindowIcon, CaretUpIcon, MonitorIcon } from "@phosphor-icons/react"; import * as React from "react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { useScopedT } from "@/contexts/I18nContext"; import { cn } from "@/lib/utils"; import { type DesktopSource, isScreenSource, isWindowSource, mapRawSource, } from "./popovers/launchPopoverTypes"; import "./launchTheme.css"; import "./SourceSelector.css"; import { useHudInteraction } from "./contexts/HudInteractionContext"; import { MarqueeText } from "./MarqueeText"; interface SourceSelectorProps { /** List of available screen sources */ screenSources?: DesktopSource[]; /** List of available window sources */ windowSources?: DesktopSource[]; /** Currently selected source name */ selectedSource?: string; /** Loading state */ loading?: boolean; /** Callback when a source is selected */ onSourceSelect?: (source: DesktopSource) => void; /** Callback to fetch sources */ onFetchSources?: () => Promise; /** Whether the popover is open */ open?: boolean; /** Callback when open state changes */ onOpenChange?: (open: boolean) => void; /** Optional custom trigger element */ children?: React.ReactNode; } /** * SourceSelectorContent - The actual list of sources */ export const SourceSelectorContent = ({ screenSources = [], windowSources = [], selectedSource = "Screen", loading = false, onSourceSelect = () => undefined, }: Pick< SourceSelectorProps, "screenSources" | "windowSources" | "selectedSource" | "loading" | "onSourceSelect" >) => { const t = useScopedT("launch"); const renderSourceItem = (source: DesktopSource, index: number) => { const isSelected = selectedSource === source.name; return ( onSourceSelect(source)} >
{source.thumbnail ? ( { (e.target as HTMLImageElement).style.display = "none"; }} /> ) : (
{source.sourceType === "window" ? ( ) : ( )}
)}
{source.sourceType === "screen" ? t("recording.screen") : t("recording.window")}
); }; const hasAnySources = screenSources.length > 0 || windowSources.length > 0; if (loading && !hasAnySources) { return (
); } return (
{hasAnySources ? ( <> {screenSources.length > 0 ? (
{t("recording.screens")} {t("common.loading", "Refreshing...")}
{screenSources.map((source, index) => renderSourceItem(source, index), )}
) : null} {windowSources.length > 0 ? (
{t("recording.windows")}
{windowSources.map((source, index) => renderSourceItem(source, index), )}
) : null} ) : (
{t("recording.noSourcesFound")}
)}
); }; /** * SourceSelector - A rich source selection component with thumbnails * Uses the shared HeroUI popover for positioning and accessibility */ export const SourceSelector = React.memo(function SourceSelector({ screenSources: propsScreenSources, windowSources: propsWindowSources, selectedSource: propsSelectedSource, loading: propsLoading, onSourceSelect: propsOnSourceSelect, onFetchSources: propsOnFetchSources, open: propsOpen, onOpenChange: propsOnOpenChange, children, }: SourceSelectorProps) { // Internal state for standalone/uncontrolled use const [internalOpen, setInternalOpen] = useState(false); const [internalSources, setInternalSources] = useState([]); const [internalLoading, setInternalLoading] = useState(false); const [internalSelectedSource, setInternalSelectedSource] = useState("Screen"); // Determine if we should use internal or external state/logic const isAutonomous = propsOpen === undefined; const open = propsOpen ?? internalOpen; const onOpenChange = propsOnOpenChange ?? setInternalOpen; const loading = propsLoading ?? internalLoading; const selectedSource = propsSelectedSource ?? internalSelectedSource; // Default fetching logic const defaultFetchSources = useCallback(async () => { if (!window.electronAPI) return; setInternalLoading(true); try { const rawSources = await window.electronAPI.getSources({ types: ["screen", "window"], thumbnailSize: { width: 160, height: 90 }, fetchWindowIcons: true, }); setInternalSources(rawSources.map((s) => mapRawSource(s as DesktopSource))); } catch (error) { console.error("Failed to fetch sources:", error); } finally { setInternalLoading(false); } }, []); const onFetchSources = propsOnFetchSources ?? defaultFetchSources; // Default selection logic const onSourceSelect = useCallback( async (source: DesktopSource) => { if (propsOnSourceSelect) { propsOnSourceSelect(source); return; } if (!window.electronAPI) return; try { const result = await window.electronAPI.selectSource(source); if (result) { setInternalSelectedSource(source.name); await window.electronAPI.showSourceHighlight?.(source); } } catch (error) { console.error("Failed to select source:", error); } }, [propsOnSourceSelect], ); // Split sources for internal use const internalScreenSources = useMemo( () => internalSources.filter(isScreenSource), [internalSources], ); const internalWindowSources = useMemo( () => internalSources.filter(isWindowSource), [internalSources], ); const screenSources = propsScreenSources ?? internalScreenSources; const windowSources = propsWindowSources ?? internalWindowSources; const hasPrefetchedRef = useRef(false); const fetchInFlightRef = useRef(false); const lastFetchedAtRef = useRef(0); const fetchSourcesOnce = useCallback( async (allowRecentSkip: boolean) => { if (fetchInFlightRef.current) { return; } if (allowRecentSkip && Date.now() - lastFetchedAtRef.current < 750) { return; } fetchInFlightRef.current = true; try { await onFetchSources(); lastFetchedAtRef.current = Date.now(); } finally { fetchInFlightRef.current = false; } }, [onFetchSources], ); const prefetchSources = React.useCallback(() => { if (hasPrefetchedRef.current) { return; } hasPrefetchedRef.current = true; void fetchSourcesOnce(false); }, [fetchSourcesOnce]); // Fetch sources when popover opens useEffect(() => { if (open) { void fetchSourcesOnce(true); } }, [open, fetchSourcesOnce]); // In autonomous mode, we might want to start open useEffect(() => { if (isAutonomous) { setInternalOpen(true); } }, [isAutonomous]); const trigger = children ? ( React.isValidElement(children) ? ( React.cloneElement(children as React.ReactElement>, { onPointerEnter: prefetchSources, onFocusCapture: prefetchSources, }) ) : ( children ) ) : ( ); const { onMouseEnter } = useHudInteraction(); return ( {trigger} ); }); SourceSelector.displayName = "SourceSelector";