Skip to content

Commit a769e80

Browse files
committed
feat: enhance screen selection and UI for Meeting Video Transrecorder
1 parent f32ee2d commit a769e80

21 files changed

Lines changed: 696 additions & 370 deletions

README.md

Lines changed: 213 additions & 314 deletions
Large diffs are not rendered by default.

src/renderer/src/App.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,11 @@ export default function App(): React.JSX.Element {
1515
hasVideo,
1616
videoRef,
1717
previewVideoRef,
18+
availableScreens,
19+
selectedScreen,
1820
setRecordingType,
21+
setSelectedScreen,
22+
getAvailableScreens,
1923
startRecording,
2024
stopRecording,
2125
downloadVideo,
@@ -35,7 +39,11 @@ export default function App(): React.JSX.Element {
3539
videoRef={videoRef}
3640
stream={stream}
3741
recordedVideo={recordedVideo}
42+
availableScreens={availableScreens}
43+
selectedScreen={selectedScreen}
3844
onRecordingTypeChange={setRecordingType}
45+
onScreenSelect={setSelectedScreen}
46+
onRefreshScreens={getAvailableScreens}
3947
onStartRecording={startRecording}
4048
onStopRecording={stopRecording}
4149
onDownloadVideo={downloadVideo}

src/renderer/src/assets/main.css

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,3 +224,75 @@
224224
.btn-icon {
225225
@apply ml-2;
226226
}
227+
228+
.screen-selector-card {
229+
@apply bg-white rounded-lg shadow-sm border border-slate-200;
230+
}
231+
232+
.screen-selector-header {
233+
@apply p-6 border-b border-slate-200 flex items-center justify-between;
234+
}
235+
236+
.screen-selector-title {
237+
@apply flex items-center gap-2;
238+
}
239+
240+
.screen-selector-icon {
241+
@apply w-5 h-5;
242+
}
243+
244+
.screen-selector-heading {
245+
@apply text-xl font-semibold text-slate-900;
246+
}
247+
248+
.screen-selector-content {
249+
@apply p-6;
250+
}
251+
252+
.screen-selector-empty {
253+
@apply text-center py-8;
254+
}
255+
256+
.screen-selector-empty-text {
257+
@apply text-slate-600 text-sm;
258+
}
259+
260+
.screen-selector-list {
261+
@apply space-y-3;
262+
}
263+
264+
.screen-selector-option {
265+
@apply flex items-start gap-3 p-4 border border-slate-200 rounded-lg cursor-pointer hover:bg-slate-50 transition-colors;
266+
}
267+
268+
.screen-selector-option:hover {
269+
@apply border-slate-300;
270+
}
271+
272+
.screen-selector-radio {
273+
@apply w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 focus:ring-2 mt-1;
274+
}
275+
276+
.screen-selector-option-content {
277+
@apply flex-1 flex items-center justify-between;
278+
}
279+
280+
.screen-selector-option-info {
281+
@apply flex flex-col gap-1;
282+
}
283+
284+
.screen-selector-option-name {
285+
@apply text-sm font-medium text-slate-900;
286+
}
287+
288+
.screen-selector-option-id {
289+
@apply text-xs text-slate-500;
290+
}
291+
292+
.screen-selector-thumbnail {
293+
@apply w-16 h-12 rounded border border-slate-200 overflow-hidden;
294+
}
295+
296+
.screen-selector-thumbnail-img {
297+
@apply w-full h-full object-cover;
298+
}

src/renderer/src/components/AppLayout.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { RecordingControls } from './RecordingControls'
44
import { StatusMessages } from './StatusMessages'
55
import { RecordingStatus } from './RecordingStatus'
66
import { VideoPreview } from './VideoPreview'
7-
import { RecordingType } from '../types'
7+
import { ScreenSelector } from './ScreenSelector'
8+
import { RecordingType, DesktopSource } from '../types'
89

910
interface AppLayoutProps {
1011
isRecording: boolean
@@ -18,7 +19,11 @@ interface AppLayoutProps {
1819
videoRef: React.RefObject<HTMLVideoElement | null>
1920
stream: MediaStream | null
2021
recordedVideo: string | null
22+
availableScreens: DesktopSource[]
23+
selectedScreen: DesktopSource | null
2124
onRecordingTypeChange: (type: RecordingType) => void
25+
onScreenSelect: (screen: DesktopSource | null) => void
26+
onRefreshScreens: () => Promise<void>
2227
onStartRecording: () => Promise<void>
2328
onStopRecording: () => void
2429
onDownloadVideo: () => Promise<void>
@@ -37,7 +42,11 @@ export const AppLayout: React.FC<AppLayoutProps> = ({
3742
videoRef,
3843
stream,
3944
recordedVideo,
45+
availableScreens,
46+
selectedScreen,
4047
onRecordingTypeChange,
48+
onScreenSelect,
49+
onRefreshScreens,
4150
onStartRecording,
4251
onStopRecording,
4352
onDownloadVideo,
@@ -48,12 +57,20 @@ export const AppLayout: React.FC<AppLayoutProps> = ({
4857
<div className="app-content">
4958
<Header />
5059

60+
<ScreenSelector
61+
availableScreens={availableScreens}
62+
selectedScreen={selectedScreen}
63+
onScreenSelect={onScreenSelect}
64+
onRefreshScreens={onRefreshScreens}
65+
/>
66+
5167
<RecordingControls
5268
isRecording={isRecording}
5369
isSaving={isSaving}
5470
isProcessingTranscript={isProcessingTranscript}
5571
hasVideo={hasVideo}
5672
recordingType={recordingType}
73+
selectedScreen={selectedScreen}
5774
onRecordingTypeChange={onRecordingTypeChange}
5875
onStartRecording={onStartRecording}
5976
onStopRecording={onStopRecording}

src/renderer/src/components/RecordingControls.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React from 'react'
2-
import { RecordingType } from '../types'
2+
import { RecordingType, DesktopSource } from '../types'
33
import { getRecordingTypeIcon, getRecordingTypeLabel } from '../utils/recording.utils'
44
import { getButtonClasses } from '../utils/ui.utils'
55
import { PlayIcon, SquareIcon, DownloadIcon, FileTextIcon, LoaderIcon, VideoIcon } from './icons'
@@ -10,6 +10,7 @@ interface RecordingControlsProps {
1010
isProcessingTranscript: boolean
1111
hasVideo: boolean
1212
recordingType: RecordingType
13+
selectedScreen: DesktopSource | null
1314
onRecordingTypeChange: (type: RecordingType) => void
1415
onStartRecording: () => void
1516
onStopRecording: () => void
@@ -23,6 +24,7 @@ export const RecordingControls: React.FC<RecordingControlsProps> = ({
2324
isProcessingTranscript,
2425
hasVideo,
2526
recordingType,
27+
selectedScreen,
2628
onRecordingTypeChange,
2729
onStartRecording,
2830
onStopRecording,
@@ -68,8 +70,9 @@ export const RecordingControls: React.FC<RecordingControlsProps> = ({
6870
<div className="controls-buttons">
6971
<button
7072
onClick={onStartRecording}
71-
disabled={isRecording}
72-
className={getButtonClasses('danger', isRecording)}
73+
disabled={isRecording || !selectedScreen}
74+
className={getButtonClasses('danger', isRecording || !selectedScreen)}
75+
title={!selectedScreen ? 'Please select a screen to record' : undefined}
7376
>
7477
{isRecording ? (
7578
<>
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import React, { useEffect, useRef, useState } from 'react'
2+
import { DesktopSource } from '../types'
3+
import { getButtonClasses } from '../utils/ui.utils'
4+
import { MonitorIcon, RefreshIcon } from './icons'
5+
6+
interface ScreenSelectorProps {
7+
availableScreens: DesktopSource[]
8+
selectedScreen: DesktopSource | null
9+
onScreenSelect: (screen: DesktopSource | null) => void
10+
onRefreshScreens: () => Promise<void>
11+
isLoading?: boolean
12+
}
13+
14+
export const ScreenSelector: React.FC<ScreenSelectorProps> = ({
15+
availableScreens,
16+
selectedScreen,
17+
onScreenSelect,
18+
onRefreshScreens,
19+
isLoading = false
20+
}): React.JSX.Element => {
21+
const [previewStreams, setPreviewStreams] = useState<Record<string, MediaStream | null>>({})
22+
const videoRefs = useRef<Record<string, HTMLVideoElement | null>>({})
23+
24+
useEffect(() => {
25+
let isMounted = true
26+
const getPreviewStream = async (sourceId: string): Promise<MediaStream | null> => {
27+
try {
28+
const stream = await navigator.mediaDevices.getUserMedia({
29+
audio: false,
30+
video: {
31+
mandatory: {
32+
chromeMediaSource: 'desktop',
33+
chromeMediaSourceId: sourceId,
34+
maxWidth: 320,
35+
maxHeight: 180
36+
}
37+
} as MediaTrackConstraints
38+
})
39+
return stream
40+
} catch {
41+
return null
42+
}
43+
}
44+
;(async () => {
45+
const streams: Record<string, MediaStream | null> = {}
46+
for (const screen of availableScreens) {
47+
streams[screen.id] = await getPreviewStream(screen.id)
48+
}
49+
if (isMounted) setPreviewStreams(streams)
50+
})()
51+
return () => {
52+
isMounted = false
53+
Object.values(previewStreams).forEach((stream) => {
54+
if (stream) {
55+
stream.getTracks().forEach((track) => track.stop())
56+
}
57+
})
58+
setPreviewStreams({})
59+
}
60+
}, [availableScreens])
61+
62+
useEffect(() => {
63+
for (const [id, stream] of Object.entries(previewStreams)) {
64+
const video = videoRefs.current[id]
65+
if (video && stream) {
66+
video.srcObject = stream
67+
}
68+
}
69+
}, [previewStreams])
70+
71+
return (
72+
<div className="screen-selector-card">
73+
<div className="screen-selector-header">
74+
<div className="screen-selector-title">
75+
<div className="screen-selector-icon">
76+
<MonitorIcon />
77+
</div>
78+
<h3 className="screen-selector-heading">Screen Selection</h3>
79+
</div>
80+
<button
81+
onClick={onRefreshScreens}
82+
disabled={isLoading}
83+
className={getButtonClasses('secondary', isLoading)}
84+
title="Refresh available screens"
85+
>
86+
<RefreshIcon />
87+
<span className="btn-icon">Refresh</span>
88+
</button>
89+
</div>
90+
<div className="screen-selector-content">
91+
{availableScreens.length === 0 ? (
92+
<div className="screen-selector-empty">
93+
<p className="screen-selector-empty-text">
94+
No screens available. Click &quot;Refresh&quot; to detect available screens.
95+
</p>
96+
</div>
97+
) : (
98+
<div className="screen-selector-list">
99+
{availableScreens.map((screen) => (
100+
<label key={screen.id} className="screen-selector-option">
101+
<input
102+
type="radio"
103+
name="screenSelection"
104+
value={screen.id}
105+
checked={selectedScreen?.id === screen.id}
106+
onChange={() => onScreenSelect(screen)}
107+
className="screen-selector-radio"
108+
/>
109+
<div className="screen-selector-option-content">
110+
<div className="screen-selector-option-info">
111+
<span className="screen-selector-option-name">{screen.name}</span>
112+
<span className="screen-selector-option-id">ID: {screen.display_id}</span>
113+
</div>
114+
<div className="screen-selector-thumbnail">
115+
<video
116+
ref={(el) => {
117+
videoRefs.current[screen.id] = el
118+
}}
119+
className="screen-selector-thumbnail-img"
120+
autoPlay
121+
muted
122+
playsInline
123+
width={160}
124+
height={90}
125+
style={{ background: '#222', borderRadius: 6, objectFit: 'cover' }}
126+
/>
127+
</div>
128+
</div>
129+
</label>
130+
))}
131+
</div>
132+
)}
133+
</div>
134+
</div>
135+
)
136+
}
Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,21 @@
1-
export const AlertCircleIcon = () => (
2-
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1+
import React from 'react'
2+
3+
export const AlertCircleIcon: React.FC<React.SVGProps<SVGSVGElement>> = (
4+
props
5+
): React.JSX.Element => (
6+
<svg
7+
width="24"
8+
height="24"
9+
viewBox="0 0 24 24"
10+
fill="none"
11+
stroke="currentColor"
12+
strokeWidth="2"
13+
strokeLinecap="round"
14+
strokeLinejoin="round"
15+
{...props}
16+
>
317
<circle cx="12" cy="12" r="10" />
4-
<line x1="12" y1="8" x2="12" y2="12" />
5-
<line x1="12" y1="16" x2="12.01" y2="16" />
18+
<line x1="12" x2="12" y1="8" y2="12" />
19+
<line x1="12" x2="12.01" y1="16" y2="16" />
620
</svg>
721
)

src/renderer/src/components/icons/CheckCircle.icon.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
1-
export const CheckCircleIcon = () => (
2-
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1+
import React from 'react'
2+
3+
export const CheckCircleIcon: React.FC<React.SVGProps<SVGSVGElement>> = (
4+
props
5+
): React.JSX.Element => (
6+
<svg
7+
width="24"
8+
height="24"
9+
viewBox="0 0 24 24"
10+
fill="none"
11+
stroke="currentColor"
12+
strokeWidth="2"
13+
strokeLinecap="round"
14+
strokeLinejoin="round"
15+
{...props}
16+
>
317
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
418
<polyline points="22,4 12,14.01 9,11.01" />
519
</svg>
Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1-
export const DownloadIcon = () => (
2-
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1+
import React from 'react'
2+
3+
export const DownloadIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props): React.JSX.Element => (
4+
<svg
5+
width="24"
6+
height="24"
7+
viewBox="0 0 24 24"
8+
fill="none"
9+
stroke="currentColor"
10+
strokeWidth="2"
11+
strokeLinecap="round"
12+
strokeLinejoin="round"
13+
{...props}
14+
>
315
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
416
<polyline points="7,10 12,15 17,10" />
5-
<line x1="12" y1="15" x2="12" y2="3" />
17+
<line x1="12" x2="12" y1="15" y2="3" />
618
</svg>
719
)

0 commit comments

Comments
 (0)