mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-25 15:25:44 +00:00
Improve project autosave and media path handling
This commit is contained in:
Vendored
+5
-2
@@ -398,12 +398,15 @@ interface Window {
|
||||
message?: string;
|
||||
error?: string;
|
||||
}>;
|
||||
setCurrentVideoPath: (path: string) => Promise<{ success: boolean }>;
|
||||
setCurrentVideoPath: (
|
||||
path: string,
|
||||
options?: { preserveProjectPath?: boolean },
|
||||
) => Promise<{ success: boolean }>;
|
||||
setCurrentRecordingSession: (session: {
|
||||
videoPath: string;
|
||||
webcamPath?: string | null;
|
||||
timeOffsetMs?: number;
|
||||
}) => Promise<{ success: boolean }>;
|
||||
}, options?: { preserveProjectPath?: boolean }) => Promise<{ success: boolean }>;
|
||||
getCurrentRecordingSession: () => Promise<{
|
||||
success: boolean;
|
||||
session?: { videoPath: string; webcamPath?: string | null; timeOffsetMs?: number };
|
||||
|
||||
@@ -79,12 +79,13 @@ describe("local media path policy", () => {
|
||||
const videoPath = path.join(downloadsPath, "external-video.mp4");
|
||||
await fs.mkdir(downloadsPath, { recursive: true });
|
||||
await fs.writeFile(videoPath, "test-video");
|
||||
const resolvedVideoPath = await fs.realpath(videoPath);
|
||||
|
||||
const { resolveApprovedLocalMediaPath } = await import("./manager");
|
||||
const { isAllowedMediaPath } = await import("../../mediaServer");
|
||||
|
||||
expect(isAllowedMediaPath(videoPath)).toBe(false);
|
||||
await expect(resolveApprovedLocalMediaPath(videoPath)).resolves.toBe(videoPath);
|
||||
await expect(resolveApprovedLocalMediaPath(videoPath)).resolves.toBe(resolvedVideoPath);
|
||||
expect(isAllowedMediaPath(videoPath)).toBe(true);
|
||||
});
|
||||
|
||||
@@ -100,4 +101,18 @@ describe("local media path policy", () => {
|
||||
await expect(resolveApprovedLocalMediaPath(textPath)).resolves.toBeNull();
|
||||
expect(isAllowedMediaPath(textPath)).toBe(false);
|
||||
});
|
||||
|
||||
it("preserves an existing project thumbnail when no replacement is provided", async () => {
|
||||
const projectPath = path.join(tempRoot, "Projects", "demo.recordly");
|
||||
const thumbnailDataUrl = `data:image/png;base64,${Buffer.from("png-thumbnail").toString("base64")}`;
|
||||
await fs.mkdir(path.dirname(projectPath), { recursive: true });
|
||||
|
||||
const { getProjectThumbnailPath, saveProjectThumbnail } = await import("./manager");
|
||||
const thumbnailPath = getProjectThumbnailPath(projectPath);
|
||||
|
||||
await saveProjectThumbnail(projectPath, thumbnailDataUrl);
|
||||
await saveProjectThumbnail(projectPath, undefined);
|
||||
|
||||
await expect(fs.readFile(thumbnailPath, "utf8")).resolves.toBe("png-thumbnail");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,19 +68,36 @@ export async function isAllowedLocalMediaPath(candidatePath: string) {
|
||||
return isAllowedLocalReadPath(normalizedCandidatePath);
|
||||
}
|
||||
|
||||
async function collectApprovedLocalReadPaths(filePath?: string | null): Promise<string[]> {
|
||||
const normalizedPath = normalizeVideoSourcePath(filePath);
|
||||
if (!normalizedPath) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const approvedPaths = [normalizePath(normalizedPath)];
|
||||
|
||||
try {
|
||||
const realPath = await fs.realpath(approvedPaths[0]);
|
||||
const normalizedRealPath = normalizePath(realPath);
|
||||
if (!approvedPaths.includes(normalizedRealPath)) {
|
||||
approvedPaths.push(normalizedRealPath);
|
||||
}
|
||||
} catch {
|
||||
// Ignore missing files; the eventual read will surface the real error.
|
||||
}
|
||||
|
||||
return approvedPaths;
|
||||
}
|
||||
|
||||
export async function rememberApprovedLocalReadPath(filePath?: string | null) {
|
||||
const normalizedPath = normalizeVideoSourcePath(filePath);
|
||||
if (!normalizedPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedPath = normalizePath(normalizedPath);
|
||||
approvedLocalReadPaths.add(resolvedPath);
|
||||
|
||||
try {
|
||||
approvedLocalReadPaths.add(await fs.realpath(resolvedPath));
|
||||
} catch {
|
||||
// Ignore missing files; the eventual read will surface the real error.
|
||||
const approvedPaths = await collectApprovedLocalReadPaths(normalizedPath);
|
||||
for (const approvedPath of approvedPaths) {
|
||||
approvedLocalReadPaths.add(approvedPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,13 +118,26 @@ export async function resolveApprovedLocalMediaPath(candidatePath: string): Prom
|
||||
return null;
|
||||
}
|
||||
|
||||
await rememberApprovedLocalReadPath(realPath);
|
||||
await rememberApprovedLocalReadPath(candidatePath);
|
||||
return realPath;
|
||||
}
|
||||
|
||||
export async function replaceApprovedSessionLocalReadPaths(filePaths: Array<string | null | undefined>) {
|
||||
const nextApprovedPaths = new Set<string>();
|
||||
const approvedPathLists = await Promise.all(
|
||||
filePaths.map((filePath) => collectApprovedLocalReadPaths(filePath)),
|
||||
);
|
||||
|
||||
for (const approvedPathList of approvedPathLists) {
|
||||
for (const approvedPath of approvedPathList) {
|
||||
nextApprovedPaths.add(approvedPath);
|
||||
}
|
||||
}
|
||||
|
||||
approvedLocalReadPaths.clear();
|
||||
await Promise.all(filePaths.map((filePath) => rememberApprovedLocalReadPath(filePath)));
|
||||
for (const approvedPath of nextApprovedPaths) {
|
||||
approvedLocalReadPaths.add(approvedPath);
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveProjectMediaSources(project: unknown): Promise<
|
||||
@@ -196,6 +226,10 @@ export function getProjectThumbnailPath(projectPath: string) {
|
||||
|
||||
export async function saveProjectThumbnail(projectPath: string, thumbnailDataUrl?: string | null) {
|
||||
const thumbnailPath = getProjectThumbnailPath(projectPath);
|
||||
if (thumbnailDataUrl === undefined) {
|
||||
return existsSync(thumbnailPath) ? thumbnailPath : null;
|
||||
}
|
||||
|
||||
if (!thumbnailDataUrl) {
|
||||
await fs.rm(thumbnailPath, { force: true }).catch(() => undefined);
|
||||
return null;
|
||||
@@ -383,4 +417,3 @@ export function isTrustedProjectPath(filePath?: string | null): boolean {
|
||||
if (!filePath || !currentProjectPath) return false;
|
||||
return normalizePath(filePath) === normalizePath(currentProjectPath);
|
||||
}
|
||||
|
||||
|
||||
@@ -527,7 +527,7 @@ export function registerProjectHandlers() {
|
||||
return { success: false, error: String(error), message: 'Failed to open projects folder.' }
|
||||
}
|
||||
})
|
||||
ipcMain.handle('set-current-video-path', async (_, path: string) => {
|
||||
ipcMain.handle('set-current-video-path', async (_, path: string, options?: { preserveProjectPath?: boolean }) => {
|
||||
setCurrentVideoPath(normalizeVideoSourcePath(path) ?? path)
|
||||
approveUserPath(currentVideoPath)
|
||||
const resolvedSession = await resolveRecordingSession(currentVideoPath)
|
||||
@@ -547,11 +547,13 @@ export function registerProjectHandlers() {
|
||||
await persistRecordingSessionManifest(resolvedSession)
|
||||
}
|
||||
|
||||
setCurrentProjectPath(null)
|
||||
if (!options?.preserveProjectPath) {
|
||||
setCurrentProjectPath(null)
|
||||
}
|
||||
return { success: true, webcamPath: resolvedSession.webcamPath ?? null }
|
||||
})
|
||||
|
||||
ipcMain.handle('set-current-recording-session', async (_, session: { videoPath: string; webcamPath?: string | null; timeOffsetMs?: number }) => {
|
||||
ipcMain.handle('set-current-recording-session', async (_, session: { videoPath: string; webcamPath?: string | null; timeOffsetMs?: number }, options?: { preserveProjectPath?: boolean }) => {
|
||||
const normalizedVideoPath = normalizeVideoSourcePath(session.videoPath) ?? session.videoPath
|
||||
setCurrentVideoPath(normalizedVideoPath)
|
||||
setCurrentRecordingSession({
|
||||
@@ -563,7 +565,9 @@ export function registerProjectHandlers() {
|
||||
currentRecordingSession!.videoPath,
|
||||
currentRecordingSession!.webcamPath,
|
||||
])
|
||||
setCurrentProjectPath(null)
|
||||
if (!options?.preserveProjectPath) {
|
||||
setCurrentProjectPath(null)
|
||||
}
|
||||
await persistRecordingSessionManifest(currentRecordingSession!)
|
||||
return { success: true }
|
||||
})
|
||||
|
||||
+4
-4
@@ -430,15 +430,15 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
||||
}) => {
|
||||
return ipcRenderer.invoke("generate-auto-captions", options);
|
||||
},
|
||||
setCurrentVideoPath: (path: string) => {
|
||||
return ipcRenderer.invoke("set-current-video-path", path);
|
||||
setCurrentVideoPath: (path: string, options?: { preserveProjectPath?: boolean }) => {
|
||||
return ipcRenderer.invoke("set-current-video-path", path, options);
|
||||
},
|
||||
setCurrentRecordingSession: (session: {
|
||||
videoPath: string;
|
||||
webcamPath?: string | null;
|
||||
timeOffsetMs?: number;
|
||||
}) => {
|
||||
return ipcRenderer.invoke("set-current-recording-session", session);
|
||||
}, options?: { preserveProjectPath?: boolean }) => {
|
||||
return ipcRenderer.invoke("set-current-recording-session", session, options);
|
||||
},
|
||||
getCurrentRecordingSession: () => {
|
||||
return ipcRenderer.invoke("get-current-recording-session");
|
||||
|
||||
@@ -226,6 +226,13 @@ type SmokeExportConfig = {
|
||||
fps?: ExportMp4FrameRate;
|
||||
};
|
||||
|
||||
type SaveProjectOptions = {
|
||||
silent?: boolean;
|
||||
remountPreviewAfterSave?: boolean;
|
||||
refreshLibraryAfterSave?: boolean;
|
||||
captureThumbnail?: boolean;
|
||||
};
|
||||
|
||||
async function writeSmokeExportReport(
|
||||
outputPath: string | null,
|
||||
report: Record<string, unknown>,
|
||||
@@ -251,6 +258,7 @@ async function writeSmokeExportReport(
|
||||
|
||||
const DEFAULT_MP4_EXPORT_FRAME_RATE: ExportMp4FrameRate = 30;
|
||||
const SOURCE_AUDIO_FALLBACK_TOAST_ID = "source-audio-fallback-error";
|
||||
const PROJECT_AUTOSAVE_DELAY_MS = 1000;
|
||||
|
||||
function getEncodingModeBitrateMultiplier(encodingMode: ExportEncodingMode): number {
|
||||
switch (encodingMode) {
|
||||
@@ -695,6 +703,8 @@ export default function VideoEditor() {
|
||||
const cropSnapshotRef = useRef<CropRegion | null>(null);
|
||||
const mp4SupportRequestRef = useRef(0);
|
||||
const smokeExportStartedRef = useRef(false);
|
||||
const projectAutosaveTimeoutRef = useRef<number | null>(null);
|
||||
const projectSaveQueueRef = useRef<Promise<unknown>>(Promise.resolve());
|
||||
const [historyVersion, setHistoryVersion] = useState(0);
|
||||
const timelineRef = useRef<TimelineEditorHandle>(null);
|
||||
|
||||
@@ -982,6 +992,19 @@ export default function VideoEditor() {
|
||||
setPreviewVersion((version) => version + 1);
|
||||
}, []);
|
||||
|
||||
const clearPendingProjectAutosave = useCallback(() => {
|
||||
if (projectAutosaveTimeoutRef.current !== null) {
|
||||
window.clearTimeout(projectAutosaveTimeoutRef.current);
|
||||
projectAutosaveTimeoutRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const queueProjectSave = useCallback((task: () => Promise<boolean>) => {
|
||||
const run = projectSaveQueueRef.current.catch(() => undefined).then(task);
|
||||
projectSaveQueueRef.current = run.catch(() => undefined);
|
||||
return run;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
exporterRef.current?.cancel();
|
||||
@@ -999,6 +1022,10 @@ export default function VideoEditor() {
|
||||
window.clearTimeout(pendingFreshRecordingAutoSuggestTimeoutRef.current);
|
||||
pendingFreshRecordingAutoSuggestTimeoutRef.current = null;
|
||||
}
|
||||
if (projectAutosaveTimeoutRef.current !== null) {
|
||||
window.clearTimeout(projectAutosaveTimeoutRef.current);
|
||||
projectAutosaveTimeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -1561,20 +1588,24 @@ export default function VideoEditor() {
|
||||
setCurrentTime(0);
|
||||
setDuration(0);
|
||||
|
||||
setError(null);
|
||||
setVideoSourcePath(sourcePath);
|
||||
setVideoPath(await resolveVideoUrl(sourcePath));
|
||||
setCurrentProjectPath(path ?? null);
|
||||
pendingFreshRecordingAutoZoomPathRef.current = null;
|
||||
if (normalizedEditor.webcam.sourcePath) {
|
||||
await window.electronAPI.setCurrentRecordingSession?.({
|
||||
videoPath: sourcePath,
|
||||
webcamPath: normalizedEditor.webcam.sourcePath,
|
||||
timeOffsetMs: normalizedEditor.webcam.timeOffsetMs,
|
||||
});
|
||||
} else {
|
||||
await window.electronAPI.setCurrentVideoPath(sourcePath);
|
||||
}
|
||||
setError(null);
|
||||
setVideoSourcePath(sourcePath);
|
||||
setVideoPath(await resolveVideoUrl(sourcePath));
|
||||
setCurrentProjectPath(path ?? null);
|
||||
pendingFreshRecordingAutoZoomPathRef.current = null;
|
||||
if (normalizedEditor.webcam.sourcePath) {
|
||||
await window.electronAPI.setCurrentRecordingSession?.({
|
||||
videoPath: sourcePath,
|
||||
webcamPath: normalizedEditor.webcam.sourcePath,
|
||||
timeOffsetMs: normalizedEditor.webcam.timeOffsetMs,
|
||||
}, {
|
||||
preserveProjectPath: Boolean(path),
|
||||
});
|
||||
} else {
|
||||
await window.electronAPI.setCurrentVideoPath(sourcePath, {
|
||||
preserveProjectPath: Boolean(path),
|
||||
});
|
||||
}
|
||||
|
||||
setWallpaper(normalizedEditor.wallpaper);
|
||||
setShadowIntensity(normalizedEditor.shadowIntensity);
|
||||
@@ -1711,9 +1742,11 @@ export default function VideoEditor() {
|
||||
: webcamPath
|
||||
? webcam.timeOffsetMs
|
||||
: DEFAULT_WEBCAM_TIME_OFFSET_MS,
|
||||
}, {
|
||||
preserveProjectPath: Boolean(currentProjectPath),
|
||||
});
|
||||
},
|
||||
[currentSourcePath, webcam.timeOffsetMs],
|
||||
[currentProjectPath, currentSourcePath, webcam.timeOffsetMs],
|
||||
);
|
||||
|
||||
const syncActiveVideoSource = useCallback(
|
||||
@@ -1723,13 +1756,17 @@ export default function VideoEditor() {
|
||||
videoPath: sourcePath,
|
||||
webcamPath,
|
||||
timeOffsetMs: webcam.timeOffsetMs,
|
||||
}, {
|
||||
preserveProjectPath: Boolean(currentProjectPath),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await window.electronAPI.setCurrentVideoPath(sourcePath);
|
||||
await window.electronAPI.setCurrentVideoPath(sourcePath, {
|
||||
preserveProjectPath: Boolean(currentProjectPath),
|
||||
});
|
||||
},
|
||||
[webcam.timeOffsetMs],
|
||||
[currentProjectPath, webcam.timeOffsetMs],
|
||||
);
|
||||
|
||||
const handleUploadWebcam = useCallback(async () => {
|
||||
@@ -2237,83 +2274,106 @@ export default function VideoEditor() {
|
||||
}, []);
|
||||
|
||||
const saveProject = useCallback(
|
||||
async (forceSaveAs: boolean) => {
|
||||
if (!currentSourcePath) {
|
||||
toast.error("No video loaded");
|
||||
return false;
|
||||
}
|
||||
async (forceSaveAs: boolean, options?: SaveProjectOptions) => {
|
||||
clearPendingProjectAutosave();
|
||||
return queueProjectSave(async () => {
|
||||
if (!currentSourcePath) {
|
||||
if (!options?.silent) {
|
||||
toast.error("No video loaded");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const projectData =
|
||||
currentProjectSnapshot?.videoPath === currentSourcePath
|
||||
? currentProjectSnapshot
|
||||
: createProjectData(
|
||||
currentSourcePath,
|
||||
currentPersistedEditorState,
|
||||
lastSavedSnapshot?.projectId ?? null,
|
||||
);
|
||||
const shouldCaptureThumbnail = options?.captureThumbnail ?? true;
|
||||
const shouldRefreshLibrary = options?.refreshLibraryAfterSave ?? true;
|
||||
const shouldRemountPreview = options?.remountPreviewAfterSave ?? true;
|
||||
|
||||
const fileNameBase =
|
||||
currentSourcePath
|
||||
.split(/[\\/]/)
|
||||
.pop()
|
||||
?.replace(/\.[^.]+$/, "") || `project-${Date.now()}`;
|
||||
let targetProjectPath = forceSaveAs ? undefined : (currentProjectPath ?? undefined);
|
||||
try {
|
||||
const projectData =
|
||||
currentProjectSnapshot?.videoPath === currentSourcePath
|
||||
? currentProjectSnapshot
|
||||
: createProjectData(
|
||||
currentSourcePath,
|
||||
currentPersistedEditorState,
|
||||
lastSavedSnapshot?.projectId ?? null,
|
||||
);
|
||||
|
||||
if (!forceSaveAs && !targetProjectPath) {
|
||||
const activeProjectResult = await window.electronAPI.loadCurrentProjectFile();
|
||||
if (activeProjectResult.success && activeProjectResult.path) {
|
||||
targetProjectPath = activeProjectResult.path;
|
||||
setCurrentProjectPath(activeProjectResult.path);
|
||||
const fileNameBase =
|
||||
currentSourcePath
|
||||
.split(/[\\/]/)
|
||||
.pop()
|
||||
?.replace(/\.[^.]+$/, "") || `project-${Date.now()}`;
|
||||
let targetProjectPath = forceSaveAs ? undefined : (currentProjectPath ?? undefined);
|
||||
|
||||
if (!forceSaveAs && !targetProjectPath) {
|
||||
const activeProjectResult = await window.electronAPI.loadCurrentProjectFile();
|
||||
if (activeProjectResult.success && activeProjectResult.path) {
|
||||
targetProjectPath = activeProjectResult.path;
|
||||
setCurrentProjectPath(activeProjectResult.path);
|
||||
}
|
||||
}
|
||||
|
||||
const thumbnailDataUrl = shouldCaptureThumbnail
|
||||
? await captureProjectThumbnail()
|
||||
: undefined;
|
||||
|
||||
const result = await window.electronAPI.saveProjectFile(
|
||||
projectData,
|
||||
fileNameBase,
|
||||
targetProjectPath,
|
||||
thumbnailDataUrl,
|
||||
);
|
||||
|
||||
if (result.canceled) {
|
||||
if (!options?.silent) {
|
||||
toast.info("Project save canceled");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
if (!options?.silent) {
|
||||
toast.error(result.message || "Failed to save project");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.path) {
|
||||
setCurrentProjectPath(result.path);
|
||||
}
|
||||
setLastSavedSnapshot(
|
||||
cloneStructured(
|
||||
createProjectData(
|
||||
projectData.videoPath,
|
||||
projectData.editor,
|
||||
result.projectId ?? projectData.projectId ?? null,
|
||||
),
|
||||
),
|
||||
);
|
||||
if (shouldRefreshLibrary) {
|
||||
await refreshProjectLibrary();
|
||||
}
|
||||
|
||||
if (!options?.silent) {
|
||||
toast.success(`Project saved to ${result.path}`);
|
||||
}
|
||||
return true;
|
||||
} finally {
|
||||
if (shouldRemountPreview) {
|
||||
remountPreview();
|
||||
}
|
||||
}
|
||||
|
||||
const thumbnailDataUrl = await captureProjectThumbnail();
|
||||
|
||||
const result = await window.electronAPI.saveProjectFile(
|
||||
projectData,
|
||||
fileNameBase,
|
||||
targetProjectPath,
|
||||
thumbnailDataUrl,
|
||||
);
|
||||
|
||||
if (result.canceled) {
|
||||
toast.info("Project save canceled");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
toast.error(result.message || "Failed to save project");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (result.path) {
|
||||
setCurrentProjectPath(result.path);
|
||||
}
|
||||
setLastSavedSnapshot(
|
||||
cloneStructured(
|
||||
createProjectData(
|
||||
projectData.videoPath,
|
||||
projectData.editor,
|
||||
result.projectId ?? projectData.projectId ?? null,
|
||||
),
|
||||
),
|
||||
);
|
||||
await refreshProjectLibrary();
|
||||
|
||||
toast.success(`Project saved to ${result.path}`);
|
||||
return true;
|
||||
} finally {
|
||||
remountPreview();
|
||||
}
|
||||
});
|
||||
},
|
||||
[
|
||||
captureProjectThumbnail,
|
||||
clearPendingProjectAutosave,
|
||||
currentSourcePath,
|
||||
currentProjectPath,
|
||||
currentProjectSnapshot,
|
||||
currentPersistedEditorState,
|
||||
lastSavedSnapshot?.projectId,
|
||||
queueProjectSave,
|
||||
refreshProjectLibrary,
|
||||
remountPreview,
|
||||
],
|
||||
@@ -2342,6 +2402,27 @@ export default function VideoEditor() {
|
||||
}
|
||||
}, [saveProject]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentProjectPath || !hasUnsavedChanges) {
|
||||
clearPendingProjectAutosave();
|
||||
return;
|
||||
}
|
||||
|
||||
projectAutosaveTimeoutRef.current = window.setTimeout(() => {
|
||||
projectAutosaveTimeoutRef.current = null;
|
||||
void saveProject(false, {
|
||||
silent: true,
|
||||
remountPreviewAfterSave: false,
|
||||
refreshLibraryAfterSave: false,
|
||||
captureThumbnail: false,
|
||||
});
|
||||
}, PROJECT_AUTOSAVE_DELAY_MS);
|
||||
|
||||
return () => {
|
||||
clearPendingProjectAutosave();
|
||||
};
|
||||
}, [clearPendingProjectAutosave, currentProjectPath, hasUnsavedChanges, saveProject]);
|
||||
|
||||
/**
|
||||
* Saves the current project directly into the projects library under a chosen name.
|
||||
*/
|
||||
|
||||
@@ -240,7 +240,7 @@ describe("editorPreferences", () => {
|
||||
cursorClickBounceDuration: 350,
|
||||
cursorSway: 1.5,
|
||||
borderRadius: 18,
|
||||
padding: 30,
|
||||
padding: { top: 30, right: 30, bottom: 30, left: 30, linked: true },
|
||||
frame: DEFAULT_EDITOR_PREFERENCES.frame,
|
||||
aspectRatio: "4:5",
|
||||
exportEncodingMode: "quality",
|
||||
@@ -282,7 +282,7 @@ describe("editorPreferences", () => {
|
||||
cursorClickBounceDuration: 350,
|
||||
cursorSway: 1.5,
|
||||
borderRadius: 18,
|
||||
padding: 30,
|
||||
padding: { top: 30, right: 30, bottom: 30, left: 30, linked: true },
|
||||
frame: DEFAULT_EDITOR_PREFERENCES.frame,
|
||||
aspectRatio: "4:5",
|
||||
exportEncodingMode: "quality",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { fromFileUrl, toFileUrl } from "@/components/video-editor/projectPersist
|
||||
const NOOP = () => undefined;
|
||||
const REMOTE_MEDIA_URL_PATTERN = /^(https?:|blob:|data:)/i;
|
||||
const LOOPBACK_MEDIA_HOSTS = new Set(["127.0.0.1", "localhost"]);
|
||||
const BUNDLED_ASSET_PATH_PREFIXES = ["/wallpapers/", "/app-icons/"];
|
||||
|
||||
export function isAbsoluteLocalPath(resource: string) {
|
||||
return (
|
||||
@@ -12,6 +13,10 @@ export function isAbsoluteLocalPath(resource: string) {
|
||||
);
|
||||
}
|
||||
|
||||
function isBundledAssetPath(resource: string) {
|
||||
return BUNDLED_ASSET_PATH_PREFIXES.some((prefix) => resource.startsWith(prefix));
|
||||
}
|
||||
|
||||
function getLocalMediaServerPath(resource: string) {
|
||||
if (!/^https?:\/\//i.test(resource)) {
|
||||
return null;
|
||||
@@ -44,6 +49,10 @@ export function getLocalFilePath(resource: string) {
|
||||
return fromFileUrl(resource);
|
||||
}
|
||||
|
||||
if (isBundledAssetPath(resource)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return isAbsoluteLocalPath(resource) ? resource : null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user