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:
@@ -0,0 +1,120 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { fetchChangelog, type ChangelogEntry } from '../lib/changelog';
|
||||
import { SparklesIcon, SpinnerIcon } from '../components/icons';
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
export function ChangelogPage() {
|
||||
const [entries, setEntries] = useState<ChangelogEntry[] | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [visible, setVisible] = useState(PAGE_SIZE);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
void (async () => {
|
||||
try {
|
||||
const list = await fetchChangelog();
|
||||
if (!cancelled) setEntries(list);
|
||||
} catch (err: unknown) {
|
||||
if (!cancelled) {
|
||||
setError(err instanceof Error ? err.message : 'Konnte Changelog nicht laden.');
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-full bg-surface-3 text-fg">
|
||||
<div className="mx-auto flex max-w-3xl flex-col gap-6 px-6 py-8">
|
||||
<header className="mb-2 flex items-start gap-3">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-accent/15 text-accent">
|
||||
<SparklesIcon className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-display text-2xl font-semibold tracking-tight text-fg">
|
||||
Was ist neu
|
||||
</h1>
|
||||
<p className="mt-0.5 text-sm text-fg-muted">
|
||||
Alle Änderungen in dieser App, neueste zuerst.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{entries === null && !error && (
|
||||
<div className="flex items-center gap-2 rounded-xl border border-line bg-surface-2 p-6 text-sm text-fg-muted">
|
||||
<SpinnerIcon className="h-4 w-4 text-accent" />
|
||||
<span>Lade Changelog…</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="rounded-xl border border-rose-500/30 bg-rose-500/10 p-4 text-sm text-rose-600 dark:text-rose-200">
|
||||
<p className="font-semibold">Fehler beim Laden</p>
|
||||
<p className="mt-1 text-xs opacity-90">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{entries !== null && entries.length === 0 && !error && (
|
||||
<div className="rounded-xl border border-line bg-surface-2 p-8 text-center text-sm text-fg-muted">
|
||||
Noch keine Einträge vorhanden.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{entries && entries.length > 0 && (
|
||||
<ol className="flex flex-col gap-4">
|
||||
{entries.slice(0, visible).map((entry) => (
|
||||
<li
|
||||
key={entry.version}
|
||||
className="rounded-2xl border border-line bg-surface-2 p-5 shadow-sm"
|
||||
>
|
||||
<div className="flex flex-wrap items-baseline justify-between gap-3">
|
||||
<h2 className="font-display text-lg font-semibold tracking-tight text-fg">
|
||||
v{entry.version}
|
||||
</h2>
|
||||
<time
|
||||
dateTime={entry.pub_date}
|
||||
className="text-xs tabular-nums text-fg-muted"
|
||||
>
|
||||
{formatDate(entry.pub_date)}
|
||||
</time>
|
||||
</div>
|
||||
<p className="mt-3 whitespace-pre-line break-words text-sm leading-relaxed text-fg">
|
||||
{entry.notes}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
{visible < entries.length && (
|
||||
<li>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setVisible((n) => n + PAGE_SIZE)}
|
||||
className="inline-flex w-full cursor-pointer items-center justify-center rounded-lg border border-line bg-surface-2 px-3 py-2.5 text-sm font-semibold text-fg transition hover:bg-surface-3 focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/40"
|
||||
>
|
||||
Mehr laden ({entries.length - visible} verbleibend)
|
||||
</button>
|
||||
</li>
|
||||
)}
|
||||
</ol>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return iso;
|
||||
return d.toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user