Skip to content
This repository was archived by the owner on Jun 17, 2026. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions electron/windows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,10 @@ export function createHudOverlayWindow(): BrowserWindow {
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
}

// Use "floating" level so macOS system dialogs (e.g. screen recording
// permission prompt) are not hidden behind the HUD overlay.
win.setAlwaysOnTop(true, "floating");

win.webContents.on("did-finish-load", () => {
win?.webContents.send("main-process-message", new Date().toLocaleString());
});
Expand Down Expand Up @@ -167,6 +171,9 @@ export function createSourceSelectorWindow(): BrowserWindow {
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
}

// Use "floating" level so macOS system dialogs appear above this window.
win.setAlwaysOnTop(true, "floating");

if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL + "?windowType=source-selector");
} else {
Expand Down
19 changes: 13 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
},
"scripts": {
"dev": "vite",
"build": "tsc && vite build && electron-builder",
"build": "tsc && vite build && node scripts/fix-electron-esm.mjs && electron-builder",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write .",
"i18n:check": "node scripts/i18n-check.mjs",
"preview": "vite preview",
"build:mac": "tsc && vite build && electron-builder --mac",
"build:win": "tsc && vite build && electron-builder --win",
"build:linux": "tsc && vite build && electron-builder --linux AppImage deb",
"build:mac": "tsc && vite build && node scripts/fix-electron-esm.mjs && electron-builder --mac",
"build:win": "tsc && vite build && node scripts/fix-electron-esm.mjs && electron-builder --win",
"build:linux": "tsc && vite build && node scripts/fix-electron-esm.mjs && electron-builder --linux AppImage deb",
"test": "vitest --run",
"test:watch": "vitest",
"build-vite": "tsc && vite build",
Expand All @@ -46,6 +46,7 @@
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@sapphi-red/web-noise-suppressor": "^0.3.5",
"@types/gif.js": "^0.2.5",
"@uiw/color-convert": "^2.9.2",
"@uiw/react-color-block": "^2.9.2",
Expand Down
24 changes: 24 additions & 0 deletions scripts/fix-electron-esm.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env node
/**
* Post-build fix: Places a { "type": "commonjs" } package.json inside
* dist-electron/ so Node.js treats .js files there as CJS — even though
* the root package.json has "type": "module".
*
* vite-plugin-electron/simple already emits CJS output for the main process
* when it detects "type": "module", but the file extension stays .js and
* Node resolves the nearest package.json to decide the module type.
* This script bridges that gap.
*/
import fs from "node:fs";
import path from "node:path";

const distElectron = path.resolve("dist-electron");
const target = path.join(distElectron, "package.json");

if (!fs.existsSync(distElectron)) {
console.error("[fix-electron-esm] dist-electron/ not found after vite build.");
process.exit(1);
}

fs.writeFileSync(target, JSON.stringify({ type: "commonjs" }, null, 2) + "\n");
console.log("[fix-electron-esm] Wrote dist-electron/package.json (type: commonjs)");
69 changes: 69 additions & 0 deletions src/components/video-editor/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,12 @@ interface SettingsPanelProps {
webcamSizePreset?: WebcamSizePreset;
onWebcamSizePresetChange?: (size: WebcamSizePreset) => void;
onWebcamSizePresetCommit?: () => void;
noiseReductionEnabled?: boolean;
onNoiseReductionEnabledChange?: (enabled: boolean) => void;
noiseReductionLevel?: import("@/lib/audioEnhancement").NoiseReductionLevel;
onNoiseReductionLevelChange?: (
level: import("@/lib/audioEnhancement").NoiseReductionLevel,
) => void;
}

export default SettingsPanel;
Expand Down Expand Up @@ -324,6 +330,10 @@ export function SettingsPanel({
webcamSizePreset = DEFAULT_WEBCAM_SIZE_PRESET,
onWebcamSizePresetChange,
onWebcamSizePresetCommit,
noiseReductionEnabled = false,
onNoiseReductionEnabledChange,
noiseReductionLevel = "moderate",
onNoiseReductionLevelChange,
}: SettingsPanelProps) {
const t = useScopedT("settings");
const [wallpaperPaths, setWallpaperPaths] = useState<string[]>([]);
Expand Down Expand Up @@ -1065,6 +1075,65 @@ export function SettingsPanel({
</AccordionContent>
</AccordionItem>

<AccordionItem value="audio" className="border-white/5 rounded-xl bg-white/[0.02] px-3">
<AccordionTrigger className="py-2.5 hover:no-underline">
<div className="flex items-center gap-2">
<svg
className="w-4 h-4 text-[#34B27B]"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M2 10v3" />
<path d="M6 6v11" />
<path d="M10 3v18" />
<path d="M14 8v7" />
<path d="M18 5v13" />
<path d="M22 10v3" />
</svg>
<span className="text-xs font-medium">{t("audio.title")}</span>
</div>
</AccordionTrigger>
<AccordionContent className="pb-3 space-y-3">
<div className="flex items-center justify-between">
<label className="text-[11px] text-slate-300">{t("audio.noiseReduction")}</label>
<Switch
checked={noiseReductionEnabled}
onCheckedChange={onNoiseReductionEnabledChange}
/>
</div>
{noiseReductionEnabled && (
<div className="space-y-2">
<label className="text-[10px] text-slate-400">{t("audio.level")}</label>
<div className="grid grid-cols-3 gap-1">
{(["light", "moderate", "aggressive"] as const).map((lvl) => (
<button
key={lvl}
type="button"
className={`text-[10px] py-1.5 rounded-md border transition-all ${
noiseReductionLevel === lvl
? lvl === "aggressive"
? "bg-red-500/20 border-red-500/50 text-red-300"
: lvl === "moderate"
? "bg-blue-500/20 border-blue-500/50 text-blue-300"
: "bg-green-500/20 border-green-500/50 text-green-300"
: "bg-white/5 border-white/10 text-slate-400 hover:bg-white/10"
}`}
onClick={() => onNoiseReductionLevelChange?.(lvl)}
>
{t(`audio.nrLevel.${lvl}`)}
</button>
))}
</div>
<p className="text-[9px] text-slate-500">{t("audio.nrDescription")}</p>
</div>
)}
</AccordionContent>
</AccordionItem>

<AccordionItem
value="background"
className="border-white/5 rounded-xl bg-white/[0.02] px-3"
Expand Down
9 changes: 9 additions & 0 deletions src/components/video-editor/VideoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ export default function VideoEditor() {
format: string;
} | null>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const [noiseReductionEnabled, setNoiseReductionEnabled] = useState(false);
const [noiseReductionLevel, setNoiseReductionLevel] =
useState<import("@/lib/audioEnhancement").NoiseReductionLevel>("moderate");

const playerContainerRef = useRef<HTMLDivElement>(null);
const videoPlaybackRef = useRef<VideoPlaybackRef>(null);
Expand Down Expand Up @@ -1857,6 +1860,8 @@ export default function VideoEditor() {
onBlurDataChange={handleBlurDataPreviewChange}
onBlurDataCommit={commitState}
cursorTelemetry={cursorTelemetry}
noiseReductionEnabled={noiseReductionEnabled}
noiseReductionLevel={noiseReductionLevel}
/>
</div>
</div>
Expand Down Expand Up @@ -2051,6 +2056,10 @@ export default function VideoEditor() {
onZoomDurationChange={(zoomIn, zoomOut) =>
selectedZoomId && handleZoomDurationChange(selectedZoomId, zoomIn, zoomOut)
}
noiseReductionEnabled={noiseReductionEnabled}
onNoiseReductionEnabledChange={setNoiseReductionEnabled}
noiseReductionLevel={noiseReductionLevel}
onNoiseReductionLevelChange={setNoiseReductionLevel}
/>
</div>
</div>
Expand Down
10 changes: 10 additions & 0 deletions src/components/video-editor/VideoPlayback.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
useRef,
useState,
} from "react";
import { useAudioEnhancement } from "@/hooks/useAudioEnhancement";
import { getAssetPath } from "@/lib/assetPath";
import {
getWebcamLayoutCssBoxShadow,
Expand Down Expand Up @@ -110,6 +111,8 @@ interface VideoPlaybackProps {
onBlurDataChange?: (id: string, blurData: BlurData) => void;
onBlurDataCommit?: () => void;
cursorTelemetry?: import("./types").CursorTelemetryPoint[];
noiseReductionEnabled?: boolean;
noiseReductionLevel?: import("@/lib/audioEnhancement").NoiseReductionLevel;
}

export interface VideoPlaybackRef {
Expand Down Expand Up @@ -168,17 +171,23 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(
onBlurDataChange,
onBlurDataCommit,
cursorTelemetry = [],
noiseReductionEnabled = false,
noiseReductionLevel = "moderate",
},
ref,
) => {
const videoRef = useRef<HTMLVideoElement | null>(null);
const [videoElement, setVideoElement] = useState<HTMLVideoElement | null>(null);
const webcamVideoRef = useRef<HTMLVideoElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const appRef = useRef<Application | null>(null);
const videoSpriteRef = useRef<Sprite | null>(null);
const videoContainerRef = useRef<Container | null>(null);
const cameraContainerRef = useRef<Container | null>(null);
const timeUpdateAnimationRef = useRef<number | null>(null);
// Post-processing noise reduction on playback audio
useAudioEnhancement(videoElement, noiseReductionEnabled, noiseReductionLevel);
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const [pixiReady, setPixiReady] = useState(false);
const [videoReady, setVideoReady] = useState(false);
const [overlaySize, setOverlaySize] = useState({ width: 800, height: 600 });
Expand Down Expand Up @@ -1083,6 +1092,7 @@ const VideoPlayback = forwardRef<VideoPlaybackRef, VideoPlaybackProps>(

const handleLoadedMetadata = (e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
const video = e.currentTarget;
setVideoElement(video);
onDurationChange(video.duration);
video.currentTime = 0;
video.pause();
Expand Down
Loading
Loading