1303c8e26f
- backup/restore dialog + user profile popover components - image compression, video blur, wake lock utilities - message cache + conversation messages hook refinements - call context, active speakers, screen share dialog tweaks - audio + screen share settings persistence - refreshed app icons (smaller sizes) across all platforms
263 lines
9.8 KiB
TypeScript
263 lines
9.8 KiB
TypeScript
import { useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import {
|
|
type DisplaySurfaceHint,
|
|
getPresetParams,
|
|
getScreenShareSettings,
|
|
PRESET_ORDER,
|
|
type ScreenSharePreset,
|
|
updateScreenShareSettings,
|
|
} from '../lib/screenShareSettings';
|
|
import {
|
|
MonitorShareIcon,
|
|
SpinnerIcon,
|
|
XIcon,
|
|
} from './icons';
|
|
|
|
interface Props {
|
|
open: boolean;
|
|
onClose: () => void;
|
|
onStart: (opts: {
|
|
preset: ScreenSharePreset;
|
|
displaySurface: DisplaySurfaceHint;
|
|
framerate: number | null;
|
|
}) => Promise<void>;
|
|
}
|
|
|
|
const FRAMERATE_OPTIONS: ReadonlyArray<{ value: number | null; label: string }> = [
|
|
{ value: null, label: 'Preset-Standard' },
|
|
{ value: 15, label: '15 fps' },
|
|
{ value: 30, label: '30 fps' },
|
|
{ value: 60, label: '60 fps' },
|
|
];
|
|
|
|
// Discord-style pre-share dialog. The OS still owns the final source picker
|
|
// (browser/OS limitation — only Chrome/Edge plus a native plugin can enumerate
|
|
// windows from JS), but we pre-filter with the `displaySurface` hint and lock
|
|
// in quality + framerate up-front so the user doesn't have to re-open the
|
|
// system picker to adjust them mid-call.
|
|
export function ScreenShareDialog({ open, onClose, onStart }: Props) {
|
|
const { t } = useTranslation(['app']);
|
|
const initial = getScreenShareSettings();
|
|
const [surface, setSurface] = useState<DisplaySurfaceHint>(initial.displaySurface);
|
|
const [preset, setPreset] = useState<ScreenSharePreset>(initial.preset);
|
|
const [framerate, setFramerate] = useState<number | null>(initial.framerateOverride);
|
|
const [includeAudio, setIncludeAudio] = useState<boolean>(initial.includeSystemAudio);
|
|
const [busy, setBusy] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
if (!open) return null;
|
|
|
|
async function handleStart() {
|
|
setBusy(true);
|
|
setError(null);
|
|
try {
|
|
// Persist the audio choice alongside the other picker prefs so the
|
|
// upstream startScreenShare picks it up on its settings read.
|
|
updateScreenShareSettings({ includeSystemAudio: includeAudio });
|
|
await onStart({ preset, displaySurface: surface, framerate });
|
|
onClose();
|
|
} catch (err: unknown) {
|
|
setError(err instanceof Error ? err.message : 'screenshare failed');
|
|
} finally {
|
|
setBusy(false);
|
|
}
|
|
}
|
|
|
|
const presetParams = getPresetParams(preset);
|
|
const effectiveFps = framerate ?? presetParams.framerate;
|
|
|
|
return (
|
|
<div
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label={t('app:call.share_dialog_title', { defaultValue: 'Bildschirm teilen' })}
|
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4"
|
|
onClick={onClose}
|
|
>
|
|
<div
|
|
onClick={(e) => e.stopPropagation()}
|
|
className="flex w-full max-w-lg flex-col overflow-hidden rounded-2xl border border-line bg-surface-3 shadow-xl"
|
|
>
|
|
<header className="flex items-center justify-between border-b border-line px-5 py-3">
|
|
<div className="flex items-center gap-2">
|
|
<MonitorShareIcon className="h-4 w-4 text-accent" />
|
|
<h3 className="font-display text-sm font-semibold text-fg">
|
|
{t('app:call.share_dialog_title', { defaultValue: 'Bildschirm teilen' })}
|
|
</h3>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
aria-label="Close"
|
|
className="flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg text-fg-muted transition hover:bg-surface-2 hover:text-fg"
|
|
>
|
|
<XIcon className="h-4 w-4" />
|
|
</button>
|
|
</header>
|
|
|
|
<div className="space-y-5 p-5">
|
|
<div>
|
|
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-fg-muted">
|
|
{t('app:call.share_surface', { defaultValue: 'Quelle' })}
|
|
</p>
|
|
<div className="grid grid-cols-3 gap-2">
|
|
<SurfaceOption
|
|
active={surface === null}
|
|
onClick={() => setSurface(null)}
|
|
label={t('app:call.share_any', { defaultValue: 'Alle anzeigen' })}
|
|
sub={t('app:call.share_any_sub', { defaultValue: 'Bildschirm + Fenster' })}
|
|
/>
|
|
<SurfaceOption
|
|
active={surface === 'monitor'}
|
|
onClick={() => setSurface('monitor')}
|
|
label={t('app:call.share_monitor', { defaultValue: 'Bildschirm' })}
|
|
sub={t('app:call.share_monitor_sub', { defaultValue: 'Ganzer Monitor' })}
|
|
/>
|
|
<SurfaceOption
|
|
active={surface === 'window'}
|
|
onClick={() => setSurface('window')}
|
|
label={t('app:call.share_window', { defaultValue: 'Fenster' })}
|
|
sub={t('app:call.share_window_sub', { defaultValue: 'Einzelnes Fenster' })}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-fg-muted">
|
|
{t('app:call.share_quality', { defaultValue: 'Qualität' })}
|
|
</p>
|
|
<select
|
|
value={preset}
|
|
onChange={(e) => setPreset(e.target.value as ScreenSharePreset)}
|
|
className="w-full rounded-lg border border-line bg-surface-2 px-3 py-2 text-sm text-fg focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30"
|
|
>
|
|
{PRESET_ORDER.map((p) => (
|
|
<option key={p} value={p}>
|
|
{getPresetParams(p).label} · ≤ {getPresetParams(p).bitrateKbps} kbps
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-fg-muted">
|
|
{t('app:call.share_fps', { defaultValue: 'Bildrate' })}
|
|
</p>
|
|
<div className="flex flex-wrap gap-2">
|
|
{FRAMERATE_OPTIONS.map((opt) => (
|
|
<button
|
|
key={String(opt.value)}
|
|
type="button"
|
|
onClick={() => setFramerate(opt.value)}
|
|
className={
|
|
'cursor-pointer rounded-lg border px-3 py-1.5 text-xs font-medium transition ' +
|
|
(framerate === opt.value
|
|
? 'border-accent bg-accent/15 text-accent'
|
|
: 'border-line bg-surface-2 text-fg hover:bg-surface')
|
|
}
|
|
>
|
|
{opt.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<p className="mt-2 text-[11px] text-fg-muted">
|
|
{t('app:call.share_fps_effective', {
|
|
defaultValue: 'Effektiv: {{fps}} fps',
|
|
fps: effectiveFps,
|
|
})}
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="flex cursor-pointer items-start gap-2 rounded-lg border border-line bg-surface-2 px-3 py-2 text-xs hover:bg-surface">
|
|
<input
|
|
type="checkbox"
|
|
checked={includeAudio}
|
|
onChange={(e) => setIncludeAudio(e.target.checked)}
|
|
className="mt-0.5 accent-accent"
|
|
/>
|
|
<span className="min-w-0 flex-1">
|
|
<span className="block font-semibold text-fg">
|
|
{t('app:call.share_system_audio', {
|
|
defaultValue: 'System-Sound mit übertragen',
|
|
})}
|
|
</span>
|
|
<span className="mt-0.5 block text-[11px] text-fg-muted">
|
|
{t('app:call.share_system_audio_hint', {
|
|
defaultValue:
|
|
'"Go Live" — Systemsound wird mitgesendet. Auf macOS braucht das extra Berechtigungen; wird sonst stumm geteilt.',
|
|
})}
|
|
</span>
|
|
</span>
|
|
</label>
|
|
</div>
|
|
|
|
{error && (
|
|
<p role="alert" className="text-sm text-rose-600 dark:text-rose-300">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
<p className="text-[11px] text-fg-muted">
|
|
{t('app:call.share_hint', {
|
|
defaultValue:
|
|
'Nach "Teilen starten" öffnet das Betriebssystem den Quellen-Picker. Qualität + Bildrate werden bereits angewendet.',
|
|
})}
|
|
</p>
|
|
</div>
|
|
|
|
<footer className="flex items-center justify-end gap-2 border-t border-line bg-surface-2 px-5 py-3">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="cursor-pointer rounded-md border border-line bg-surface-3 px-3 py-2 text-sm text-fg transition hover:bg-surface-2"
|
|
>
|
|
{t('app:friends.action_cancel', { defaultValue: 'Abbrechen' })}
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => void handleStart()}
|
|
disabled={busy}
|
|
className="inline-flex cursor-pointer items-center gap-1.5 rounded-md bg-accent px-3 py-2 text-sm font-semibold text-accent-fg transition hover:brightness-110 disabled:cursor-not-allowed disabled:opacity-60"
|
|
>
|
|
{busy && <SpinnerIcon className="h-4 w-4" />}
|
|
<span>
|
|
{t('app:call.share_start', { defaultValue: 'Teilen starten' })}
|
|
</span>
|
|
</button>
|
|
</footer>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function SurfaceOption({
|
|
active,
|
|
onClick,
|
|
label,
|
|
sub,
|
|
}: {
|
|
active: boolean;
|
|
onClick: () => void;
|
|
label: string;
|
|
sub: string;
|
|
}) {
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onClick}
|
|
className={
|
|
'flex flex-col items-start gap-0.5 rounded-lg border p-2.5 text-left transition ' +
|
|
(active
|
|
? 'border-accent bg-accent/10 text-fg'
|
|
: 'border-line bg-surface-2 text-fg hover:bg-surface')
|
|
}
|
|
>
|
|
<span className="text-xs font-semibold">{label}</span>
|
|
<span className="text-[10px] text-fg-muted">{sub}</span>
|
|
</button>
|
|
);
|
|
}
|