825160ee46
Chronological port of every Tauri release commit (v0.11.4 -> v0.15.2)
into the Electron rebuild, plus Discord-style audio handling that goes
beyond the Tauri original.
Highlights:
- All 17 Tauri release commits ported (audio fixes, custom notification
sound, Discord-style chat UX, profile banner, changelog page,
Discord-parity call UX, screen-share echo + re-watch UX, audio-loop
fix).
- Native napi-rs audio-loopback addon with WASAPI process-loopback:
* EXCLUDE_TARGET_PROCESS_TREE for full-screen shares -> peers
never hear themselves echoed back through the capture.
* INCLUDE_TARGET_PROCESS_TREE for window shares -> only the
picked window's audio is captured, not the whole OS mixer
(Discord parity).
* HWND -> PID resolution via Win32 GetWindowThreadProcessId.
- Discord-style screen-source picker (thumbnail grid, screens vs
apps tabs, live-refreshing thumbnails).
- Hash routing fix for packaged builds (file:// can't resolve
BrowserRouter paths).
- Tauri sources removed (apps/desktop/src-tauri).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
255 lines
7.4 KiB
TypeScript
255 lines
7.4 KiB
TypeScript
import { useTranslation } from 'react-i18next';
|
|
|
|
import {
|
|
CaptionsIcon,
|
|
HeadphonesIcon,
|
|
HeadphonesOffIcon,
|
|
MicIcon,
|
|
MicOffIcon,
|
|
MonitorShareIcon,
|
|
MonitorStopIcon,
|
|
MusicIcon,
|
|
PhoneOffIcon,
|
|
UsersIcon,
|
|
VideoIcon,
|
|
} from './icons';
|
|
|
|
interface Props {
|
|
muted: boolean;
|
|
sharing: boolean;
|
|
video: boolean;
|
|
deafened: boolean;
|
|
onToggleMute: () => void;
|
|
onToggleShare: () => void;
|
|
/** Right-click on the share button opens the quality picker dialog while
|
|
* left-click just starts with last-used settings. Optional so pages that
|
|
* don't need the advanced path (mobile, etc.) can skip it. */
|
|
onShareContextMenu?: (e: React.MouseEvent) => void;
|
|
onToggleVideo?: () => void;
|
|
onToggleDeafen: () => void;
|
|
onHangup: () => void;
|
|
onOpenParticipants?: () => void;
|
|
/** Toggle the in-call soundboard popover. Active = panel currently open. */
|
|
onToggleSoundboard?: () => void;
|
|
soundboardOpen?: boolean;
|
|
/** Discord-style live-captions toggle. Optional — pages that don't support
|
|
* SpeechRecognition (Firefox) skip the prop and the button is hidden. */
|
|
onToggleCaptions?: () => void;
|
|
captionsOn?: boolean;
|
|
participantsOpen?: boolean;
|
|
/** Compact variant used inside the docked call (36px buttons). */
|
|
compact?: boolean;
|
|
/** Glass variant used when controls float on fullscreen cinema mode. */
|
|
glass?: boolean;
|
|
/** Disable transient interactions while the call isn't fully connected. */
|
|
disabledMedia?: boolean;
|
|
}
|
|
|
|
export function CallControls({
|
|
muted,
|
|
sharing,
|
|
video,
|
|
deafened,
|
|
onToggleMute,
|
|
onToggleShare,
|
|
onShareContextMenu,
|
|
onToggleVideo,
|
|
onToggleDeafen,
|
|
onHangup,
|
|
onOpenParticipants,
|
|
onToggleSoundboard,
|
|
soundboardOpen = false,
|
|
onToggleCaptions,
|
|
captionsOn = false,
|
|
participantsOpen = false,
|
|
compact = false,
|
|
glass = false,
|
|
disabledMedia = false,
|
|
}: Props) {
|
|
const { t } = useTranslation(['app']);
|
|
const wrapBase = glass
|
|
? 'glass-pill flex items-center justify-center gap-2 rounded-[18px] p-2.5'
|
|
: 'flex items-center justify-center gap-2 border-t border-line bg-surface-3 p-3';
|
|
const btnSize = compact ? 'h-9 w-9 rounded-[10px]' : 'h-12 w-12 rounded-[14px]';
|
|
const hangupSize = compact ? 'h-9 w-[54px] rounded-[10px]' : 'h-12 w-[72px] rounded-[14px]';
|
|
return (
|
|
<div className={wrapBase}>
|
|
<CallButton
|
|
label={muted ? t('app:call.unmute') : t('app:call.mute')}
|
|
active={muted}
|
|
activeTone="danger"
|
|
onClick={onToggleMute}
|
|
disabled={disabledMedia}
|
|
glass={glass}
|
|
className={btnSize}
|
|
>
|
|
{muted ? <MicOffIcon className="h-5 w-5" /> : <MicIcon className="h-5 w-5" />}
|
|
</CallButton>
|
|
<CallButton
|
|
label={
|
|
deafened
|
|
? t('app:call.undeafen', { defaultValue: 'Ton wieder aktiv' })
|
|
: t('app:call.deafen', { defaultValue: 'Alle stumm' })
|
|
}
|
|
active={deafened}
|
|
activeTone="danger"
|
|
onClick={onToggleDeafen}
|
|
glass={glass}
|
|
className={btnSize}
|
|
>
|
|
{deafened ? (
|
|
<HeadphonesOffIcon className="h-5 w-5" />
|
|
) : (
|
|
<HeadphonesIcon className="h-5 w-5" />
|
|
)}
|
|
</CallButton>
|
|
{onToggleVideo && (
|
|
<CallButton
|
|
label={t('app:call.start_video', { defaultValue: 'Video' })}
|
|
active={video}
|
|
activeTone="accent"
|
|
onClick={onToggleVideo}
|
|
disabled={disabledMedia}
|
|
glass={glass}
|
|
className={btnSize}
|
|
>
|
|
<VideoIcon className="h-5 w-5" />
|
|
</CallButton>
|
|
)}
|
|
<CallButton
|
|
label={
|
|
sharing
|
|
? t('app:call.stop_share_screen', { defaultValue: 'Screen-Share stoppen' })
|
|
: t('app:call.share_screen', { defaultValue: 'Bildschirm teilen' })
|
|
}
|
|
active={sharing}
|
|
activeTone="accent"
|
|
onClick={onToggleShare}
|
|
{...(onShareContextMenu ? { onContextMenu: onShareContextMenu } : {})}
|
|
disabled={disabledMedia}
|
|
glass={glass}
|
|
className={btnSize}
|
|
>
|
|
{sharing ? (
|
|
<MonitorStopIcon className="h-5 w-5" />
|
|
) : (
|
|
<MonitorShareIcon className="h-5 w-5" />
|
|
)}
|
|
</CallButton>
|
|
{onToggleSoundboard && (
|
|
<CallButton
|
|
label={t('app:soundboard.toggle', { defaultValue: 'Soundboard' })}
|
|
active={soundboardOpen}
|
|
activeTone="accent"
|
|
onClick={onToggleSoundboard}
|
|
glass={glass}
|
|
className={btnSize}
|
|
>
|
|
<MusicIcon className="h-5 w-5" />
|
|
</CallButton>
|
|
)}
|
|
{onToggleCaptions && (
|
|
<CallButton
|
|
label={
|
|
captionsOn
|
|
? t('app:call.captions_off', { defaultValue: 'Untertitel aus' })
|
|
: t('app:call.captions_on', { defaultValue: 'Untertitel an' })
|
|
}
|
|
active={captionsOn}
|
|
activeTone="accent"
|
|
onClick={onToggleCaptions}
|
|
glass={glass}
|
|
className={btnSize}
|
|
>
|
|
<CaptionsIcon className="h-5 w-5" />
|
|
</CallButton>
|
|
)}
|
|
{onOpenParticipants && (
|
|
<CallButton
|
|
label={t('app:call.participants', { defaultValue: 'Teilnehmer' })}
|
|
onClick={onOpenParticipants}
|
|
active={participantsOpen}
|
|
activeTone="accent"
|
|
dataTrigger="participants"
|
|
glass={glass}
|
|
className={btnSize}
|
|
>
|
|
<UsersIcon className="h-5 w-5" />
|
|
</CallButton>
|
|
)}
|
|
<CallButton
|
|
label={t('app:call.hangup')}
|
|
onClick={onHangup}
|
|
tone="danger"
|
|
glass={glass}
|
|
className={hangupSize}
|
|
>
|
|
<PhoneOffIcon className="h-5 w-5" />
|
|
</CallButton>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface CallButtonProps {
|
|
label: string;
|
|
onClick: () => void;
|
|
onContextMenu?: (e: React.MouseEvent) => void;
|
|
disabled?: boolean;
|
|
active?: boolean;
|
|
activeTone?: 'accent' | 'danger';
|
|
tone?: 'default' | 'danger';
|
|
glass?: boolean;
|
|
className?: string;
|
|
/** Stable trigger id so portals (popovers) can skip outside-click dismiss
|
|
* when the user is toggling their own trigger. */
|
|
dataTrigger?: string;
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
function CallButton({
|
|
label,
|
|
onClick,
|
|
onContextMenu,
|
|
disabled,
|
|
active,
|
|
activeTone = 'accent',
|
|
tone = 'default',
|
|
glass = false,
|
|
className = '',
|
|
dataTrigger,
|
|
children,
|
|
}: CallButtonProps) {
|
|
const base =
|
|
'inline-flex cursor-pointer items-center justify-center border transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 disabled:cursor-not-allowed disabled:opacity-50';
|
|
|
|
let toneClass: string;
|
|
if (tone === 'danger') {
|
|
toneClass = 'bg-rose-600 text-white border-rose-600 hover:bg-rose-500';
|
|
} else if (active && activeTone === 'danger') {
|
|
toneClass = 'bg-rose-600 text-white border-rose-600 hover:bg-rose-500';
|
|
} else if (active) {
|
|
toneClass = 'bg-accent text-accent-fg border-accent hover:brightness-110';
|
|
} else if (glass) {
|
|
toneClass =
|
|
'border-white/15 bg-white/10 text-white hover:bg-white/15';
|
|
} else {
|
|
toneClass = 'border-line bg-surface-2 text-fg hover:bg-surface';
|
|
}
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={onClick}
|
|
onContextMenu={onContextMenu}
|
|
disabled={disabled}
|
|
aria-label={label}
|
|
aria-pressed={active}
|
|
title={label}
|
|
className={`${base} ${toneClass} ${className}`}
|
|
{...(dataTrigger ? { [`data-${dataTrigger}-trigger`]: 'true' } : {})}
|
|
>
|
|
{children}
|
|
</button>
|
|
);
|
|
}
|