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
@@ -0,0 +1,120 @@
import { useEffect, useState } from 'react';
import { PollIcon, PlusIcon, SpinnerIcon, TrashIcon } from './icons';
import { Modal } from './Modal';
interface Props {
open: boolean;
sending: boolean;
error: string | null;
onClose: () => void;
onSubmit: (question: string, options: string[]) => Promise<void>;
}
export function PollComposerDialog({ open, sending, error, onClose, onSubmit }: Props) {
const [question, setQuestion] = useState('');
const [options, setOptions] = useState(['', '']);
useEffect(() => {
if (!open) return;
setQuestion('');
setOptions(['', '']);
}, [open]);
const filledOptions = options.map((option) => option.trim()).filter(Boolean);
const canSubmit = question.trim().length > 0 && filledOptions.length >= 2 && !sending;
return (
<Modal open={open} title="Umfrage erstellen" onClose={onClose} size="md">
<form
className="flex flex-col gap-4"
onSubmit={(e) => {
e.preventDefault();
if (!canSubmit) return;
void onSubmit(question, options);
}}
>
<label className="flex flex-col gap-1.5 text-sm">
<span className="font-semibold text-fg">Frage</span>
<input
autoFocus
value={question}
onChange={(e) => setQuestion(e.target.value)}
maxLength={140}
placeholder="Worüber sollen wir abstimmen?"
className="rounded-lg border border-line bg-surface-2 px-3 py-2 text-sm text-fg outline-none transition placeholder:text-fg-muted focus:border-accent/60 focus:ring-2 focus:ring-accent/20"
/>
</label>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-fg">Antworten</p>
<button
type="button"
onClick={() => setOptions((prev) => [...prev, ''])}
disabled={options.length >= 10}
className="inline-flex h-8 w-8 cursor-pointer items-center justify-center rounded-lg text-fg-muted transition hover:bg-surface-2 hover:text-fg focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:cursor-not-allowed disabled:opacity-45"
aria-label="Antwort hinzufügen"
title="Antwort hinzufügen"
>
<PlusIcon className="h-4 w-4" />
</button>
</div>
{options.map((option, idx) => (
<div key={idx} className="flex items-center gap-2">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-accent/10 text-xs font-semibold text-accent">
{idx + 1}
</span>
<input
value={option}
onChange={(e) => {
const value = e.target.value;
setOptions((prev) =>
prev.map((item, itemIdx) => (itemIdx === idx ? value : item)),
);
}}
maxLength={80}
placeholder={'Antwort ' + (idx + 1)}
className="min-w-0 flex-1 rounded-lg border border-line bg-surface-2 px-3 py-2 text-sm text-fg outline-none transition placeholder:text-fg-muted focus:border-accent/60 focus:ring-2 focus:ring-accent/20"
/>
<button
type="button"
onClick={() => setOptions((prev) => prev.filter((_, itemIdx) => itemIdx !== idx))}
disabled={options.length <= 2}
aria-label="Antwort entfernen"
title="Antwort entfernen"
className="inline-flex h-9 w-9 cursor-pointer items-center justify-center rounded-lg text-fg-muted transition hover:bg-rose-500/15 hover:text-rose-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-rose-500/30 disabled:cursor-not-allowed disabled:opacity-35"
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
))}
</div>
{error && (
<p className="rounded-lg border border-rose-500/30 bg-rose-500/10 px-3 py-2 text-xs text-rose-600 dark:text-rose-200">
{error}
</p>
)}
<div className="flex justify-end gap-2 pt-1">
<button
type="button"
onClick={onClose}
className="cursor-pointer rounded-lg border border-line bg-surface-2 px-3 py-2 text-sm text-fg transition hover:bg-surface-3"
>
Abbrechen
</button>
<button
type="submit"
disabled={!canSubmit}
className="inline-flex cursor-pointer items-center gap-2 rounded-lg bg-accent px-3 py-2 text-sm font-semibold text-accent-fg transition hover:brightness-110 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 disabled:cursor-not-allowed disabled:opacity-45"
>
{sending ? <SpinnerIcon className="h-4 w-4" /> : <PollIcon className="h-4 w-4" />}
<span>Senden</span>
</button>
</div>
</form>
</Modal>
);
}