Skip to content

Commit d8e34a1

Browse files
authored
Merge pull request #686 from webadderallorg/feat/export-caption-sidecars
feat: export caption sidecars and merge editor follow-ups
2 parents 952d63d + def1bac commit d8e34a1

33 files changed

Lines changed: 1075 additions & 706 deletions

electron/electron-env.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,7 @@ interface Window {
752752
projectData: unknown,
753753
projectName: string,
754754
thumbnailDataUrl?: string | null,
755+
mode?: "rename" | "copy",
755756
) => Promise<{
756757
success: boolean;
757758
path?: string;

electron/ipc/register/project.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,12 @@ function normalizeProjectSaveName(projectName?: string | null) {
8080
return sanitizedName || null;
8181
}
8282

83+
type NamedProjectSaveMode = "rename" | "copy";
84+
85+
function normalizeNamedProjectSaveMode(value: unknown): NamedProjectSaveMode {
86+
return value === "copy" ? "copy" : "rename";
87+
}
88+
8389
/**
8490
* Extracts the persisted source video path from a saved project payload.
8591
*/
@@ -292,8 +298,10 @@ export function registerProjectHandlers() {
292298
try {
293299
const projectsDir = await getProjectsDir()
294300
const preparedProject = ensureProjectDataHasProjectId(projectData)
295-
const trustedExistingProjectPath = isTrustedProjectPath(existingProjectPath)
296-
? existingProjectPath
301+
const trustedExistingProjectPath = existingProjectPath &&
302+
path.extname(existingProjectPath).toLowerCase() === `.${PROJECT_FILE_EXTENSION}` &&
303+
(isTrustedProjectPath(existingProjectPath) || isPathInsideDirectory(existingProjectPath, projectsDir))
304+
? path.resolve(existingProjectPath)
297305
: null
298306

299307
if (trustedExistingProjectPath) {
@@ -309,6 +317,13 @@ export function registerProjectHandlers() {
309317
}
310318
}
311319

320+
if (existingProjectPath) {
321+
return {
322+
success: false,
323+
message: 'Project path is no longer trusted. Use Save As to choose a project file.',
324+
}
325+
}
326+
312327
const safeName = normalizeProjectSaveName(suggestedName) || `project-${Date.now()}`
313328
const defaultName = `${safeName}.${PROJECT_FILE_EXTENSION}`
314329

@@ -351,7 +366,7 @@ export function registerProjectHandlers() {
351366
}
352367
})
353368

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

364379
const projectsDir = await getProjectsDir()
365-
const preparedProject = ensureProjectDataHasProjectId(projectData)
380+
const namedSaveMode = normalizeNamedProjectSaveMode(mode)
366381
const activeProjectPath = isTrustedProjectPath(currentProjectPath)
367382
? currentProjectPath
368383
: null
369384
const targetProjectPath = path.join(
370385
projectsDir,
371386
`${normalizedProjectName}.${PROJECT_FILE_EXTENSION}`,
372387
)
388+
const [activeResolvedPath, targetResolvedPath] = await Promise.all([
389+
activeProjectPath ? resolveComparablePath(activeProjectPath) : Promise.resolve(null),
390+
resolveComparablePath(targetProjectPath),
391+
])
392+
const isSavingToDifferentPath =
393+
!activeResolvedPath || activeResolvedPath !== targetResolvedPath
394+
const preparedProject =
395+
namedSaveMode === "copy" && isSavingToDifferentPath
396+
? (() => {
397+
const projectId = randomUUID()
398+
return {
399+
projectId,
400+
projectData: withProjectId(projectData, projectId),
401+
}
402+
})()
403+
: ensureProjectDataHasProjectId(projectData)
373404

374405
const overwriteCheck = await ensureNamedProjectSaveDoesNotOverwriteDifferentProject(
375406
targetProjectPath,
@@ -384,13 +415,7 @@ export function registerProjectHandlers() {
384415
await saveProjectThumbnail(targetProjectPath, thumbnailDataUrl)
385416
await rememberRecentProject(targetProjectPath)
386417

387-
if (activeProjectPath) {
388-
const [activeResolvedPath, targetResolvedPath] = await Promise.all([
389-
resolveComparablePath(activeProjectPath),
390-
resolveComparablePath(targetProjectPath),
391-
])
392-
393-
if (activeResolvedPath !== targetResolvedPath) {
418+
if (namedSaveMode === "rename" && activeProjectPath && isSavingToDifferentPath) {
394419
await fs.unlink(activeProjectPath).catch((unlinkError: NodeJS.ErrnoException) => {
395420
if (unlinkError.code !== 'ENOENT') {
396421
throw unlinkError
@@ -407,7 +432,6 @@ export function registerProjectHandlers() {
407432
}
408433
}
409434
await saveRecentProjectPaths(filteredRecentProjectPaths)
410-
}
411435
}
412436

413437
setCurrentProjectPath(targetProjectPath)

electron/preload.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -783,12 +783,14 @@ contextBridge.exposeInMainWorld("electronAPI", {
783783
projectData: unknown,
784784
projectName: string,
785785
thumbnailDataUrl?: string | null,
786+
mode?: "rename" | "copy",
786787
) => {
787788
return ipcRenderer.invoke(
788789
"save-project-file-named",
789790
projectData,
790791
projectName,
791792
thumbnailDataUrl,
793+
mode,
792794
);
793795
},
794796
loadProjectFile: () => {

src/components/video-editor/SettingsPanel.tsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import type {
7373
ZoomTransitionEasing,
7474
} from "./types";
7575
import {
76+
ADVANCED_VERTICAL_PADDING_MAX,
7677
DEFAULT_AUTO_CAPTION_SETTINGS,
7778
DEFAULT_CROP_REGION,
7879
DEFAULT_CURSOR_CLICK_BOUNCE,
@@ -107,6 +108,7 @@ import {
107108
} from "./videoPlayback/uploadedCursorAssets";
108109
import { WebcamCropControl } from "./WebcamCropControl";
109110
import {
111+
getCropMatchedWebcamHeightPercent,
110112
getWebcamPositionForPreset,
111113
normalizeWebcamCropRegion,
112114
resolveWebcamCorner,
@@ -1662,6 +1664,8 @@ export function SettingsPanel({
16621664
const webcamPositionPreset = webcam?.positionPreset ?? DEFAULT_WEBCAM_POSITION_PRESET;
16631665
const webcamPositionX = webcam?.positionX ?? DEFAULT_WEBCAM_POSITION_X;
16641666
const webcamPositionY = webcam?.positionY ?? DEFAULT_WEBCAM_POSITION_Y;
1667+
const webcamWidth = webcam?.width ?? webcam?.size ?? DEFAULT_WEBCAM_SIZE;
1668+
const webcamHeight = webcam?.height ?? webcam?.size ?? DEFAULT_WEBCAM_SIZE;
16651669
const webcamCrop = normalizeWebcamCropRegion(webcam?.cropRegion);
16661670

16671671
const getWallpaperTileState = (candidateValue: string, previewPath?: string) => {
@@ -2413,7 +2417,7 @@ export function SettingsPanel({
24132417
value={padding.top}
24142418
defaultValue={DEFAULT_PADDING.top}
24152419
min={0}
2416-
max={100}
2420+
max={ADVANCED_VERTICAL_PADDING_MAX}
24172421
step={1}
24182422
onChange={(v) => handlePaddingSideChange("top", v)}
24192423
formatValue={(v) => `${v}%`}
@@ -2424,7 +2428,7 @@ export function SettingsPanel({
24242428
value={padding.bottom}
24252429
defaultValue={DEFAULT_PADDING.bottom}
24262430
min={0}
2427-
max={100}
2431+
max={ADVANCED_VERTICAL_PADDING_MAX}
24282432
step={1}
24292433
onChange={(v) => handlePaddingSideChange("bottom", v)}
24302434
formatValue={(v) => `${v}%`}
@@ -3871,13 +3875,24 @@ export function SettingsPanel({
38713875
/>
38723876
</div>
38733877
<SliderControl
3874-
label={tSettings("effects.webcamSize")}
3875-
value={webcam?.size ?? DEFAULT_WEBCAM_SIZE}
3878+
label={tSettings("effects.webcamWidth", "Webcam Width")}
3879+
value={webcamWidth}
38763880
defaultValue={DEFAULT_WEBCAM_SIZE}
38773881
min={10}
38783882
max={100}
38793883
step={1}
3880-
onChange={(v) => updateWebcam({ size: v })}
3884+
onChange={(v) => updateWebcam({ width: v, size: v })}
3885+
formatValue={(v) => `${Math.round(v)}%`}
3886+
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
3887+
/>
3888+
<SliderControl
3889+
label={tSettings("effects.webcamHeight", "Webcam Height")}
3890+
value={webcamHeight}
3891+
defaultValue={DEFAULT_WEBCAM_SIZE}
3892+
min={10}
3893+
max={100}
3894+
step={1}
3895+
onChange={(v) => updateWebcam({ height: v })}
38813896
formatValue={(v) => `${Math.round(v)}%`}
38823897
parseInput={(text) => parseFloat(text.replace(/%$/, ""))}
38833898
/>
@@ -3903,7 +3918,20 @@ export function SettingsPanel({
39033918
previewCurrentTime={webcamPreviewCurrentTime}
39043919
previewPlaying={webcamPreviewPlaying}
39053920
previewTimeOffsetMs={webcam?.timeOffsetMs}
3906-
onCropChange={(cropRegion) => updateWebcam({ cropRegion })}
3921+
onCropChange={(cropRegion, previewFrame) =>
3922+
updateWebcam({
3923+
cropRegion,
3924+
height: previewFrame
3925+
? getCropMatchedWebcamHeightPercent(
3926+
webcamWidth,
3927+
webcamHeight,
3928+
previewFrame.width,
3929+
previewFrame.height,
3930+
cropRegion,
3931+
)
3932+
: webcamHeight,
3933+
})
3934+
}
39073935
/>
39083936
</div>
39093937
<div className="rounded-lg bg-foreground/[0.03] px-2.5 py-2">

0 commit comments

Comments
 (0)