Skip to content
Merged
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: 5 additions & 2 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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; webcamPath: string | null }>;
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 };
Expand Down
17 changes: 16 additions & 1 deletion electron/ipc/project/manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand All @@ -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");
});
});
49 changes: 41 additions & 8 deletions electron/ipc/project/manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,20 +68,37 @@ export async function isAllowedLocalMediaPath(candidatePath: string) {
return isAllowedLocalReadPath(normalizedCandidatePath);
}

export async function rememberApprovedLocalReadPath(filePath?: string | null) {
async function collectApprovedLocalReadPaths(filePath?: string | null): Promise<string[]> {
const normalizedPath = normalizeVideoSourcePath(filePath);
if (!normalizedPath) {
return;
return [];
}

const resolvedPath = normalizePath(normalizedPath);
approvedLocalReadPaths.add(resolvedPath);
const approvedPaths = [normalizePath(normalizedPath)];

try {
approvedLocalReadPaths.add(await fs.realpath(resolvedPath));
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 approvedPaths = await collectApprovedLocalReadPaths(normalizedPath);
for (const approvedPath of approvedPaths) {
approvedLocalReadPaths.add(approvedPath);
}
}

export async function resolveApprovedLocalMediaPath(candidatePath: string): Promise<string | null> {
Expand All @@ -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<
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -383,4 +417,3 @@ export function isTrustedProjectPath(filePath?: string | null): boolean {
if (!filePath || !currentProjectPath) return false;
return normalizePath(filePath) === normalizePath(currentProjectPath);
}

12 changes: 8 additions & 4 deletions electron/ipc/register/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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({
Expand All @@ -563,7 +565,9 @@ export function registerProjectHandlers() {
currentRecordingSession!.videoPath,
currentRecordingSession!.webcamPath,
])
setCurrentProjectPath(null)
if (!options?.preserveProjectPath) {
setCurrentProjectPath(null)
}
await persistRecordingSessionManifest(currentRecordingSession!)
return { success: true }
})
Expand Down
8 changes: 4 additions & 4 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
Binary file added public/wallpapers/energy-17.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/energy-19.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/glassmorphism-3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/glassmorphism-4.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/ipad-17-dark.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/ipad-17-light.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/iridescent-9.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/midnight-8.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/sequoia-blue-orange.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/sequoia-blue.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/sonoma-clouds.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/sonoma-dark.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/sonoma-evening.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/sonoma-horizon.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/sonoma-light.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/tahoe-dark.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/tahoe-light.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/ventura-dark.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/wallpapers/ventura.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 6 additions & 2 deletions src/components/video-editor/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1776,14 +1776,18 @@ export function SettingsPanel({
<div
key={g}
className={wallpaperTileClass(gradient === g)}
style={{ background: g }}
aria-label={`Gradient ${idx + 1}`}
onClick={() => {
setGradient(g);
onWallpaperChange(g);
}}
role="button"
/>
>
<div
className="absolute inset-[1px] overflow-hidden rounded-[8px]"
style={{ background: g }}
/>
</div>
))}
</div>
)}
Expand Down
Loading