Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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.log("[fix-electron-esm] dist-electron/ not found, skipping.");
process.exit(0);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}

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