diff --git a/electron/windows.ts b/electron/windows.ts index a7623668..f5a46a54 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -28,8 +28,8 @@ export function createHudOverlayWindow(): BrowserWindow { const { workArea } = primaryDisplay; - const windowWidth = 600; - const windowHeight = 450; + const windowWidth = 620; + const windowHeight = 520; const x = Math.floor(workArea.x + (workArea.width - windowWidth) / 2); const y = Math.floor(workArea.y + workArea.height - windowHeight - 5); @@ -37,10 +37,10 @@ export function createHudOverlayWindow(): BrowserWindow { const win = new BrowserWindow({ width: windowWidth, height: windowHeight, - minWidth: 600, - maxWidth: 600, - minHeight: 450, - maxHeight: 450, + minWidth: 620, + maxWidth: 620, + minHeight: 520, + maxHeight: 520, x: x, y: y, frame: false, diff --git a/src/components/launch/LaunchWindow.module.css b/src/components/launch/LaunchWindow.module.css index 30304580..2ca0623e 100644 --- a/src/components/launch/LaunchWindow.module.css +++ b/src/components/launch/LaunchWindow.module.css @@ -9,11 +9,11 @@ .bar { display: flex; align-items: center; - gap: 6px; + gap: 8px; background: rgba(18, 18, 24, 0.97); border: 1px solid rgba(255, 255, 255, 0.07); - border-radius: 16px; - padding: 7px 10px; + border-radius: 18px; + padding: 10px 14px; box-shadow: 0 8px 40px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.04); @@ -23,7 +23,7 @@ .sep { width: 1px; - height: 22px; + height: 26px; background: #2a2a34; margin: 0 4px; flex-shrink: 0; @@ -31,9 +31,9 @@ .ib { position: relative; - width: 36px; - height: 36px; - border-radius: 10px; + width: 40px; + height: 40px; + border-radius: 11px; border: none; background: transparent; color: #6b6b78; @@ -79,9 +79,9 @@ display: inline-flex; align-items: center; gap: 7px; - height: 36px; - padding: 0 12px 0 10px; - border-radius: 10px; + height: 40px; + padding: 0 14px 0 12px; + border-radius: 11px; border: 1px solid #2a2a34; background: #1a1a22; color: #eeeef2; @@ -97,23 +97,35 @@ background: #20202a; } -.dropdown { - width: 260px; - max-height: 320px; +.menuArea { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + pointer-events: none; + overflow: hidden; + min-height: 0; +} + +.menuCard { + width: 300px; + max-height: 400px; overflow-y: auto; background: rgba(22, 22, 30, 0.96); border: 1px solid rgba(255, 255, 255, 0.07); - border-radius: 12px; - padding: 6px; + border-radius: 14px; + padding: 8px; + margin-top: auto; margin-bottom: 8px; box-shadow: 0 12px 48px rgba(0, 0, 0, 0.5); - animation: dropdownIn 0.15s ease; + pointer-events: auto; + animation: menuCardIn 0.18s ease; } -@keyframes dropdownIn { +@keyframes menuCardIn { from { opacity: 0; - transform: translateY(8px); + transform: translateY(12px); } to { opacity: 1; @@ -121,15 +133,15 @@ } } -.dropdown::-webkit-scrollbar { +.menuCard::-webkit-scrollbar { width: 4px; } -.dropdown::-webkit-scrollbar-track { +.menuCard::-webkit-scrollbar-track { background: transparent; } -.dropdown::-webkit-scrollbar-thumb { +.menuCard::-webkit-scrollbar-thumb { background: #2a2a34; border-radius: 2px; } @@ -170,8 +182,8 @@ .recBtn { position: relative; - width: 42px; - height: 42px; + width: 46px; + height: 46px; border-radius: 50%; border: none; background: #f43f5e; diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index e97cae17..3e5da6e4 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -100,6 +100,33 @@ function Separator() { return
; } +function MicDeviceRow({ + device, + selected, + onSelect, +}: { + device: { deviceId: string; label: string }; + selected: boolean; + onSelect: () => void; +}) { + const { level } = useAudioLevelMeter({ + enabled: true, + deviceId: device.deviceId, + }); + + return ( + + ); +} + export function LaunchWindow() { const { locale, setLocale } = useI18n(); const t = useScopedT("launch"); @@ -126,18 +153,14 @@ export function LaunchWindow() { const [selectedSource, setSelectedSource] = useState("Screen"); const [hasSelectedSource, setHasSelectedSource] = useState(false); const [recordingsDirectory, setRecordingsDirectory] = useState(null); - const [activeDropdown, setActiveDropdown] = useState<"none" | "sources" | "more">("none"); + const [activeDropdown, setActiveDropdown] = useState<"none" | "sources" | "more" | "mic">("none"); const [sources, setSources] = useState([]); const [sourcesLoading, setSourcesLoading] = useState(false); const dropdownRef = useRef(null); - const showMicControls = microphoneEnabled && !recording; + const micDropdownOpen = activeDropdown === "mic"; const { devices, selectedDeviceId, setSelectedDeviceId } = - useMicrophoneDevices(microphoneEnabled); - const { level } = useAudioLevelMeter({ - enabled: showMicControls, - deviceId: microphoneDeviceId, - }); + useMicrophoneDevices(microphoneEnabled || micDropdownOpen); useEffect(() => { if (selectedDeviceId && selectedDeviceId !== "default") { @@ -260,7 +283,7 @@ export function LaunchWindow() { } }, []); - const toggleDropdown = (which: "sources" | "more") => { + const toggleDropdown = (which: "sources" | "more" | "mic") => { setActiveDropdown(activeDropdown === which ? "none" : which); if (activeDropdown !== which && which === "sources") fetchSources(); }; @@ -302,7 +325,8 @@ export function LaunchWindow() { }; const toggleMicrophone = () => { - if (!recording) setMicrophoneEnabled(!microphoneEnabled); + if (recording) return; + toggleDropdown("mic"); }; const screenSources = sources.filter((s) => s.sourceType === "screen"); @@ -310,113 +334,140 @@ export function LaunchWindow() { return (
- {activeDropdown !== "none" && ( -
- {activeDropdown === "sources" && ( - <> - {sourcesLoading ? ( -
-
-
- ) : ( - <> - {screenSources.length > 0 && ( - <> -
Screens
- {screenSources.map((source) => ( - } - selected={selectedSource === source.name} - onClick={() => handleSourceSelect(source)} - > - {source.name} - - ))} - - )} - {windowSources.length > 0 && ( - <> -
0 ? { marginTop: 4 } : undefined}> - Windows + {/* Top area — flex:1 reserves space, menu card sits at bottom of this area */} +
+ {activeDropdown !== "none" && ( +
+ {activeDropdown === "sources" && ( + <> + {sourcesLoading ? ( +
+
+
+ ) : ( + <> + {screenSources.length > 0 && ( + <> +
Screens
+ {screenSources.map((source) => ( + } + selected={selectedSource === source.name} + onClick={() => handleSourceSelect(source)} + > + {source.name} + + ))} + + )} + {windowSources.length > 0 && ( + <> +
0 ? { marginTop: 4 } : undefined}> + Windows +
+ {windowSources.map((source) => ( + } + selected={selectedSource === source.name} + onClick={() => handleSourceSelect(source)} + > + {source.appName && source.appName !== source.name + ? `${source.appName} — ${source.name}` + : source.name} + + ))} + + )} + {screenSources.length === 0 && windowSources.length === 0 && ( +
+ No sources found
- {windowSources.map((source) => ( - } - selected={selectedSource === source.name} - onClick={() => handleSourceSelect(source)} - > - {source.appName && source.appName !== source.name - ? `${source.appName} — ${source.name}` - : source.name} - - ))} - - )} - {screenSources.length === 0 && windowSources.length === 0 && ( -
- No sources found -
- )} - - )} - - )} + )} + + )} + + )} - {activeDropdown === "more" && ( - <> - : } - onClick={() => setSystemAudioEnabled(!systemAudioEnabled)} - trailing={systemAudioEnabled ? ✓ : undefined} - > - System Audio - - } onClick={chooseRecordingsDirectory}> - Recordings Folder - - } onClick={openVideoFile}> - {t("recording.openVideoFile")} - - } onClick={openProjectFile}> - {t("recording.openProject")} - -
Language
- {SUPPORTED_LOCALES.map((code) => ( + {activeDropdown === "mic" && ( + <> +
Microphone
+ {microphoneEnabled && ( + } + onClick={() => { setMicrophoneEnabled(false); setActiveDropdown("none"); }} + > + Turn Off Microphone + + )} + {!microphoneEnabled && ( +
+ Select a microphone to enable +
+ )} + {devices.map((device) => ( + { + setMicrophoneEnabled(true); + setSelectedDeviceId(device.deviceId); + setMicrophoneDeviceId(device.deviceId); + }} + /> + ))} + {devices.length === 0 && ( +
+ No microphones found +
+ )} + + )} + + {activeDropdown === "more" && ( + <> } - selected={locale === code} - onClick={() => { setLocale(code as AppLocale); setActiveDropdown("none"); }} + icon={systemAudioEnabled ? : } + onClick={() => setSystemAudioEnabled(!systemAudioEnabled)} + trailing={systemAudioEnabled ? ✓ : undefined} > - {LOCALE_LABELS[code] ?? code} + System Audio - ))} - - )} -
- )} + } onClick={chooseRecordingsDirectory}> + Recordings Folder + + } onClick={openVideoFile}> + {t("recording.openVideoFile")} + + } onClick={openProjectFile}> + {t("recording.openProject")} + +
Language
+ {SUPPORTED_LOCALES.map((code) => ( + } + selected={locale === code} + onClick={() => { setLocale(code as AppLocale); setActiveDropdown("none"); }} + > + {LOCALE_LABELS[code] ?? code} + + ))} + + )} +
+ )} +
- {showMicControls && ( -
- - -
- )} - -
+ {/* Bottom section — fixed height, bar always stays here */} +
+
@@ -503,6 +554,7 @@ export function LaunchWindow() { )} +
); diff --git a/src/hooks/useScreenRecorder.ts b/src/hooks/useScreenRecorder.ts index d6af4091..26a43851 100644 --- a/src/hooks/useScreenRecorder.ts +++ b/src/hooks/useScreenRecorder.ts @@ -185,13 +185,14 @@ export function useScreenRecorder(): UseScreenRecorderReturn { return; } - const recorderState = mediaRecorder.current?.state; - if (recorderState === "recording" || recorderState === "paused") { + const recorder = mediaRecorder.current; + const recorderState = recorder?.state; + if (recorder && (recorderState === "recording" || recorderState === "paused")) { if (recorderState === "paused") { - mediaRecorder.current.resume(); + recorder.resume(); } cleanupCapturedMedia(); - mediaRecorder.current.stop(); + recorder.stop(); setRecording(false); window.electronAPI?.setRecordingState(false); }