feat(desktop): port v0.11.4-v0.15.2 from Tauri to Electron + Discord-parity audio (v0.16.0)

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>
This commit is contained in:
byGalax
2026-05-06 23:35:01 +02:00
parent 72d02e385f
commit 825160ee46
504 changed files with 14217 additions and 14366 deletions
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import {
@@ -6,26 +6,52 @@ import {
setParticipantVolume,
subscribeParticipantVolumes,
} from '../lib/participantVolumes';
import {
AtIcon,
MicIcon,
MicOffIcon,
PinIcon,
PinOffIcon,
} from './icons';
interface Props {
userId: string;
displayName: string;
x: number;
y: number;
/** Discord-style pin toggle row at the top of the menu. When undefined,
* the row is hidden — used for tiles that don't make sense to pin. */
pinned?: boolean;
onTogglePin?: () => void;
/** Hide the volume slider — for self-tiles where the slider would adjust
* the local user's own playback gain (which we don't expose). Defaults to
* true so existing call sites keep their behaviour. */
renderVolume?: boolean;
/** Discord-parity: open this user's profile card from the menu. When
* undefined the row is hidden (e.g. for self-tiles where it'd just open
* your own profile). */
onShowProfile?: () => void;
onClose: () => void;
}
const MENU_W = 240;
const MENU_H = 96;
const MENU_H = 138;
export function ParticipantVolumeMenu({
userId,
displayName,
x,
y,
pinned,
onTogglePin,
renderVolume = true,
onShowProfile,
onClose,
}: Props) {
const [volume, setVolume] = useState<number>(() => getParticipantVolume(userId));
// Remembers the volume before "Für mich stummschalten" so unmute restores
// it instead of snapping back to 100%.
const preMuteRef = useRef<number | null>(volume > 0 ? volume : null);
// Re-sync from store in case another menu instance changed the same user.
useEffect(() => subscribeParticipantVolumes(() => {
@@ -63,35 +89,99 @@ export function ParticipantVolumeMenu({
>
<div className="mb-2 flex items-center justify-between gap-2 text-xs">
<span className="truncate font-semibold text-fg">{displayName}</span>
<span
{renderVolume && (
<span
className={
'tabular-nums ' + (volume > 1 ? 'text-amber-500' : 'text-fg-muted')
}
>
{Math.round(volume * 100)}%
</span>
)}
</div>
{onShowProfile && (
<button
type="button"
onClick={() => {
onShowProfile();
onClose();
}}
className="mb-1.5 flex w-full cursor-pointer items-center gap-2 rounded-md border border-line bg-surface-3 px-2.5 py-1.5 text-xs font-medium text-fg transition hover:border-accent hover:bg-accent/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
>
<AtIcon className="h-3.5 w-3.5" />
<span>Profil anzeigen</span>
</button>
)}
{renderVolume && (
<button
type="button"
onClick={() => {
// Discord-parity "Mute for me": toggle local playback gain.
// Storing the pre-mute level lets unmute restore the user's
// chosen volume (e.g. 130% → 0 → 130%) instead of snapping to 100.
if (volume === 0) {
const restored = preMuteRef.current ?? 1;
setVolume(restored);
setParticipantVolume(userId, restored);
} else {
preMuteRef.current = volume;
setVolume(0);
setParticipantVolume(userId, 0);
}
}}
className={
'tabular-nums ' + (volume > 1 ? 'text-amber-500' : 'text-fg-muted')
'mb-1.5 flex w-full cursor-pointer items-center gap-2 rounded-md border px-2.5 py-1.5 text-xs font-medium transition focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 ' +
(volume === 0
? 'border-rose-500/40 bg-rose-500/15 text-rose-600 hover:bg-rose-500/20 dark:text-rose-300'
: 'border-line bg-surface-3 text-fg hover:border-accent hover:bg-accent/10')
}
>
{Math.round(volume * 100)}%
</span>
</div>
{/* Up to 200% via WebAudio gain. Values above 100% can clip on loud
mics — the amber count-up hints at that without a verbose warning. */}
<input
type="range"
min={0}
max={2}
step={0.01}
value={volume}
onChange={(e) => {
const v = Number(e.target.value);
setVolume(v);
setParticipantVolume(userId, v);
}}
aria-label={'Lautstärke ' + displayName}
className="w-full accent-accent"
/>
<div className="mt-1 flex justify-between text-[10px] text-fg-muted">
<span>0%</span>
<span className="tabular-nums">100%</span>
<span>200%</span>
</div>
{volume === 0 ? (
<MicIcon className="h-3.5 w-3.5" />
) : (
<MicOffIcon className="h-3.5 w-3.5" />
)}
<span>{volume === 0 ? 'Lokalen Ton aktivieren' : 'Für mich stummschalten'}</span>
</button>
)}
{onTogglePin && (
<button
type="button"
onClick={() => {
onTogglePin();
onClose();
}}
className="mb-2 flex w-full cursor-pointer items-center gap-2 rounded-md border border-line bg-surface-3 px-2.5 py-1.5 text-xs font-medium text-fg transition hover:border-accent hover:bg-accent/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
>
{pinned ? <PinOffIcon className="h-3.5 w-3.5" /> : <PinIcon className="h-3.5 w-3.5" />}
<span>{pinned ? 'Anpinnen aufheben' : 'Anpinnen'}</span>
</button>
)}
{renderVolume && (
<>
{/* Up to 200% via WebAudio gain. Values above 100% can clip on loud
mics — the amber count-up hints at that without a verbose warning. */}
<input
type="range"
min={0}
max={2}
step={0.01}
value={volume}
onChange={(e) => {
const v = Number(e.target.value);
setVolume(v);
setParticipantVolume(userId, v);
}}
aria-label={'Lautstärke ' + displayName}
className="w-full accent-accent"
/>
<div className="mt-1 flex justify-between text-[10px] text-fg-muted">
<span>0%</span>
<span className="tabular-nums">100%</span>
<span>200%</span>
</div>
</>
)}
</div>,
document.body,
);