diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 570ec280..328db39d 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -1,4 +1,4 @@ -import { Check, ChevronDown, Languages } from "lucide-react"; +import { Check, ChevronDown, Columns3, Languages, Rows3 } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { BsPauseCircle, BsPlayCircle, BsRecordCircle } from "react-icons/bs"; @@ -27,6 +27,7 @@ import { useCameraDevices } from "../../hooks/useCameraDevices"; import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices"; import { useScreenRecorder } from "../../hooks/useScreenRecorder"; import { requestCameraAccess } from "../../lib/requestCameraAccess"; +import { loadUserPreferences, saveUserPreferences } from "../../lib/userPreferences"; import { formatTimePadded } from "../../utils/timeUtils"; import { AudioLevelMeter } from "../ui/audio-level-meter"; import { Button } from "../ui/button"; @@ -59,6 +60,7 @@ const ICON_CONFIG = { type IconName = keyof typeof ICON_CONFIG; +/** Renders the configured icon for a HUD control. */ function getIcon(name: IconName, className?: string) { const { icon: Icon, size } = ICON_CONFIG[name]; return ; @@ -77,7 +79,10 @@ const windowBtnClasses = "flex h-8 w-8 items-center justify-center rounded-lg transition-all duration-150 cursor-pointer opacity-50 hover:opacity-90 hover:bg-white/[0.08]"; const hudSidebarClasses = "ml-0.5 pl-1.5 border-l border-white/10 flex items-center gap-0.5"; +const hudSidebarVerticalClasses = + "mt-0.5 pt-1.5 border-t border-white/10 flex flex-col items-center gap-0.5"; +/** Launches the floating recording HUD and its recorder controls. */ export function LaunchWindow() { const t = useScopedT("launch"); const availableLocales = getAvailableLocales(); @@ -128,6 +133,9 @@ export function LaunchWindow() { const [isWebcamFocused, setIsWebcamFocused] = useState(false); const webcamExpanded = isWebcamHovered || isWebcamFocused; const [isLanguageMenuOpen, setIsLanguageMenuOpen] = useState(false); + const [trayLayout, setTrayLayout] = useState<"horizontal" | "vertical">( + () => loadUserPreferences().trayLayout, + ); const [supportsCursorModeToggle, setSupportsCursorModeToggle] = useState(false); const languageTriggerRef = useRef(null); const languageMenuPanelRef = useRef(null); @@ -365,6 +373,12 @@ export function LaunchWindow() { window.electronAPI.hudOverlayClose(); } }; + /** Switches the HUD between horizontal and vertical tray layouts. */ + const toggleTrayLayout = () => { + const nextLayout = trayLayout === "horizontal" ? "vertical" : "horizontal"; + setTrayLayout(nextLayout); + saveUserPreferences({ trayLayout: nextLayout }); + }; const toggleMicrophone = () => { if (!recording) { @@ -589,7 +603,12 @@ export function LaunchWindow() { {/* HUD bar — fixed at bottom center, viewport-relative, never moves */} setHudMouseEventsEnabled(true)} onPointerDown={() => setHudMouseEventsEnabled(true)} onMouseEnter={() => setHudMouseEventsEnabled(true)} @@ -610,6 +629,33 @@ export function LaunchWindow() { {getIcon("drag", "text-white/30")} + + + {trayLayout === "horizontal" ? ( + + ) : ( + + )} + + + {/* Source selector */} {/* Audio controls group */} - + {recording && ( - + {canPauseRecording && ( + + { it("returns the directory for a POSIX path", () => { @@ -24,3 +24,21 @@ describe("parentDirectoryOf", () => { expect(parentDirectoryOf("")).toBeNull(); }); }); + +describe("user preferences", () => { + beforeEach(() => { + localStorage.clear(); + }); + + it("persists the tray layout preference", () => { + saveUserPreferences({ trayLayout: "vertical" }); + + expect(loadUserPreferences().trayLayout).toBe("vertical"); + }); + + it("falls back to the default tray layout for invalid stored values", () => { + localStorage.setItem("openscreen_user_preferences", JSON.stringify({ trayLayout: "diagonal" })); + + expect(loadUserPreferences().trayLayout).toBe("horizontal"); + }); +}); diff --git a/src/lib/userPreferences.ts b/src/lib/userPreferences.ts index 28a45068..128eb73b 100644 --- a/src/lib/userPreferences.ts +++ b/src/lib/userPreferences.ts @@ -29,6 +29,8 @@ export interface UserPreferences { exportFormat: ExportFormat; /** Folder used for the most recent successful export, if any */ exportFolder: string | null; + /** Recording HUD control layout */ + trayLayout: "horizontal" | "vertical"; } export const DEFAULT_PREFS: UserPreferences = { @@ -37,8 +39,10 @@ export const DEFAULT_PREFS: UserPreferences = { exportQuality: DEFAULT_EXPORT_SETTINGS.quality, exportFormat: DEFAULT_EXPORT_SETTINGS.format, exportFolder: null, + trayLayout: "horizontal", }; +/** Parses stored preferences without throwing on malformed JSON. */ function safeJsonParse(text: string | null): Record | null { if (!text) return null; try { @@ -87,6 +91,10 @@ export function loadUserPreferences(): UserPreferences { typeof raw.exportFolder === "string" && raw.exportFolder.length > 0 ? raw.exportFolder : DEFAULT_PREFS.exportFolder, + trayLayout: + raw.trayLayout === "horizontal" || raw.trayLayout === "vertical" + ? raw.trayLayout + : DEFAULT_PREFS.trayLayout, }; }