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,135 @@
// IndexedDB-backed custom notification sound. Single-row object store —
// user either has one uploaded clip overriding the built-in two-tone
// chime, or doesn't and we fall back to the synthesized tone.
//
// Separate from `soundboardStorage.ts` because the soundboard is a
// multi-entry user library with categories + hotkeys + per-clip gain;
// this module only needs "one blob, replace-on-upload, wipe-on-reset".
export interface NotificationSoundEntry {
filename: string;
mime: string;
size: number;
uploadedAt: number;
}
interface StoredNotificationSound extends NotificationSoundEntry {
blob: Blob;
}
// 1 MB cap — notification sounds should be short (<5s), and we don't
// want an IDB quota bust from a 200MB MP3.
export const MAX_NOTIFICATION_SOUND_BYTES = 1 * 1024 * 1024;
// --- Change observer — the notificationSound module subscribes to
// invalidate its cached AudioBuffer when upload/reset happens, so the
// next ping uses the fresh sound without an app restart.
type Listener = () => void;
const listeners = new Set<Listener>();
export function subscribeNotificationSoundChanges(l: Listener): () => void {
listeners.add(l);
return () => {
listeners.delete(l);
};
}
function notifyChange(): void {
for (const l of listeners) {
try {
l();
} catch (err: unknown) {
console.warn('notification-sound change listener threw', err);
}
}
}
const DB_NAME = 'netralax-notification';
const DB_VERSION = 1;
const STORE = 'sound';
const KEY = 'current';
let dbPromise: Promise<IDBDatabase> | null = null;
function openDb(): Promise<IDBDatabase> {
if (dbPromise) return dbPromise;
dbPromise = new Promise<IDBDatabase>((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION);
req.onupgradeneeded = () => {
const db = req.result;
if (!db.objectStoreNames.contains(STORE)) {
db.createObjectStore(STORE);
}
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => reject(req.error ?? new Error('indexedDB open failed'));
});
return dbPromise;
}
export async function getCustomNotificationSound(): Promise<
{ blob: Blob; entry: NotificationSoundEntry } | null
> {
const db = await openDb();
return new Promise((resolve, reject) => {
const t = db.transaction(STORE, 'readonly');
const req = t.objectStore(STORE).get(KEY);
req.onsuccess = () => {
const stored = req.result as StoredNotificationSound | undefined;
if (!stored) {
resolve(null);
return;
}
const { blob, ...entry } = stored;
resolve({ blob, entry });
};
req.onerror = () => reject(req.error);
t.onerror = () => reject(t.error);
});
}
export async function getCustomNotificationSoundMeta(): Promise<NotificationSoundEntry | null> {
const res = await getCustomNotificationSound();
return res ? res.entry : null;
}
export async function setCustomNotificationSound(file: File): Promise<NotificationSoundEntry> {
if (file.size === 0) throw new Error('empty_file');
if (file.size > MAX_NOTIFICATION_SOUND_BYTES) throw new Error('sound_too_large');
const mime = file.type || 'application/octet-stream';
if (!mime.startsWith('audio/')) throw new Error('sound_not_audio');
const stored: StoredNotificationSound = {
filename: file.name || 'notification.audio',
mime,
size: file.size,
uploadedAt: Date.now(),
blob: file,
};
const db = await openDb();
await new Promise<void>((resolve, reject) => {
const t = db.transaction(STORE, 'readwrite');
const req = t.objectStore(STORE).put(stored, KEY);
req.onsuccess = () => resolve();
req.onerror = () => reject(req.error);
t.onerror = () => reject(t.error);
});
notifyChange();
const { blob: _blob, ...entry } = stored;
return entry;
}
export async function clearCustomNotificationSound(): Promise<void> {
const db = await openDb();
await new Promise<void>((resolve, reject) => {
const t = db.transaction(STORE, 'readwrite');
const req = t.objectStore(STORE).delete(KEY);
req.onsuccess = () => resolve();
req.onerror = () => reject(req.error);
t.onerror = () => reject(t.error);
});
notifyChange();
}