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>
136 lines
4.1 KiB
TypeScript
136 lines
4.1 KiB
TypeScript
// 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();
|
|
}
|