All checks were successful
Build and Release / build-and-release (push) Successful in 14m19s
249 lines
11 KiB
JavaScript
249 lines
11 KiB
JavaScript
import React, { useState, useEffect } from 'react';
|
|
|
|
const ScreenShareModal = ({ onClose, onSelectSource }) => {
|
|
const [activeTab, setActiveTab] = useState('applications'); // applications | screens | devices
|
|
const [sources, setSources] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [shareAudio, setShareAudio] = useState(true);
|
|
|
|
useEffect(() => {
|
|
loadSources();
|
|
}, []);
|
|
|
|
const loadSources = async () => {
|
|
setLoading(true);
|
|
try {
|
|
// Get screen/window sources from Electron
|
|
const desktopSources = await window.cryptoAPI.getScreenSources();
|
|
|
|
// Get video input devices (webcams)
|
|
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
const videoDevices = devices.filter(d => d.kind === 'videoinput');
|
|
|
|
// Categorize
|
|
const apps = desktopSources.filter(s => s.id.startsWith('window'));
|
|
const screens = desktopSources.filter(s => s.id.startsWith('screen'));
|
|
|
|
const formattedDevices = videoDevices.map(d => ({
|
|
id: d.deviceId,
|
|
name: d.label || `Camera ${d.deviceId.substring(0, 4)}...`,
|
|
isDevice: true,
|
|
thumbnail: null // Devices don't have static thumbnails easily referencable without opening stream
|
|
}));
|
|
|
|
setSources({
|
|
applications: apps,
|
|
screens: screens,
|
|
devices: formattedDevices
|
|
});
|
|
} catch (err) {
|
|
console.error("Failed to load sources:", err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSelect = (source) => {
|
|
// If device, pass constraints differently (webcams don't have loopback audio)
|
|
if (source.isDevice) {
|
|
onSelectSource({ deviceId: source.id, type: 'device', shareAudio: false });
|
|
} else {
|
|
onSelectSource({ sourceId: source.id, type: 'screen', shareAudio });
|
|
}
|
|
onClose();
|
|
};
|
|
|
|
const renderGrid = (items) => {
|
|
if (!items || items.length === 0) return <div style={{ color: '#b9bbbe', padding: 20, textAlign: 'center' }}>No sources found.</div>;
|
|
|
|
return (
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '16px', padding: '16px' }}>
|
|
{items.map(item => (
|
|
<div
|
|
key={item.id}
|
|
onClick={() => handleSelect(item)}
|
|
style={{
|
|
cursor: 'pointer',
|
|
borderRadius: '4px',
|
|
padding: '8px',
|
|
}}
|
|
>
|
|
<div style={{ position: 'relative', width: '100%', height: '250px', width: '450px', borderRadius: '8px', overflow: 'hidden', marginBottom: '8px' }}>
|
|
{/* Thumbnail/Placeholder */}
|
|
{item.thumbnail ? (
|
|
<img src={item.thumbnail} alt={item.name} style={{ width: '100%', height: '100%', borderRadius: '8px', objectFit: 'cover', display: 'block' }} />
|
|
) : (
|
|
<div style={{ width: '100%', height: '100%', backgroundColor: '#202225', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#b9bbbe' }}>
|
|
📷
|
|
</div>
|
|
)}
|
|
|
|
{/* Hover Overlay */}
|
|
<div
|
|
className="share-overlay"
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100%',
|
|
height: '100%',
|
|
backgroundColor: 'rgba(0,0,0,0.5)',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
opacity: 0,
|
|
transition: 'opacity 0.2s'
|
|
}}
|
|
onMouseEnter={(e) => e.currentTarget.style.opacity = 1}
|
|
onMouseLeave={(e) => e.currentTarget.style.opacity = 0}
|
|
>
|
|
<button style={{
|
|
backgroundColor: '#5865F2',
|
|
color: 'white',
|
|
border: 'none',
|
|
padding: '8px 16px',
|
|
borderRadius: '4px',
|
|
fontWeight: 'bold',
|
|
cursor: 'pointer',
|
|
fontSize: '14px',
|
|
boxShadow: '0 4px 6px rgba(0,0,0,0.3)'
|
|
}}>
|
|
Share Screen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', alignItems: 'center', marginTop: '4px', maxWidth: '450px' }}>
|
|
{item.appIcon && (
|
|
<img src={item.appIcon} alt="" style={{ width: '20px', height: '20px', marginRight: '8px' }} />
|
|
)}
|
|
<div style={{ color: '#dcddde', fontSize: '14px', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', fontWeight: '500' }}>
|
|
{item.name}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
return (
|
|
<div style={{
|
|
position: 'fixed',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100%',
|
|
height: '100%',
|
|
backgroundColor: 'rgba(0,0,0,0.7)',
|
|
zIndex: 10000,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center'
|
|
}} onClick={onClose}>
|
|
<style>{`
|
|
.no-scrollbar::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
.no-scrollbar {
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
}
|
|
`}</style>
|
|
<div style={{
|
|
backgroundColor: '#242429',
|
|
borderRadius: '8px',
|
|
width: '965px',
|
|
height: '740px',
|
|
maxWidth: '95vw',
|
|
maxHeight: '95vh',
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
boxShadow: '0 8px 16px rgba(0,0,0,0.24)'
|
|
}} onClick={e => e.stopPropagation()}>
|
|
|
|
{/* Header/Tabs */}
|
|
<div style={{ display: 'flex', borderBottom: '1px solid #2f3136', padding: '16px 16px 0 16px', flexShrink: 0 }}>
|
|
<div
|
|
onClick={() => setActiveTab('applications')}
|
|
style={{
|
|
padding: '8px 16px',
|
|
cursor: 'pointer',
|
|
borderBottom: activeTab === 'applications' ? '2px solid white' : '2px solid transparent',
|
|
color: activeTab === 'applications' ? 'white' : '#b9bbbe',
|
|
fontWeight: '500'
|
|
}}
|
|
>
|
|
Applications
|
|
</div>
|
|
<div
|
|
onClick={() => setActiveTab('screens')}
|
|
style={{
|
|
padding: '8px 16px',
|
|
cursor: 'pointer',
|
|
borderBottom: activeTab === 'screens' ? '2px solid white' : '2px solid transparent',
|
|
color: activeTab === 'screens' ? 'white' : '#b9bbbe',
|
|
fontWeight: '500'
|
|
}}
|
|
>
|
|
Screens
|
|
</div>
|
|
<div
|
|
onClick={() => setActiveTab('devices')}
|
|
style={{
|
|
padding: '8px 16px',
|
|
cursor: 'pointer',
|
|
borderBottom: activeTab === 'devices' ? '2px solid white' : '2px solid transparent',
|
|
color: activeTab === 'devices' ? 'white' : '#b9bbbe',
|
|
fontWeight: '500'
|
|
}}
|
|
>
|
|
Devices
|
|
</div>
|
|
</div>
|
|
|
|
{/* Content */}
|
|
<div className="no-scrollbar" style={{ flex: 1, overflowY: 'auto' }}>
|
|
{loading ? (
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: '#b9bbbe' }}>
|
|
Loading sources...
|
|
</div>
|
|
) : (
|
|
renderGrid(sources[activeTab])
|
|
)}
|
|
</div>
|
|
|
|
{/* Audio sharing footer — hidden for device sources (webcams) */}
|
|
{activeTab !== 'devices' && (
|
|
<div style={{
|
|
borderTop: '1px solid #2f3136',
|
|
padding: '12px 16px',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
flexShrink: 0,
|
|
}}>
|
|
<label style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
cursor: 'pointer',
|
|
color: '#dcddde',
|
|
fontSize: '14px',
|
|
userSelect: 'none',
|
|
}}>
|
|
<input
|
|
type="checkbox"
|
|
checked={shareAudio}
|
|
onChange={(e) => setShareAudio(e.target.checked)}
|
|
style={{ accentColor: '#5865F2', width: '16px', height: '16px', cursor: 'pointer' }}
|
|
/>
|
|
Also share computer audio
|
|
</label>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default ScreenShareModal;
|