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
+111
View File
@@ -0,0 +1,111 @@
import { supabase } from './supabase';
const BUCKET = 'profile-banners';
// 3:1 hero crop. 1500x500 hits the sweet spot between crisp on a wide
// settings preview and a payload that stays well under the 8 MB pre-encode
// budget we accept from the user (post-WebP that's typically ~150300 KB).
const TARGET_WIDTH = 1500;
const TARGET_HEIGHT = 500;
const QUALITY = 0.82;
const MAX_INPUT_BYTES = 8 * 1024 * 1024;
// Resizes the source image to a centred 3:1 crop at TARGET_WIDTH x TARGET_HEIGHT
// and re-encodes as WebP (JPEG fallback if WebP encode unsupported).
async function resizeToBanner(file: File): Promise<Blob> {
const url = URL.createObjectURL(file);
try {
const img = await new Promise<HTMLImageElement>((resolve, reject) => {
const i = new Image();
i.onload = () => resolve(i);
i.onerror = () => reject(new Error('image load failed'));
i.src = url;
});
const srcRatio = img.naturalWidth / img.naturalHeight;
const targetRatio = TARGET_WIDTH / TARGET_HEIGHT;
let sx = 0;
let sy = 0;
let sw = img.naturalWidth;
let sh = img.naturalHeight;
if (srcRatio > targetRatio) {
// Source is wider than 3:1 — crop horizontally, keep full height.
sw = Math.round(img.naturalHeight * targetRatio);
sx = Math.round((img.naturalWidth - sw) / 2);
} else if (srcRatio < targetRatio) {
// Source is taller than 3:1 — crop vertically, keep full width.
sh = Math.round(img.naturalWidth / targetRatio);
sy = Math.round((img.naturalHeight - sh) / 2);
}
// Don't upscale — if the source crop is smaller than the target, render
// at the source crop size so we don't waste bytes on synthetic detail.
const outWidth = Math.min(TARGET_WIDTH, sw);
const outHeight = Math.min(TARGET_HEIGHT, sh);
const canvas = document.createElement('canvas');
canvas.width = outWidth;
canvas.height = outHeight;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('canvas context unavailable');
ctx.drawImage(img, sx, sy, sw, sh, 0, 0, outWidth, outHeight);
const blob = await new Promise<Blob | null>((resolve) =>
canvas.toBlob(resolve, 'image/webp', QUALITY),
);
if (blob) return blob;
const jpeg = await new Promise<Blob | null>((resolve) =>
canvas.toBlob(resolve, 'image/jpeg', QUALITY),
);
if (!jpeg) throw new Error('canvas toBlob returned null');
return jpeg;
} finally {
URL.revokeObjectURL(url);
}
}
export async function uploadBanner(userId: string, file: File): Promise<string> {
if (!file.type.startsWith('image/')) {
throw new Error('only image files are accepted');
}
if (file.size > MAX_INPUT_BYTES) {
throw new Error('image must be 8 MB or smaller');
}
const blob = await resizeToBanner(file);
return uploadBannerBlob(userId, blob);
}
// Upload an already-cropped Blob (e.g. from ImageCropDialog). Skips the
// legacy center-crop path so the user-chosen framing is preserved.
export async function uploadBannerBlob(userId: string, blob: Blob): Promise<string> {
const ext = blob.type === 'image/webp' ? 'webp' : 'jpg';
// Random filename so old uploads don't get overwritten before we update
// the profile row — Supabase Storage CDN caches by URL, so a fresh path
// also forces clients to fetch the new image.
const name =
userId + '/' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8) + '.' + ext;
const { error: upErr } = await supabase.storage.from(BUCKET).upload(name, blob, {
contentType: blob.type,
cacheControl: '604800',
upsert: false,
});
if (upErr) throw upErr;
const { data: pub } = supabase.storage.from(BUCKET).getPublicUrl(name);
return pub.publicUrl;
}
export const BANNER_TARGET_WIDTH = TARGET_WIDTH;
export const BANNER_TARGET_HEIGHT = TARGET_HEIGHT;
export const BANNER_MAX_INPUT_BYTES = MAX_INPUT_BYTES;
export async function deleteBannerObject(publicUrl: string): Promise<void> {
// Public URLs look like
// https://<host>/storage/v1/object/public/profile-banners/<path>
// Extract <path> and remove.
const marker = '/object/public/' + BUCKET + '/';
const idx = publicUrl.indexOf(marker);
if (idx === -1) return;
const path = publicUrl.slice(idx + marker.length);
const { error } = await supabase.storage.from(BUCKET).remove([path]);
if (error) throw error;
}