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>
116 lines
4.0 KiB
TypeScript
116 lines
4.0 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
import {
|
|
checkForUpdate,
|
|
IDLE_UPDATE_STATE,
|
|
installUpdate,
|
|
type UpdateState,
|
|
} from '../lib/appUpdates';
|
|
import { SparklesIcon, SpinnerIcon, XIcon } from './icons';
|
|
|
|
// Light-weight update UX: check once on mount, then every hour while the
|
|
// app is open. When an update exists show a toast with "Install & Restart"
|
|
// and a dismiss button. Dismiss is session-scoped — on next launch we check
|
|
// again.
|
|
export function UpdateToast() {
|
|
const { t } = useTranslation(['app']);
|
|
const [state, setState] = useState<UpdateState>(IDLE_UPDATE_STATE);
|
|
const [dismissed, setDismissed] = useState(false);
|
|
const [installing, setInstalling] = useState(false);
|
|
const [progressPct, setProgressPct] = useState<number | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
const run = async () => {
|
|
const next = await checkForUpdate();
|
|
if (!cancelled) setState(next);
|
|
};
|
|
void run();
|
|
const id = window.setInterval(run, 60 * 60 * 1000);
|
|
return () => {
|
|
cancelled = true;
|
|
window.clearInterval(id);
|
|
};
|
|
}, []);
|
|
|
|
if (!state.available || dismissed) return null;
|
|
|
|
const handleInstall = async () => {
|
|
setInstalling(true);
|
|
setProgressPct(0);
|
|
try {
|
|
await installUpdate((downloaded, total) => {
|
|
if (total && total > 0) setProgressPct((downloaded / total) * 100);
|
|
});
|
|
} catch (err: unknown) {
|
|
setState((s) => ({
|
|
...s,
|
|
error: err instanceof Error ? err.message : 'install failed',
|
|
}));
|
|
setInstalling(false);
|
|
setProgressPct(null);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div
|
|
role="status"
|
|
aria-live="polite"
|
|
className="fixed bottom-6 left-6 z-50 w-80 overflow-hidden rounded-2xl border border-line bg-surface-3 p-4 shadow-2xl"
|
|
>
|
|
<div className="flex items-start gap-3">
|
|
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-accent/20 text-accent">
|
|
<SparklesIcon className="h-4 w-4" />
|
|
</div>
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-sm font-semibold text-fg">
|
|
{t('app:update.available', { defaultValue: 'Update verfügbar' })}
|
|
{state.version ? ' · v' + state.version : ''}
|
|
</p>
|
|
{state.notes && (
|
|
<p className="mt-0.5 line-clamp-6 whitespace-pre-line text-xs text-fg-muted">
|
|
{state.notes}
|
|
</p>
|
|
)}
|
|
{state.error && (
|
|
<p className="mt-1 text-xs text-rose-500 dark:text-rose-300">{state.error}</p>
|
|
)}
|
|
</div>
|
|
{!installing && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setDismissed(true)}
|
|
aria-label={t('common:close', { defaultValue: 'Schließen' })}
|
|
className="inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md text-fg-muted transition hover:bg-surface-2 hover:text-fg"
|
|
>
|
|
<XIcon className="h-3.5 w-3.5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => void handleInstall()}
|
|
disabled={installing}
|
|
className="mt-3 inline-flex w-full cursor-pointer items-center justify-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/60 disabled:cursor-not-allowed disabled:opacity-60"
|
|
>
|
|
{installing ? (
|
|
<>
|
|
<SpinnerIcon className="h-3.5 w-3.5" />
|
|
<span>
|
|
{progressPct != null
|
|
? t('app:update.downloading', { defaultValue: 'Lade…' }) +
|
|
' ' +
|
|
Math.round(progressPct) +
|
|
'%'
|
|
: t('app:update.installing', { defaultValue: 'Installiere…' })}
|
|
</span>
|
|
</>
|
|
) : (
|
|
<span>{t('app:update.install', { defaultValue: 'Installieren & Neustarten' })}</span>
|
|
)}
|
|
</button>
|
|
</div>
|
|
);
|
|
}
|