Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 61 additions & 7 deletions src/components/launch/LaunchWindow.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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 <Icon size={size} className={className} />;
Expand All @@ -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();
Expand Down Expand Up @@ -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<HTMLButtonElement | null>(null);
const languageMenuPanelRef = useRef<HTMLDivElement | null>(null);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -589,7 +603,12 @@ export function LaunchWindow() {
{/* HUD bar — fixed at bottom center, viewport-relative, never moves */}
<div
data-hud-interactive="true"
className={`fixed bottom-5 left-1/2 -translate-x-1/2 flex items-center gap-1.5 rounded-2xl border border-white/[0.10] bg-[#07080a]/90 px-2 py-1.5 shadow-[0_20px_60px_rgba(0,0,0,0.42),inset_0_1px_0_rgba(255,255,255,0.06)] backdrop-blur-2xl backdrop-saturate-[140%]`}
data-tray-layout={trayLayout}
className={`fixed bottom-5 left-1/2 -translate-x-1/2 flex rounded-2xl border border-white/[0.10] bg-[#07080a]/90 shadow-[0_20px_60px_rgba(0,0,0,0.42),inset_0_1px_0_rgba(255,255,255,0.06)] backdrop-blur-2xl backdrop-saturate-[140%] ${
trayLayout === "vertical"
? "max-h-[calc(100vh-2.5rem)] flex-col items-center gap-1.5 overflow-y-auto px-1.5 py-2"
: "items-center gap-1.5 px-2 py-1.5"
}`}
onPointerEnter={() => setHudMouseEventsEnabled(true)}
onPointerDown={() => setHudMouseEventsEnabled(true)}
onMouseEnter={() => setHudMouseEventsEnabled(true)}
Expand All @@ -610,6 +629,33 @@ export function LaunchWindow() {
{getIcon("drag", "text-white/30")}
</div>

<Tooltip
content={
trayLayout === "horizontal"
? t("tooltips.useVerticalTray")
: t("tooltips.useHorizontalTray")
}
>
<button
data-testid="launch-tray-layout-button"
type="button"
aria-label={
trayLayout === "horizontal"
? t("tooltips.useVerticalTray")
: t("tooltips.useHorizontalTray")
}
aria-pressed={trayLayout === "vertical"}
className={`${hudIconBtnClasses} ${styles.electronNoDrag}`}
onClick={toggleTrayLayout}
>
{trayLayout === "horizontal" ? (
<Columns3 size={ICON_SIZE} className="text-white/60" />
) : (
<Rows3 size={ICON_SIZE} className="text-white/60" />
)}
</button>
</Tooltip>

{/* Source selector */}
<button
className={`${hudGroupClasses} h-8 px-2.5 ${styles.electronNoDrag}`}
Expand All @@ -624,7 +670,9 @@ export function LaunchWindow() {
</button>

{/* Audio controls group */}
<div className={`${hudGroupClasses} ${styles.electronNoDrag}`}>
<div
className={`${hudGroupClasses} ${trayLayout === "vertical" ? "flex-col py-1" : ""} ${styles.electronNoDrag}`}
>
<button
data-testid="launch-system-audio-button"
className={`${hudIconBtnClasses} ${systemAudioEnabled ? "drop-shadow-[0_0_4px_rgba(74,222,128,0.4)]" : ""}`}
Expand Down Expand Up @@ -697,7 +745,7 @@ export function LaunchWindow() {
{/* Record/Stop group */}
<button
data-testid="launch-record-button"
className={`flex items-center justify-center rounded-full p-2 transition-[min-width,background-color] duration-150 ${recording ? "min-w-[78px]" : "min-w-[36px]"} ${styles.electronNoDrag} ${
className={`flex items-center justify-center rounded-full p-2 transition-[min-width,background-color] duration-150 ${recording ? "min-w-[78px]" : "min-w-[36px]"} ${trayLayout === "vertical" ? "min-h-9" : ""} ${styles.electronNoDrag} ${
recording
? paused
? "bg-amber-500/10 hover:bg-amber-500/15"
Expand All @@ -723,7 +771,9 @@ export function LaunchWindow() {
</button>

{recording && (
<div className={`flex items-center gap-0.5 ${styles.electronNoDrag}`}>
<div
className={`flex items-center gap-0.5 ${trayLayout === "vertical" ? "flex-col" : ""} ${styles.electronNoDrag}`}
>
{canPauseRecording && (
<Tooltip
content={paused ? t("tooltips.resumeRecording") : t("tooltips.pauseRecording")}
Expand Down Expand Up @@ -776,7 +826,9 @@ export function LaunchWindow() {
)}

{/* Right sidebar controls */}
<div className={`${hudSidebarClasses} ${styles.electronNoDrag}`}>
<div
className={`${trayLayout === "vertical" ? hudSidebarVerticalClasses : hudSidebarClasses} ${styles.electronNoDrag}`}
>
<div className={`${styles.languageMenuContainer} ${styles.electronNoDrag}`}>
<button
ref={languageTriggerRef}
Expand Down Expand Up @@ -841,7 +893,9 @@ export function LaunchWindow() {
: null}

{/* Window controls */}
<div className="flex items-center gap-0.5">
<div
className={`flex items-center gap-0.5 ${trayLayout === "vertical" ? "flex-col" : ""}`}
>
<button
className={windowBtnClasses}
title={t("tooltips.hideHUD")}
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/ar/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"pauseRecording": "إيقاف التسجيل مؤقتاً",
"resumeRecording": "استئناف التسجيل",
"openVideoFile": "فتح ملف فيديو",
"openProject": "فتح مشروع"
"openProject": "فتح مشروع",
"useVerticalTray": "استخدام الشريط العمودي",
"useHorizontalTray": "استخدام الشريط الأفقي"
},
"audio": {
"enableSystemAudio": "تفعيل صوت النظام",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/en/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"pauseRecording": "Pause recording",
"resumeRecording": "Resume recording",
"openVideoFile": "Open video file",
"openProject": "Open project"
"openProject": "Open project",
"useVerticalTray": "Use vertical tray",
"useHorizontalTray": "Use horizontal tray"
},
"audio": {
"enableSystemAudio": "Enable system audio",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/es/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"pauseRecording": "Pausar grabación",
"resumeRecording": "Reanudar grabación",
"openVideoFile": "Abrir archivo de video",
"openProject": "Abrir proyecto"
"openProject": "Abrir proyecto",
"useVerticalTray": "Usar bandeja vertical",
"useHorizontalTray": "Usar bandeja horizontal"
},
"audio": {
"enableSystemAudio": "Activar audio del sistema",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/fr/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"pauseRecording": "Mettre en pause l'enregistrement",
"resumeRecording": "Reprendre l'enregistrement",
"openVideoFile": "Ouvrir un fichier vidéo",
"openProject": "Ouvrir un projet"
"openProject": "Ouvrir un projet",
"useVerticalTray": "Utiliser la barre verticale",
"useHorizontalTray": "Utiliser la barre horizontale"
},
"audio": {
"enableSystemAudio": "Activer l'audio système",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/it/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"pauseRecording": "Metti in pausa registrazione",
"resumeRecording": "Riprendi registrazione",
"openVideoFile": "Apri file video",
"openProject": "Apri progetto"
"openProject": "Apri progetto",
"useVerticalTray": "Usa barra verticale",
"useHorizontalTray": "Usa barra orizzontale"
},
"audio": {
"enableSystemAudio": "Abilita audio di sistema",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/ja-JP/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"pauseRecording": "録画を一時停止",
"resumeRecording": "録画を再開",
"openVideoFile": "動画ファイルを開く",
"openProject": "プロジェクトを開く"
"openProject": "プロジェクトを開く",
"useVerticalTray": "縦型トレイを使用",
"useHorizontalTray": "横型トレイを使用"
},
"audio": {
"enableSystemAudio": "システム音声を有効にする",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/ko-KR/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"pauseRecording": "녹화 일시정지",
"resumeRecording": "녹화 재개",
"openVideoFile": "비디오 파일 열기",
"openProject": "프로젝트 열기"
"openProject": "프로젝트 열기",
"useVerticalTray": "세로 트레이 사용",
"useHorizontalTray": "가로 트레이 사용"
},
"audio": {
"enableSystemAudio": "시스템 오디오 활성화",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/ru/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"pauseRecording": "Приостановить запись",
"resumeRecording": "Возобновить запись",
"openVideoFile": "Открыть видеофайл",
"openProject": "Открыть проект"
"openProject": "Открыть проект",
"useVerticalTray": "Использовать вертикальную панель",
"useHorizontalTray": "Использовать горизонтальную панель"
},
"audio": {
"enableSystemAudio": "Включить системное аудио",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/tr/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"pauseRecording": "Kaydı duraklat",
"resumeRecording": "Kayda devam et",
"openVideoFile": "Video dosyası aç",
"openProject": "Proje aç"
"openProject": "Proje aç",
"useVerticalTray": "Dikey araç çubuğunu kullan",
"useHorizontalTray": "Yatay araç çubuğunu kullan"
},
"audio": {
"enableSystemAudio": "Sistem sesini etkinleştir",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/vi/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"pauseRecording": "Tạm dừng ghi hình",
"resumeRecording": "Tiếp tục ghi hình",
"openVideoFile": "Mở tệp video",
"openProject": "Mở dự án"
"openProject": "Mở dự án",
"useVerticalTray": "Dùng khay dọc",
"useHorizontalTray": "Dùng khay ngang"
},
"audio": {
"enableSystemAudio": "Bật âm thanh hệ thống",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/zh-CN/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"pauseRecording": "暂停录制",
"resumeRecording": "继续录制",
"openVideoFile": "打开视频文件",
"openProject": "打开项目"
"openProject": "打开项目",
"useVerticalTray": "使用竖向托盘",
"useHorizontalTray": "使用横向托盘"
},
"audio": {
"enableSystemAudio": "启用系统音频",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/zh-TW/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
"pauseRecording": "暫停錄製",
"resumeRecording": "繼續錄製",
"openVideoFile": "開啟影片檔案",
"openProject": "開啟專案"
"openProject": "開啟專案",
"useVerticalTray": "使用直向托盤",
"useHorizontalTray": "使用橫向托盤"
},
"audio": {
"enableSystemAudio": "啟用系統音訊",
Expand Down
22 changes: 20 additions & 2 deletions src/lib/userPreferences.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { parentDirectoryOf } from "./userPreferences";
import { beforeEach, describe, expect, it } from "vitest";
import { loadUserPreferences, parentDirectoryOf, saveUserPreferences } from "./userPreferences";

describe("parentDirectoryOf", () => {
it("returns the directory for a POSIX path", () => {
Expand All @@ -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");
});
});
8 changes: 8 additions & 0 deletions src/lib/userPreferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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<string, unknown> | null {
if (!text) return null;
try {
Expand Down Expand Up @@ -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,
};
}

Expand Down
Loading