revamped HUD

This commit is contained in:
Siddharth
2026-03-07 17:06:22 -08:00
parent 371f79a35f
commit 555b199e03
4 changed files with 291 additions and 205 deletions
+108 -18
View File
@@ -2,16 +2,119 @@
-webkit-app-region: drag;
}
.hudBar {
isolation: isolate;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.18);
}
.electronNoDrag {
-webkit-app-region: no-drag;
}
.hudBar {
isolation: isolate;
box-shadow:
0 2px 16px rgba(0, 0, 0, 0.25),
0 0 40px rgba(100, 80, 200, 0.08);
}
/* Sub-pill group container */
.hudGroup {
display: flex;
align-items: center;
gap: 2px;
background: rgba(255, 255, 255, 0.05);
border-radius: 9999px;
padding: 4px 8px;
transition: background 0.15s ease;
}
.hudGroup:hover {
background: rgba(255, 255, 255, 0.08);
}
/* Icon button within groups */
.hudIconBtn {
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
border-radius: 9999px;
transition: all 0.15s ease;
cursor: pointer;
background: transparent;
border: none;
color: #fff;
}
.hudIconBtn:hover {
background: rgba(255, 255, 255, 0.1);
transform: scale(1.08);
}
.hudIconBtn:active {
transform: scale(0.95);
}
/* Active icon glow (green) for enabled audio toggles */
.hudIconActive {
filter: drop-shadow(0 0 4px rgba(74, 222, 128, 0.4));
}
/* Recording pulse animation on the record group */
@keyframes recordPulse {
0%, 100% {
box-shadow: 0 0 8px rgba(239, 68, 68, 0.15);
}
50% {
box-shadow: 0 0 16px rgba(239, 68, 68, 0.4);
}
}
.recordingPulse {
animation: recordPulse 1.5s ease-in-out infinite;
background: rgba(239, 68, 68, 0.1) !important;
}
/* Mic panel above the bar */
.micPanel {
background: linear-gradient(135deg, rgba(28, 28, 36, 0.97) 0%, rgba(18, 18, 26, 0.96) 100%);
backdrop-filter: blur(16px) saturate(140%);
-webkit-backdrop-filter: blur(16px) saturate(140%);
border: 1px solid rgba(80, 80, 120, 0.25);
border-radius: 16px;
box-shadow:
0 2px 12px rgba(0, 0, 0, 0.2),
0 0 30px rgba(100, 80, 200, 0.06);
animation: micPanelIn 0.15s ease-out;
}
@keyframes micPanelIn {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Window control buttons */
.windowBtn {
display: flex;
align-items: center;
justify-content: center;
padding: 3px;
border-radius: 9999px;
transition: all 0.15s ease;
cursor: pointer;
background: transparent;
border: none;
opacity: 0.5;
}
.windowBtn:hover {
opacity: 0.9;
background: rgba(255, 255, 255, 0.08);
}
/* Folder button */
.folderButton {
cursor: pointer;
display: flex;
@@ -27,16 +130,3 @@
.folderButton:hover .folderText {
text-decoration: underline;
}
.hudOverlayButton {
cursor: pointer;
background: none;
border: none;
color: #fff;
opacity: 0.7;
transition: opacity 0.15s;
}
.hudOverlayButton:hover {
opacity: 0.7;
background: none !important;
}
+88 -101
View File
@@ -4,14 +4,13 @@ import { useScreenRecorder } from "../../hooks/useScreenRecorder";
import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices";
import { useAudioLevelMeter } from "../../hooks/useAudioLevelMeter";
import { AudioLevelMeter } from "../ui/audio-level-meter";
import { Button } from "../ui/button";
import { BsRecordCircle } from "react-icons/bs";
import { FaRegStopCircle } from "react-icons/fa";
import { MdMonitor, MdMic, MdMicOff, MdVolumeUp, MdVolumeOff } from "react-icons/md";
import { RxDragHandleDots2 } from "react-icons/rx";
import { FaFolderMinus } from "react-icons/fa6";
import { FiMinus, FiX } from "react-icons/fi";
import { ContentClamp } from "../ui/content-clamp";
export function LaunchWindow() {
const { recording, toggleRecording, microphoneEnabled, setMicrophoneEnabled, microphoneDeviceId, setMicrophoneDeviceId, systemAudioEnabled, setSystemAudioEnabled } = useScreenRecorder();
@@ -73,7 +72,7 @@ export function LaunchWindow() {
};
checkSelectedSource();
const interval = setInterval(checkSelectedSource, 500);
return () => clearInterval(interval);
}, []);
@@ -86,18 +85,17 @@ export function LaunchWindow() {
const openVideoFile = async () => {
const result = await window.electronAPI.openVideoFilePicker();
if (result.canceled) {
return;
}
if (result.success && result.path) {
await window.electronAPI.setCurrentVideoPath(result.path);
await window.electronAPI.switchToEditor();
}
};
// IPC events for hide/close
const sendHudOverlayHide = () => {
if (window.electronAPI && window.electronAPI.hudOverlayHide) {
window.electronAPI.hudOverlayHide();
@@ -117,26 +115,17 @@ export function LaunchWindow() {
return (
<div className="w-full h-full flex items-end justify-center bg-transparent">
<div
className={`w-full max-w-[500px] mx-auto flex flex-col px-4 py-2 ${styles.electronDrag} ${styles.hudBar}`}
style={{
borderRadius: 16,
background: 'linear-gradient(135deg, rgba(28,28,36,0.97) 0%, rgba(18,18,26,0.96) 100%)',
backdropFilter: 'blur(16px) saturate(140%)',
WebkitBackdropFilter: 'blur(16px) saturate(140%)',
border: '1px solid rgba(80,80,120,0.25)',
minHeight: 44,
}}
>
<div className={`flex flex-col items-center gap-2 mx-auto ${styles.electronDrag}`}>
{/* Mic controls panel */}
{showMicControls && (
<div className={`flex items-center gap-2 mb-2 pb-2 border-b border-white/10 ${styles.electronNoDrag}`}>
<div className={`flex items-center gap-2 px-4 py-2 ${styles.micPanel} ${styles.electronNoDrag}`}>
<select
value={microphoneDeviceId || selectedDeviceId}
onChange={(e) => {
setSelectedDeviceId(e.target.value);
setMicrophoneDeviceId(e.target.value);
}}
className="flex-1 bg-white/10 text-white text-xs rounded px-2 py-1 border border-white/20 outline-none truncate"
className="flex-1 bg-white/10 text-white text-xs rounded-full px-3 py-1 border border-white/20 outline-none truncate"
style={{ maxWidth: '70%' }}
>
{devices.map((device) => (
@@ -149,107 +138,105 @@ export function LaunchWindow() {
</div>
)}
<div className="flex items-center justify-between">
<div className={`flex items-center gap-1 ${styles.electronDrag}`}> <RxDragHandleDots2 size={18} className="text-white/40" /> </div>
{/* Main pill bar */}
<div
className={`flex items-center gap-1.5 px-2 py-1.5 ${styles.hudBar}`}
style={{
borderRadius: 9999,
background: 'linear-gradient(135deg, rgba(28,28,36,0.97) 0%, rgba(18,18,26,0.96) 100%)',
backdropFilter: 'blur(16px) saturate(140%)',
WebkitBackdropFilter: 'blur(16px) saturate(140%)',
border: '1px solid rgba(80,80,120,0.25)',
}}
>
{/* Drag handle */}
<div className={`flex items-center px-1 ${styles.electronDrag}`}>
<RxDragHandleDots2 size={16} className="text-white/30" />
</div>
<Button
variant="link"
size="sm"
className={`gap-1 text-white bg-transparent hover:bg-transparent px-0 flex-1 text-left text-xs ${styles.electronNoDrag}`}
{/* Source selector */}
<button
className={`${styles.hudGroup} ${styles.electronNoDrag}`}
onClick={openSourceSelector}
disabled={recording}
title={selectedSource}
>
<MdMonitor size={14} className="text-white" />
<ContentClamp truncateLength={6}>{selectedSource}</ContentClamp>
</Button>
<MdMonitor size={14} className="text-white/80" />
<span className="text-white/70 text-[11px] max-w-[72px] truncate">{selectedSource}</span>
</button>
<div className="w-px h-6 bg-white/30" />
{/* Audio controls group */}
<div className={`${styles.hudGroup} ${styles.electronNoDrag}`}>
<button
className={`${styles.hudIconBtn} ${systemAudioEnabled ? styles.hudIconActive : ''}`}
onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)}
disabled={recording}
title={systemAudioEnabled ? "Disable system audio" : "Enable system audio"}
>
{systemAudioEnabled ? (
<MdVolumeUp size={15} className="text-green-400" />
) : (
<MdVolumeOff size={15} className="text-white/40" />
)}
</button>
<button
className={`${styles.hudIconBtn} ${microphoneEnabled ? styles.hudIconActive : ''}`}
onClick={toggleMicrophone}
disabled={recording}
title={microphoneEnabled ? "Disable microphone" : "Enable microphone"}
>
{microphoneEnabled ? (
<MdMic size={15} className="text-green-400" />
) : (
<MdMicOff size={15} className="text-white/40" />
)}
</button>
</div>
<Button
variant="link"
size="sm"
onClick={() => !recording && setSystemAudioEnabled(!systemAudioEnabled)}
disabled={recording}
className={`gap-1 text-white bg-transparent hover:bg-transparent px-1 text-xs ${styles.electronNoDrag}`}
title={systemAudioEnabled ? "Disable system audio" : "Enable system audio"}
>
{systemAudioEnabled ? (
<MdVolumeUp size={16} className="text-green-400" />
) : (
<MdVolumeOff size={16} className="text-white/50" />
)}
</Button>
<Button
variant="link"
size="sm"
onClick={toggleMicrophone}
disabled={recording}
className={`gap-1 text-white bg-transparent hover:bg-transparent px-1 text-xs ${styles.electronNoDrag}`}
title={microphoneEnabled ? "Disable microphone" : "Enable microphone"}
>
{microphoneEnabled ? (
<MdMic size={16} className="text-green-400" />
) : (
<MdMicOff size={16} className="text-white/50" />
)}
</Button>
<div className="w-px h-6 bg-white/30" />
<Button
variant="link"
size="sm"
{/* Record/Stop group */}
<button
className={`${styles.hudGroup} ${styles.electronNoDrag} ${recording ? styles.recordingPulse : ''}`}
onClick={hasSelectedSource ? toggleRecording : openSourceSelector}
disabled={!hasSelectedSource && !recording}
className={`gap-1 text-white bg-transparent hover:bg-transparent px-0 flex-1 text-center text-xs ${styles.electronNoDrag}`}
style={{ flex: '0 0 auto' }}
>
{recording ? (
<>
<FaRegStopCircle size={14} className="text-red-400" />
<span className="text-red-400">{formatTime(elapsed)}</span>
<FaRegStopCircle size={13} className="text-red-400" />
<span className="text-red-400 text-xs font-semibold tabular-nums">{formatTime(elapsed)}</span>
</>
) : (
<>
<BsRecordCircle size={14} className={hasSelectedSource ? "text-white" : "text-white/50"} />
<span className={hasSelectedSource ? "text-white" : "text-white/50"}>Record</span>
</>
<BsRecordCircle size={14} className={hasSelectedSource ? "text-white/80" : "text-white/30"} />
)}
</Button>
</button>
<div className="w-px h-6 bg-white/30" />
<Button
variant="link"
size="sm"
{/* Open file */}
<button
className={`${styles.hudIconBtn} ${styles.electronNoDrag}`}
onClick={openVideoFile}
className={`gap-1 text-white bg-transparent hover:bg-transparent px-0 flex-1 text-right text-xs ${styles.electronNoDrag} ${styles.folderButton}`}
disabled={recording}
title="Open video file"
>
<FaFolderMinus size={14} className="text-white" />
<span className={styles.folderText}>Open</span>
</Button>
<FaFolderMinus size={14} className="text-white/60" />
</button>
<div className="w-px h-6 bg-white/30 mx-2" />
<Button
variant="link"
size="icon"
className={`ml-2 ${styles.electronNoDrag} hudOverlayButton`}
title="Hide HUD"
onClick={sendHudOverlayHide}
>
<FiMinus size={18} style={{ color: '#fff', opacity: 0.7 }} />
</Button>
<Button
variant="link"
size="icon"
className={`ml-1 ${styles.electronNoDrag} hudOverlayButton`}
title="Close App"
onClick={sendHudOverlayClose}
>
<FiX size={18} style={{ color: '#fff', opacity: 0.7 }} />
</Button>
{/* Window controls */}
<div className={`flex items-center gap-0.5 ${styles.electronNoDrag}`}>
<button
className={styles.windowBtn}
title="Hide HUD"
onClick={sendHudOverlayHide}
>
<FiMinus size={14} className="text-white" />
</button>
<button
className={styles.windowBtn}
title="Close App"
onClick={sendHudOverlayClose}
>
<FiX size={14} className="text-white" />
</button>
</div>
</div>
</div>
</div>
@@ -9,17 +9,28 @@
}
.sourceCard {
border-radius: 10px;
border-radius: 12px;
background: linear-gradient(120deg, rgba(38,38,48,0.98) 0%, rgba(24,24,32,0.96) 100%);
border: 1px solid rgba(60,60,80,0.22);
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.18);
transition: box-shadow 0.2s, border 0.2s, background 0.2s;
transition: box-shadow 0.2s ease, border-color 0.2s ease, transform 0.2s ease;
cursor: pointer;
}
.sourceCard:hover {
border-color: rgba(120,120,160,0.35);
transform: translateY(-1px);
box-shadow: 0 4px 12px 0 rgba(0,0,0,0.25);
}
.selected {
border: 2px solid #34B27B;
background: linear-gradient(120deg, rgba(91,33,182,0.18) 0%, rgba(38,38,48,0.98) 100%);
box-shadow: 0 0 0 2px #34B27B33;
background: linear-gradient(120deg, rgba(52,178,123,0.08) 0%, rgba(38,38,48,0.98) 100%);
box-shadow: 0 0 12px rgba(52,178,123,0.15), 0 0 4px rgba(52,178,123,0.1);
}
.selected:hover {
transform: translateY(0);
}
.icon {
@@ -29,8 +40,8 @@
}
.name {
font-size: 0.85rem;
color: #f3f4f6;
font-size: 0.8rem;
color: #e4e4e7;
font-weight: 500;
letter-spacing: 0.01em;
}
@@ -40,6 +51,18 @@
font-size: 0.75rem;
}
/* Checkmark badge */
.checkBadge {
width: 18px;
height: 18px;
background: #34B27B;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 0 8px rgba(52,178,123,0.4);
}
/* scrollbar */
.sourceGridScroll {
scrollbar-width: thin;
+66 -80
View File
@@ -2,7 +2,6 @@ import { useState, useEffect } from "react";
import { Button } from "../ui/button";
import { MdCheck } from "react-icons/md";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import { Card } from "../ui/card";
import styles from "./SourceSelector.module.css";
interface DesktopSource {
@@ -60,99 +59,86 @@ export function SourceSelector() {
return (
<div className={`h-full flex items-center justify-center ${styles.glassContainer}`} style={{ minHeight: '100vh' }}>
<div className="text-center">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-zinc-600 mx-auto mb-2" />
<p className="text-xs text-zinc-300">Loading sources...</p>
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-[#34B27B] mx-auto mb-2" />
<p className="text-xs text-zinc-400">Loading sources...</p>
</div>
</div>
);
}
const renderSourceCard = (source: DesktopSource) => {
const isSelected = selectedSource?.id === source.id;
return (
<div
key={source.id}
className={`${styles.sourceCard} ${isSelected ? styles.selected : ''} p-2`}
onClick={() => handleSourceSelect(source)}
>
<div className="relative mb-1.5">
<img
src={source.thumbnail || ''}
alt={source.name}
className="w-full aspect-video object-cover rounded-lg"
/>
{isSelected && (
<div className="absolute -top-1.5 -right-1.5">
<div className={styles.checkBadge}>
<MdCheck size={12} className="text-white" />
</div>
</div>
)}
</div>
<div className="flex items-center gap-1.5">
{source.appIcon && (
<img
src={source.appIcon}
alt=""
className={`${styles.icon} flex-shrink-0`}
/>
)}
<div className={`${styles.name} truncate`}>{source.name}</div>
</div>
</div>
);
};
return (
<div className={`min-h-screen flex flex-col items-center justify-center ${styles.glassContainer}`}>
<div className="flex-1 flex flex-col w-full max-w-xl" style={{ padding: 0 }}>
<Tabs defaultValue="screens">
<TabsList className="grid grid-cols-2 mb-3 bg-zinc-900/40 rounded-full">
<TabsTrigger value="screens" className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-zinc-200 rounded-full text-xs py-1">Screens</TabsTrigger>
<TabsTrigger value="windows" className="data-[state=active]:bg-[#34B27B] data-[state=active]:text-white text-zinc-200 rounded-full text-xs py-1">Windows</TabsTrigger>
<div className={`min-h-screen flex flex-col ${styles.glassContainer}`}>
<div className="flex-1 flex flex-col w-full px-4 pt-4">
<Tabs defaultValue="screens" className="flex-1 flex flex-col">
<TabsList className="grid grid-cols-2 mb-3 bg-white/5 rounded-full">
<TabsTrigger value="screens" className="data-[state=active]:bg-white/15 data-[state=active]:text-white text-zinc-400 rounded-full text-xs py-1 transition-all">Screens</TabsTrigger>
<TabsTrigger value="windows" className="data-[state=active]:bg-white/15 data-[state=active]:text-white text-zinc-400 rounded-full text-xs py-1 transition-all">Windows</TabsTrigger>
</TabsList>
<div className="h-72 flex flex-col justify-stretch">
<TabsContent value="screens" className="h-full">
<div className={`grid grid-cols-2 gap-2 h-full overflow-y-auto pr-1 relative ${styles.sourceGridScroll}`}>
{screenSources.map(source => (
<Card
key={source.id}
className={`${styles.sourceCard} ${selectedSource?.id === source.id ? styles.selected : ''} cursor-pointer h-fit p-2 scale-95`}
style={{ margin: 8, width: '90%', maxWidth: 220 }}
onClick={() => handleSourceSelect(source)}
>
<div className="p-1">
<div className="relative mb-1">
<img
src={source.thumbnail || ''}
alt={source.name}
className="w-full aspect-video object-cover rounded border border-zinc-800"
/>
{selectedSource?.id === source.id && (
<div className="absolute -top-1 -right-1">
<div className="w-4 h-4 bg-[#34B27B] rounded-full flex items-center justify-center shadow-md">
<MdCheck className={styles.icon} />
</div>
</div>
)}
</div>
<div className={styles.name + " truncate"}>{source.name}</div>
</div>
</Card>
))}
<div className="flex-1 min-h-0">
<TabsContent value="screens" className="h-full mt-0">
<div className={`grid grid-cols-2 gap-3 h-[280px] overflow-y-auto pr-1 auto-rows-min ${styles.sourceGridScroll}`}>
{screenSources.map(renderSourceCard)}
</div>
</TabsContent>
<TabsContent value="windows" className="h-full">
<div className={`grid grid-cols-2 gap-2 h-full overflow-y-auto pr-1 relative ${styles.sourceGridScroll}`}>
{windowSources.map(source => (
<Card
key={source.id}
className={`${styles.sourceCard} ${selectedSource?.id === source.id ? styles.selected : ''} cursor-pointer h-fit p-2 scale-95`}
style={{ margin: 8, width: '90%', maxWidth: 220 }}
onClick={() => handleSourceSelect(source)}
>
<div className="p-1">
<div className="relative mb-1">
<img
src={source.thumbnail || ''}
alt={source.name}
className="w-full aspect-video object-cover rounded border border-gray-700"
/>
{selectedSource?.id === source.id && (
<div className="absolute -top-1 -right-1">
<div className="w-4 h-4 bg-blue-600 rounded-full flex items-center justify-center shadow-md">
<MdCheck className={styles.icon} />
</div>
</div>
)}
</div>
<div className="flex items-center gap-1">
{source.appIcon && (
<img
src={source.appIcon}
alt="App icon"
className={styles.icon + " flex-shrink-0"}
/>
)}
<div className={styles.name + " truncate"}>{source.name}</div>
</div>
</div>
</Card>
))}
<TabsContent value="windows" className="h-full mt-0">
<div className={`grid grid-cols-2 gap-3 h-[280px] overflow-y-auto pr-1 auto-rows-min ${styles.sourceGridScroll}`}>
{windowSources.map(renderSourceCard)}
</div>
</TabsContent>
</div>
</Tabs>
</div>
<div className="border-t border-zinc-800 p-2 w-full max-w-xl">
<div className="flex justify-center gap-2">
<Button variant="outline" onClick={() => window.close()} className="px-4 py-1 text-xs bg-zinc-800 border-zinc-700 text-zinc-200 hover:bg-zinc-700">Cancel</Button>
<Button onClick={handleShare} disabled={!selectedSource} className="px-4 py-1 text-xs bg-[#34B27B] text-white hover:bg-[#34B27B]/80 disabled:opacity-50 disabled:bg-zinc-700">Share</Button>
</div>
<div className="p-3 flex justify-center gap-2">
<Button
variant="ghost"
onClick={() => window.close()}
className="px-5 py-1 text-xs text-zinc-400 hover:text-white hover:bg-white/5 rounded-full"
>
Cancel
</Button>
<Button
onClick={handleShare}
disabled={!selectedSource}
className="px-5 py-1 text-xs bg-[#34B27B] text-white hover:bg-[#34B27B]/80 disabled:opacity-30 disabled:bg-zinc-700 rounded-full"
>
Share
</Button>
</div>
</div>
);