- Fix dropdown menu pushing HUD bar down by using 100vh height container

with flex layout that reserves space above the bar for menu cards
  - Increase HUD window size to 620x520 for menu card headroom
  - Replace mic on/off toggle with mic selector card showing all available
    microphones with live audio level meters per device
  - Remove old separate mic controls panel
  - Increase bar size with larger buttons, padding, and separators
  - Fix TS null safety in stopRecording MediaRecorder accessfix
This commit is contained in:
Gurpreet Kait
2026-03-17 20:54:49 +05:30
parent 080b782659
commit 03c05c8464
4 changed files with 206 additions and 141 deletions
+6 -6
View File
@@ -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,
+35 -23
View File
@@ -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;
+160 -108
View File
@@ -100,6 +100,33 @@ function Separator() {
return <div className={styles.sep} />;
}
function MicDeviceRow({
device,
selected,
onSelect,
}: {
device: { deviceId: string; label: string };
selected: boolean;
onSelect: () => void;
}) {
const { level } = useAudioLevelMeter({
enabled: true,
deviceId: device.deviceId,
});
return (
<button
type="button"
className={`${styles.ddItem} ${selected ? styles.ddItemSelected : ""}`}
onClick={onSelect}
>
<span className="shrink-0">{selected ? <Mic size={16} /> : <MicOff size={16} />}</span>
<span className="truncate flex-1">{device.label}</span>
<AudioLevelMeter level={level} className="w-16 shrink-0" />
</button>
);
}
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<string | null>(null);
const [activeDropdown, setActiveDropdown] = useState<"none" | "sources" | "more">("none");
const [activeDropdown, setActiveDropdown] = useState<"none" | "sources" | "more" | "mic">("none");
const [sources, setSources] = useState<DesktopSource[]>([]);
const [sourcesLoading, setSourcesLoading] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(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 (
<div
className="w-full h-full flex flex-col justify-end items-center bg-transparent"
className="w-full flex flex-col bg-transparent"
style={{ height: "100vh" }}
ref={dropdownRef}
>
{activeDropdown !== "none" && (
<div className={`${styles.dropdown} ${styles.electronNoDrag}`}>
{activeDropdown === "sources" && (
<>
{sourcesLoading ? (
<div className="flex items-center justify-center py-6">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-[#6b6b78]" />
</div>
) : (
<>
{screenSources.length > 0 && (
<>
<div className={styles.ddLabel}>Screens</div>
{screenSources.map((source) => (
<DropdownItem
key={source.id}
icon={<Monitor size={16} />}
selected={selectedSource === source.name}
onClick={() => handleSourceSelect(source)}
>
{source.name}
</DropdownItem>
))}
</>
)}
{windowSources.length > 0 && (
<>
<div className={styles.ddLabel} style={screenSources.length > 0 ? { marginTop: 4 } : undefined}>
Windows
{/* Top area — flex:1 reserves space, menu card sits at bottom of this area */}
<div className={styles.menuArea}>
{activeDropdown !== "none" && (
<div className={`${styles.menuCard} ${styles.electronNoDrag}`}>
{activeDropdown === "sources" && (
<>
{sourcesLoading ? (
<div className="flex items-center justify-center py-6">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-[#6b6b78]" />
</div>
) : (
<>
{screenSources.length > 0 && (
<>
<div className={styles.ddLabel}>Screens</div>
{screenSources.map((source) => (
<DropdownItem
key={source.id}
icon={<Monitor size={16} />}
selected={selectedSource === source.name}
onClick={() => handleSourceSelect(source)}
>
{source.name}
</DropdownItem>
))}
</>
)}
{windowSources.length > 0 && (
<>
<div className={styles.ddLabel} style={screenSources.length > 0 ? { marginTop: 4 } : undefined}>
Windows
</div>
{windowSources.map((source) => (
<DropdownItem
key={source.id}
icon={<AppWindow size={16} />}
selected={selectedSource === source.name}
onClick={() => handleSourceSelect(source)}
>
{source.appName && source.appName !== source.name
? `${source.appName} — ${source.name}`
: source.name}
</DropdownItem>
))}
</>
)}
{screenSources.length === 0 && windowSources.length === 0 && (
<div className="text-center text-xs text-[#6b6b78] py-4">
No sources found
</div>
{windowSources.map((source) => (
<DropdownItem
key={source.id}
icon={<AppWindow size={16} />}
selected={selectedSource === source.name}
onClick={() => handleSourceSelect(source)}
>
{source.appName && source.appName !== source.name
? `${source.appName} — ${source.name}`
: source.name}
</DropdownItem>
))}
</>
)}
{screenSources.length === 0 && windowSources.length === 0 && (
<div className="text-center text-xs text-[#6b6b78] py-4">
No sources found
</div>
)}
</>
)}
</>
)}
)}
</>
)}
</>
)}
{activeDropdown === "more" && (
<>
<DropdownItem
icon={systemAudioEnabled ? <Volume2 size={16} className="text-[#6360f5]" /> : <VolumeX size={16} />}
onClick={() => setSystemAudioEnabled(!systemAudioEnabled)}
trailing={systemAudioEnabled ? <span className="ml-auto text-[#6360f5] text-xs">&#10003;</span> : undefined}
>
System Audio
</DropdownItem>
<DropdownItem icon={<FolderOpen size={16} />} onClick={chooseRecordingsDirectory}>
Recordings Folder
</DropdownItem>
<DropdownItem icon={<VideoIcon size={16} />} onClick={openVideoFile}>
{t("recording.openVideoFile")}
</DropdownItem>
<DropdownItem icon={<FolderOpen size={16} />} onClick={openProjectFile}>
{t("recording.openProject")}
</DropdownItem>
<div className={styles.ddLabel} style={{ marginTop: 4 }}>Language</div>
{SUPPORTED_LOCALES.map((code) => (
{activeDropdown === "mic" && (
<>
<div className={styles.ddLabel}>Microphone</div>
{microphoneEnabled && (
<DropdownItem
icon={<MicOff size={16} />}
onClick={() => { setMicrophoneEnabled(false); setActiveDropdown("none"); }}
>
Turn Off Microphone
</DropdownItem>
)}
{!microphoneEnabled && (
<div className="px-3 py-2 text-xs text-[#6b6b78]">
Select a microphone to enable
</div>
)}
{devices.map((device) => (
<MicDeviceRow
key={device.deviceId}
device={device}
selected={microphoneEnabled && (microphoneDeviceId === device.deviceId || selectedDeviceId === device.deviceId)}
onSelect={() => {
setMicrophoneEnabled(true);
setSelectedDeviceId(device.deviceId);
setMicrophoneDeviceId(device.deviceId);
}}
/>
))}
{devices.length === 0 && (
<div className="text-center text-xs text-[#6b6b78] py-4">
No microphones found
</div>
)}
</>
)}
{activeDropdown === "more" && (
<>
<DropdownItem
key={code}
icon={<Languages size={16} />}
selected={locale === code}
onClick={() => { setLocale(code as AppLocale); setActiveDropdown("none"); }}
icon={systemAudioEnabled ? <Volume2 size={16} className="text-[#6360f5]" /> : <VolumeX size={16} />}
onClick={() => setSystemAudioEnabled(!systemAudioEnabled)}
trailing={systemAudioEnabled ? <span className="ml-auto text-[#6360f5] text-xs">&#10003;</span> : undefined}
>
{LOCALE_LABELS[code] ?? code}
System Audio
</DropdownItem>
))}
</>
)}
</div>
)}
<DropdownItem icon={<FolderOpen size={16} />} onClick={chooseRecordingsDirectory}>
Recordings Folder
</DropdownItem>
<DropdownItem icon={<VideoIcon size={16} />} onClick={openVideoFile}>
{t("recording.openVideoFile")}
</DropdownItem>
<DropdownItem icon={<FolderOpen size={16} />} onClick={openProjectFile}>
{t("recording.openProject")}
</DropdownItem>
<div className={styles.ddLabel} style={{ marginTop: 4 }}>Language</div>
{SUPPORTED_LOCALES.map((code) => (
<DropdownItem
key={code}
icon={<Languages size={16} />}
selected={locale === code}
onClick={() => { setLocale(code as AppLocale); setActiveDropdown("none"); }}
>
{LOCALE_LABELS[code] ?? code}
</DropdownItem>
))}
</>
)}
</div>
)}
</div>
{showMicControls && (
<div className={`flex items-center gap-2 mb-2 rounded-xl border border-white/[0.07] bg-[rgba(18,18,24,0.97)] px-3 py-2 shadow-xl ${styles.electronNoDrag}`}>
<select
value={microphoneDeviceId || selectedDeviceId}
onChange={(e) => { setSelectedDeviceId(e.target.value); setMicrophoneDeviceId(e.target.value); }}
className={`max-w-[230px] rounded-lg border border-[#2a2a34] bg-[#1a1a22] px-3 py-1 text-xs text-[#eeeef2] outline-none ${styles.micSelect}`}
>
{devices.map((device) => (
<option key={device.deviceId} value={device.deviceId}>{device.label}</option>
))}
</select>
<AudioLevelMeter level={level} className="w-24" />
</div>
)}
<div className={`${styles.bar} ${styles.electronDrag} mb-2`}>
{/* Bottom section — fixed height, bar always stays here */}
<div className="flex flex-col items-center">
<div className={`${styles.bar} ${styles.electronDrag} mb-2`}>
<div className={`flex items-center px-0.5 ${styles.electronDrag}`}>
<RxDragHandleDots2 size={14} className="text-[#6b6b78]" />
</div>
@@ -503,6 +554,7 @@ export function LaunchWindow() {
</IconButton>
</>
)}
</div>
</div>
</div>
);
+5 -4
View File
@@ -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);
}