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
1 change: 1 addition & 0 deletions electron/electron-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,7 @@ interface Window {
projectData: unknown,
projectName: string,
thumbnailDataUrl?: string | null,
mode?: "rename" | "copy",
) => Promise<{
success: boolean;
path?: string;
Expand Down
48 changes: 36 additions & 12 deletions electron/ipc/register/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ function normalizeProjectSaveName(projectName?: string | null) {
return sanitizedName || null;
}

type NamedProjectSaveMode = "rename" | "copy";

function normalizeNamedProjectSaveMode(value: unknown): NamedProjectSaveMode {
return value === "copy" ? "copy" : "rename";
}

/**
* Extracts the persisted source video path from a saved project payload.
*/
Expand Down Expand Up @@ -292,8 +298,10 @@ export function registerProjectHandlers() {
try {
const projectsDir = await getProjectsDir()
const preparedProject = ensureProjectDataHasProjectId(projectData)
const trustedExistingProjectPath = isTrustedProjectPath(existingProjectPath)
? existingProjectPath
const trustedExistingProjectPath = existingProjectPath &&
path.extname(existingProjectPath).toLowerCase() === `.${PROJECT_FILE_EXTENSION}` &&
(isTrustedProjectPath(existingProjectPath) || isPathInsideDirectory(existingProjectPath, projectsDir))
? path.resolve(existingProjectPath)
: null

if (trustedExistingProjectPath) {
Expand All @@ -309,6 +317,13 @@ export function registerProjectHandlers() {
}
}

if (existingProjectPath) {
return {
success: false,
message: 'Project path is no longer trusted. Use Save As to choose a project file.',
}
}

const safeName = normalizeProjectSaveName(suggestedName) || `project-${Date.now()}`
const defaultName = `${safeName}.${PROJECT_FILE_EXTENSION}`

Expand Down Expand Up @@ -351,7 +366,7 @@ export function registerProjectHandlers() {
}
})

ipcMain.handle('save-project-file-named', async (_, projectData: unknown, projectName: string, thumbnailDataUrl?: string | null) => {
ipcMain.handle('save-project-file-named', async (_, projectData: unknown, projectName: string, thumbnailDataUrl?: string | null, mode?: unknown) => {
try {
const normalizedProjectName = normalizeProjectSaveName(projectName)
if (!normalizedProjectName) {
Expand All @@ -362,14 +377,30 @@ export function registerProjectHandlers() {
}

const projectsDir = await getProjectsDir()
const preparedProject = ensureProjectDataHasProjectId(projectData)
const namedSaveMode = normalizeNamedProjectSaveMode(mode)
const activeProjectPath = isTrustedProjectPath(currentProjectPath)
? currentProjectPath
: null
const targetProjectPath = path.join(
projectsDir,
`${normalizedProjectName}.${PROJECT_FILE_EXTENSION}`,
)
const [activeResolvedPath, targetResolvedPath] = await Promise.all([
activeProjectPath ? resolveComparablePath(activeProjectPath) : Promise.resolve(null),
resolveComparablePath(targetProjectPath),
])
const isSavingToDifferentPath =
!activeResolvedPath || activeResolvedPath !== targetResolvedPath
const preparedProject =
namedSaveMode === "copy" && isSavingToDifferentPath
? (() => {
const projectId = randomUUID()
return {
projectId,
projectData: withProjectId(projectData, projectId),
}
})()
: ensureProjectDataHasProjectId(projectData)

const overwriteCheck = await ensureNamedProjectSaveDoesNotOverwriteDifferentProject(
targetProjectPath,
Expand All @@ -384,13 +415,7 @@ export function registerProjectHandlers() {
await saveProjectThumbnail(targetProjectPath, thumbnailDataUrl)
await rememberRecentProject(targetProjectPath)

if (activeProjectPath) {
const [activeResolvedPath, targetResolvedPath] = await Promise.all([
resolveComparablePath(activeProjectPath),
resolveComparablePath(targetProjectPath),
])

if (activeResolvedPath !== targetResolvedPath) {
if (namedSaveMode === "rename" && activeProjectPath && isSavingToDifferentPath) {
await fs.unlink(activeProjectPath).catch((unlinkError: NodeJS.ErrnoException) => {
if (unlinkError.code !== 'ENOENT') {
throw unlinkError
Expand All @@ -407,7 +432,6 @@ export function registerProjectHandlers() {
}
}
await saveRecentProjectPaths(filteredRecentProjectPaths)
}
}

setCurrentProjectPath(targetProjectPath)
Expand Down
2 changes: 2 additions & 0 deletions electron/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -783,12 +783,14 @@ contextBridge.exposeInMainWorld("electronAPI", {
projectData: unknown,
projectName: string,
thumbnailDataUrl?: string | null,
mode?: "rename" | "copy",
) => {
return ipcRenderer.invoke(
"save-project-file-named",
projectData,
projectName,
thumbnailDataUrl,
mode,
);
},
loadProjectFile: () => {
Expand Down
40 changes: 34 additions & 6 deletions src/components/video-editor/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import type {
ZoomTransitionEasing,
} from "./types";
import {
ADVANCED_VERTICAL_PADDING_MAX,
DEFAULT_AUTO_CAPTION_SETTINGS,
DEFAULT_CROP_REGION,
DEFAULT_CURSOR_CLICK_BOUNCE,
Expand Down Expand Up @@ -107,6 +108,7 @@ import {
} from "./videoPlayback/uploadedCursorAssets";
import { WebcamCropControl } from "./WebcamCropControl";
import {
getCropMatchedWebcamHeightPercent,
getWebcamPositionForPreset,
normalizeWebcamCropRegion,
resolveWebcamCorner,
Expand Down Expand Up @@ -1662,6 +1664,8 @@ export function SettingsPanel({
const webcamPositionPreset = webcam?.positionPreset ?? DEFAULT_WEBCAM_POSITION_PRESET;
const webcamPositionX = webcam?.positionX ?? DEFAULT_WEBCAM_POSITION_X;
const webcamPositionY = webcam?.positionY ?? DEFAULT_WEBCAM_POSITION_Y;
const webcamWidth = webcam?.width ?? webcam?.size ?? DEFAULT_WEBCAM_SIZE;
const webcamHeight = webcam?.height ?? webcam?.size ?? DEFAULT_WEBCAM_SIZE;
const webcamCrop = normalizeWebcamCropRegion(webcam?.cropRegion);

const getWallpaperTileState = (candidateValue: string, previewPath?: string) => {
Expand Down Expand Up @@ -2413,7 +2417,7 @@ export function SettingsPanel({
value={padding.top}
defaultValue={DEFAULT_PADDING.top}
min={0}
max={100}
max={ADVANCED_VERTICAL_PADDING_MAX}
step={1}
onChange={(v) => handlePaddingSideChange("top", v)}
formatValue={(v) => `${v}%`}
Expand All @@ -2424,7 +2428,7 @@ export function SettingsPanel({
value={padding.bottom}
defaultValue={DEFAULT_PADDING.bottom}
min={0}
max={100}
max={ADVANCED_VERTICAL_PADDING_MAX}
step={1}
onChange={(v) => handlePaddingSideChange("bottom", v)}
formatValue={(v) => `${v}%`}
Expand Down Expand Up @@ -3871,13 +3875,24 @@ export function SettingsPanel({
/>
</div>
<SliderControl
label={tSettings("effects.webcamSize")}
value={webcam?.size ?? DEFAULT_WEBCAM_SIZE}
label={tSettings("effects.webcamWidth", "Webcam Width")}
value={webcamWidth}
defaultValue={DEFAULT_WEBCAM_SIZE}
min={10}
max={100}
step={1}
onChange={(v) => updateWebcam({ size: v })}
onChange={(v) => updateWebcam({ width: v, size: v })}
formatValue={(v) => `${Math.round(v)}%`}
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
/>
<SliderControl
label={tSettings("effects.webcamHeight", "Webcam Height")}
value={webcamHeight}
defaultValue={DEFAULT_WEBCAM_SIZE}
min={10}
max={100}
step={1}
onChange={(v) => updateWebcam({ height: v })}
formatValue={(v) => `${Math.round(v)}%`}
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
/>
Expand All @@ -3903,7 +3918,20 @@ export function SettingsPanel({
previewCurrentTime={webcamPreviewCurrentTime}
previewPlaying={webcamPreviewPlaying}
previewTimeOffsetMs={webcam?.timeOffsetMs}
onCropChange={(cropRegion) => updateWebcam({ cropRegion })}
onCropChange={(cropRegion, previewFrame) =>
updateWebcam({
cropRegion,
height: previewFrame
? getCropMatchedWebcamHeightPercent(
webcamWidth,
webcamHeight,
previewFrame.width,
previewFrame.height,
cropRegion,
)
: webcamHeight,
})
}
/>
</div>
<div className="rounded-lg bg-foreground/[0.03] px-2.5 py-2">
Expand Down
Loading