normalize paths on all OS
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { app, BrowserWindow, desktopCapturer, dialog, ipcMain, screen, shell } from "electron";
|
||||
import { RECORDINGS_DIR } from "../main";
|
||||
|
||||
@@ -18,6 +19,27 @@ function normalizePath(filePath: string) {
|
||||
return path.resolve(filePath);
|
||||
}
|
||||
|
||||
function normalizeVideoSourcePath(videoPath?: string | null): string | null {
|
||||
if (typeof videoPath !== "string") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const trimmed = videoPath.trim();
|
||||
if (!trimmed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (/^file:\/\//i.test(trimmed)) {
|
||||
try {
|
||||
return fileURLToPath(trimmed);
|
||||
} catch {
|
||||
// Fall through and keep best-effort string path below.
|
||||
}
|
||||
}
|
||||
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
function isTrustedProjectPath(filePath?: string | null) {
|
||||
if (!filePath || !currentProjectPath) {
|
||||
return false;
|
||||
@@ -199,7 +221,7 @@ export function registerIpcHandlers(
|
||||
});
|
||||
|
||||
ipcMain.handle("get-cursor-telemetry", async (_, videoPath?: string) => {
|
||||
const targetVideoPath = videoPath ?? currentVideoPath;
|
||||
const targetVideoPath = normalizeVideoSourcePath(videoPath ?? currentVideoPath);
|
||||
if (!targetVideoPath) {
|
||||
return { success: true, samples: [] };
|
||||
}
|
||||
@@ -265,9 +287,11 @@ export function registerIpcHandlers(
|
||||
ipcMain.handle("get-asset-base-path", () => {
|
||||
try {
|
||||
if (app.isPackaged) {
|
||||
return path.join(process.resourcesPath, "assets");
|
||||
const assetPath = path.join(process.resourcesPath, "assets");
|
||||
return pathToFileURL(`${assetPath}${path.sep}`).toString();
|
||||
}
|
||||
return path.join(app.getAppPath(), "public", "assets");
|
||||
const assetPath = path.join(app.getAppPath(), "public", "assets");
|
||||
return pathToFileURL(`${assetPath}${path.sep}`).toString();
|
||||
} catch (err) {
|
||||
console.error("Failed to resolve asset base path:", err);
|
||||
return null;
|
||||
@@ -456,7 +480,7 @@ export function registerIpcHandlers(
|
||||
const project = JSON.parse(content);
|
||||
currentProjectPath = filePath;
|
||||
if (project && typeof project === "object" && typeof project.videoPath === "string") {
|
||||
currentVideoPath = project.videoPath;
|
||||
currentVideoPath = normalizeVideoSourcePath(project.videoPath) ?? project.videoPath;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -483,7 +507,7 @@ export function registerIpcHandlers(
|
||||
const content = await fs.readFile(currentProjectPath, "utf-8");
|
||||
const project = JSON.parse(content);
|
||||
if (project && typeof project === "object" && typeof project.videoPath === "string") {
|
||||
currentVideoPath = project.videoPath;
|
||||
currentVideoPath = normalizeVideoSourcePath(project.videoPath) ?? project.videoPath;
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
@@ -500,7 +524,7 @@ export function registerIpcHandlers(
|
||||
}
|
||||
});
|
||||
ipcMain.handle("set-current-video-path", (_, path: string) => {
|
||||
currentVideoPath = path;
|
||||
currentVideoPath = normalizeVideoSourcePath(path) ?? path;
|
||||
currentProjectPath = null;
|
||||
return { success: true };
|
||||
});
|
||||
|
||||
@@ -118,7 +118,7 @@ export default function VideoEditor() {
|
||||
}
|
||||
|
||||
const project = candidate;
|
||||
const sourcePath = project.videoPath;
|
||||
const sourcePath = fromFileUrl(project.videoPath);
|
||||
const normalizedEditor = normalizeProjectEditor(project.editor);
|
||||
|
||||
try {
|
||||
@@ -259,8 +259,9 @@ export default function VideoEditor() {
|
||||
|
||||
const result = await window.electronAPI.getCurrentVideoPath();
|
||||
if (result.success && result.path) {
|
||||
setVideoSourcePath(result.path);
|
||||
setVideoPath(toFileUrl(result.path));
|
||||
const sourcePath = fromFileUrl(result.path);
|
||||
setVideoSourcePath(sourcePath);
|
||||
setVideoPath(toFileUrl(sourcePath));
|
||||
setCurrentProjectPath(null);
|
||||
setLastSavedSnapshot(null);
|
||||
} else {
|
||||
|
||||
@@ -58,21 +58,50 @@ function clamp(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function isFileUrl(value: string): boolean {
|
||||
return /^file:\/\//i.test(value);
|
||||
}
|
||||
|
||||
function encodePathSegments(pathname: string, keepWindowsDrive = false): string {
|
||||
return pathname
|
||||
.split("/")
|
||||
.map((segment, index) => {
|
||||
if (!segment) return "";
|
||||
if (keepWindowsDrive && index === 1 && /^[a-zA-Z]:$/.test(segment)) {
|
||||
return segment;
|
||||
}
|
||||
return encodeURIComponent(segment);
|
||||
})
|
||||
.join("/");
|
||||
}
|
||||
|
||||
export function toFileUrl(filePath: string): string {
|
||||
const normalized = filePath.replace(/\\/g, "/");
|
||||
if (normalized.match(/^[a-zA-Z]:/)) {
|
||||
return `file:///${encodeURI(normalized)}`;
|
||||
|
||||
// Windows drive path: C:/Users/...
|
||||
if (/^[a-zA-Z]:\//.test(normalized)) {
|
||||
return `file://${encodePathSegments(`/${normalized}`, true)}`;
|
||||
}
|
||||
return `file://${encodeURI(normalized)}`;
|
||||
|
||||
// UNC path: //server/share/...
|
||||
if (normalized.startsWith("//")) {
|
||||
const [host, ...pathParts] = normalized.replace(/^\/+/, "").split("/");
|
||||
const encodedPath = pathParts.map((part) => encodeURIComponent(part)).join("/");
|
||||
return encodedPath ? `file://${host}/${encodedPath}` : `file://${host}/`;
|
||||
}
|
||||
|
||||
const absolutePath = normalized.startsWith("/") ? normalized : `/${normalized}`;
|
||||
return `file://${encodePathSegments(absolutePath)}`;
|
||||
}
|
||||
|
||||
export function fromFileUrl(fileUrl: string): string {
|
||||
if (!fileUrl.startsWith("file://")) {
|
||||
const value = fileUrl.trim();
|
||||
if (!isFileUrl(value)) {
|
||||
return fileUrl;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(fileUrl);
|
||||
const url = new URL(value);
|
||||
const pathname = decodeURIComponent(url.pathname);
|
||||
|
||||
if (url.host && url.host !== "localhost") {
|
||||
@@ -85,7 +114,13 @@ export function fromFileUrl(fileUrl: string): string {
|
||||
|
||||
return pathname;
|
||||
} catch {
|
||||
const fallbackPath = decodeURIComponent(fileUrl.replace(/^file:\/\//, ""));
|
||||
const rawFallbackPath = value.replace(/^file:\/\//i, "");
|
||||
let fallbackPath = rawFallbackPath;
|
||||
try {
|
||||
fallbackPath = decodeURIComponent(rawFallbackPath);
|
||||
} catch {
|
||||
// Keep raw best-effort path if percent decoding fails.
|
||||
}
|
||||
return fallbackPath.replace(/^\/([a-zA-Z]:)/, "$1");
|
||||
}
|
||||
}
|
||||
|
||||
+18
-4
@@ -1,4 +1,19 @@
|
||||
function encodeRelativeAssetPath(relativePath: string): string {
|
||||
return relativePath
|
||||
.replace(/^\/+/, "")
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.map((part) => encodeURIComponent(part))
|
||||
.join("/");
|
||||
}
|
||||
|
||||
function ensureTrailingSlash(value: string): string {
|
||||
return value.endsWith("/") ? value : `${value}/`;
|
||||
}
|
||||
|
||||
export async function getAssetPath(relativePath: string): Promise<string> {
|
||||
const encodedRelativePath = encodeRelativeAssetPath(relativePath);
|
||||
|
||||
try {
|
||||
if (typeof window !== "undefined") {
|
||||
// If running in a dev server (http/https), prefer the web-served path
|
||||
@@ -7,14 +22,13 @@ export async function getAssetPath(relativePath: string): Promise<string> {
|
||||
window.location.protocol &&
|
||||
window.location.protocol.startsWith("http")
|
||||
) {
|
||||
return `/${relativePath.replace(/^\//, "")}`;
|
||||
return `/${encodedRelativePath}`;
|
||||
}
|
||||
|
||||
if (window.electronAPI && typeof window.electronAPI.getAssetBasePath === "function") {
|
||||
const base = await window.electronAPI.getAssetBasePath();
|
||||
if (base) {
|
||||
const normalized = base.replace(/\\/g, "/");
|
||||
return `file://${normalized}/${relativePath}`;
|
||||
return new URL(encodedRelativePath, ensureTrailingSlash(base)).toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +37,7 @@ export async function getAssetPath(relativePath: string): Promise<string> {
|
||||
}
|
||||
|
||||
// Fallback for web/dev server: public/wallpapers are served at '/wallpapers/...'
|
||||
return `/${relativePath.replace(/^\//, "")}`;
|
||||
return `/${encodedRelativePath}`;
|
||||
}
|
||||
|
||||
export default getAssetPath;
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { clampFocusToStage as clampFocusToStageUtil } from "@/components/video-editor/videoPlayback/focusUtils";
|
||||
import { findDominantRegion } from "@/components/video-editor/videoPlayback/zoomRegionUtils";
|
||||
import { applyZoomTransform } from "@/components/video-editor/videoPlayback/zoomTransform";
|
||||
import { getAssetPath } from "@/lib/assetPath";
|
||||
import { renderAnnotations } from "./annotationRenderer";
|
||||
|
||||
interface FrameRenderConfig {
|
||||
@@ -178,18 +179,14 @@ export class FrameRenderer {
|
||||
) {
|
||||
// Image background
|
||||
const img = new Image();
|
||||
// Don't set crossOrigin for same-origin images to avoid CORS taint
|
||||
// Only set it for cross-origin URLs
|
||||
let imageUrl: string;
|
||||
if (wallpaper.startsWith("http")) {
|
||||
imageUrl = wallpaper;
|
||||
if (!imageUrl.startsWith(window.location.origin)) {
|
||||
img.crossOrigin = "anonymous";
|
||||
}
|
||||
} else if (wallpaper.startsWith("file://") || wallpaper.startsWith("data:")) {
|
||||
imageUrl = wallpaper;
|
||||
} else {
|
||||
imageUrl = window.location.origin + wallpaper;
|
||||
const imageUrl = await this.resolveWallpaperImageUrl(wallpaper);
|
||||
// Don't set crossOrigin for same-origin images to avoid CORS taint.
|
||||
if (
|
||||
imageUrl.startsWith("http") &&
|
||||
window.location.origin &&
|
||||
!imageUrl.startsWith(window.location.origin)
|
||||
) {
|
||||
img.crossOrigin = "anonymous";
|
||||
}
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
@@ -283,6 +280,23 @@ export class FrameRenderer {
|
||||
this.backgroundSprite = bgCanvas;
|
||||
}
|
||||
|
||||
private async resolveWallpaperImageUrl(wallpaper: string): Promise<string> {
|
||||
if (
|
||||
wallpaper.startsWith("file://") ||
|
||||
wallpaper.startsWith("data:") ||
|
||||
wallpaper.startsWith("http")
|
||||
) {
|
||||
return wallpaper;
|
||||
}
|
||||
|
||||
const resolved = await getAssetPath(wallpaper.replace(/^\/+/, ""));
|
||||
if (resolved.startsWith("/") && window.location.protocol.startsWith("http")) {
|
||||
return `${window.location.origin}${resolved}`;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
async renderFrame(videoFrame: VideoFrame, timestamp: number): Promise<void> {
|
||||
if (!this.app || !this.videoContainer || !this.cameraContainer) {
|
||||
throw new Error("Renderer not initialized");
|
||||
|
||||
Reference in New Issue
Block a user