Skip to content
Open
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
516 changes: 516 additions & 0 deletions apps/desktop/src-tauri/src/importer.rs

Large diffs are not rendered by default.

16 changes: 8 additions & 8 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
mod audio;
mod audio_meter;
mod auth;
mod camera;
mod captions;
mod deeplink_actions;
mod editor_window;
mod export;
mod fake_window;
mod flags;
mod general_settings;
mod hotkeys;
mod importer;
mod notifications;
mod permissions;
mod platform;
mod recording;
// mod resource;
mod audio_meter;
mod editor_window;
mod export;
mod fake_window;
// mod live_state;
mod presets;
mod recording;
mod tray;
mod upload;
mod web_api;
Expand Down Expand Up @@ -1825,7 +1824,8 @@ pub async fn run(recording_logging_handle: LoggingHandle) {
captions::download_whisper_model,
captions::check_model_exists,
captions::delete_whisper_model,
captions::export_captions_srt
captions::export_captions_srt,
importer::import_video_file,
])
.events(tauri_specta::collect_events![
RecordingOptionsChanged,
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/routes/(window-chrome)/settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ export default function Settings(props: RouteSectionProps) {
name: "Integrations",
icon: IconLucideUnplug,
},
{
href: "importer",
name: "Importer",
icon: IconCapFile,
},
{
href: "license",
name: "License",
Expand Down
94 changes: 94 additions & 0 deletions apps/desktop/src/routes/(window-chrome)/settings/importer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Button } from "@cap/ui-solid";
import { open } from "@tauri-apps/plugin-dialog";
import { createSignal, Show } from "solid-js";
import toast from "solid-toast";
import { commands } from "~/utils/tauri";

export default function ImporterSettings() {
const [isImporting, setIsImporting] = createSignal(false);
const [progress, setProgress] = createSignal<string>("");

const handleImport = async (filePath: string) => {
try {
setIsImporting(true);
setProgress("Processing video file...");

const projectPath = await commands.importVideoFile(filePath);

toast.success("Video imported successfully!");
setProgress("");
} catch (error) {
console.error("Import error:", error);
toast.error(`Failed to import video: ${error}`);
setProgress("");
} finally {
setIsImporting(false);
}
};

const handleFileSelect = async () => {
try {
const selected = await open({
multiple: false,
filters: [
{
name: "Video",
extensions: ["mp4", "mov", "webm", "m4v"],
},
],
});

if (selected) {
await handleImport(selected as string);
}
} catch (error) {
console.error("File selection error:", error);
toast.error("Failed to select file");
}
};

return (
<div class="flex flex-col w-full h-full">
<div class="flex-1 custom-scroll">
<div class="p-4 space-y-4">
<div class="mb-6">
<h2 class="text-gray-12 text-lg font-medium mb-2">Import Videos</h2>
<p class="text-gray-11 text-sm">
Import existing video files into Cap to edit them with the Cap
Editor. Most common video formats are supported including .mp4,
.mov, .webm and .m4v.
</p>
</div>

<div class="flex flex-col items-center justify-center space-y-4 p-12 border-2 border-dashed border-gray-300 rounded-lg">
<svg
class="w-12 h-12 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"
/>
</svg>

<div class="text-center">
<p class="text-gray-12 font-medium mb-1">
{isImporting() ? progress() : "Select a video file to import"}
</p>
</div>

<Show when={!isImporting()}>
<Button variant="primary" size="md" onClick={handleFileSelect}>
Select Video File
</Button>
</Show>
</div>
</div>
</div>
</div>
);
}
10 changes: 5 additions & 5 deletions apps/desktop/src/routes/editor/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,24 +73,24 @@ export function Editor() {
}

function Inner() {
const { project, editorState, setEditorState } = useEditorContext();
const { project, editorState, setEditorState, videoFps } = useEditorContext();

onMount(() =>
events.editorStateChanged.listen((e) => {
renderFrame.clear();
setEditorState("playbackTime", e.payload.playhead_position / FPS);
setEditorState("playbackTime", e.payload.playhead_position / videoFps());
})
);

const renderFrame = throttle((time: number) => {
if (!editorState.playing) {
events.renderFrameEvent.emit({
frame_number: Math.max(Math.floor(time * FPS), 0),
fps: FPS,
frame_number: Math.max(Math.floor(time * videoFps()), 0),
fps: videoFps(),
resolution_base: OUTPUT_SIZE,
});
}
}, 1000 / FPS);
}, 1000 / videoFps());

const frameNumberToRender = createMemo(() => {
const preview = editorState.previewTime;
Expand Down
15 changes: 10 additions & 5 deletions apps/desktop/src/routes/editor/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function Player() {
setEditorState,
zoomOutLimit,
setProject,
videoFps,
} = useEditorContext();

// Load captions on mount
Expand Down Expand Up @@ -120,15 +121,17 @@ export function Player() {
await commands.stopPlayback();
setEditorState("playbackTime", 0);
await commands.seekTo(0);
await commands.startPlayback(FPS, OUTPUT_SIZE);
await commands.startPlayback(videoFps(), OUTPUT_SIZE);
setEditorState("playing", true);
} else if (editorState.playing) {
await commands.stopPlayback();
setEditorState("playing", false);
} else {
// Ensure we seek to the current playback time before starting playback
await commands.seekTo(Math.floor(editorState.playbackTime * FPS));
await commands.startPlayback(FPS, OUTPUT_SIZE);
await commands.seekTo(
Math.floor(editorState.playbackTime * videoFps())
);
await commands.startPlayback(videoFps(), OUTPUT_SIZE);
setEditorState("playing", true);
}
if (editorState.playing) setEditorState("previewTime", null);
Expand All @@ -146,7 +149,9 @@ export function Player() {
if (!editorState.playing) {
if (prevTime !== null) setEditorState("playbackTime", prevTime);

await commands.seekTo(Math.floor(editorState.playbackTime * FPS));
await commands.seekTo(
Math.floor(editorState.playbackTime * videoFps())
);
}

await handlePlayPauseClick();
Expand Down Expand Up @@ -320,8 +325,8 @@ function PreviewCanvas() {

return (
<div
ref={setCanvasContainerRef}
class="relative flex-1 justify-center items-center"
ref={setCanvasContainerRef}
>
<Show when={latestFrame()}>
{(currentFrame) => {
Expand Down
18 changes: 13 additions & 5 deletions apps/desktop/src/routes/editor/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
createEffect,
createResource,
createSignal,
createMemo,
on,
} from "solid-js";
import { createStore, produce, reconcile, unwrap } from "solid-js/store";
Expand Down Expand Up @@ -63,6 +64,17 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider(
props.editorInstance.savedProjectConfig
);

const videoFps = createMemo(() => {
const meta = props.meta();
if ("display" in meta) {
return meta.display.fps || 30;
}
if ("segments" in meta && meta.segments.length > 0) {
return meta.segments[0].display.fps || 30;
}
return 30;
});

const projectActions = {
splitClipSegment: (time: number) => {
setProject(
Expand Down Expand Up @@ -231,7 +243,6 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider(
| { type: "zoom"; index: number }
| { type: "clip"; index: number },
transform: {
// visible seconds
zoom: zoomOutLimit(),
updateZoom(z: number, origin: number) {
const { zoom, position } = updateZoom(
Expand All @@ -250,7 +261,6 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider(
transform.setPosition(position);
});
},
// number of seconds of leftmost point
position: 0,
setPosition(p: number) {
setEditorState(
Expand Down Expand Up @@ -288,9 +298,9 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider(
zoomOutLimit,
exportState,
setExportState,
videoFps,
};
},
// biome-ignore lint/style/noNonNullAssertion: it's ok
null!
);

Expand Down Expand Up @@ -384,8 +394,6 @@ export const [EditorInstanceContextProvider, useEditorInstanceContext] =
function createStoreHistory<T extends Static>(
...[state, setState]: ReturnType<typeof createStore<T>>
) {
// not working properly yet
// const getDelta = captureStoreUpdates(state);

const [pauseCount, setPauseCount] = createSignal(0);

Expand Down
3 changes: 3 additions & 0 deletions apps/desktop/src/utils/tauri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,9 @@ async deleteWhisperModel(modelPath: string) : Promise<null> {
*/
async exportCaptionsSrt(videoId: string) : Promise<string | null> {
return await TAURI_INVOKE("export_captions_srt", { videoId });
},
async importVideoFile(videoPath: string) : Promise<string> {
return await TAURI_INVOKE("import_video_file", { videoPath });
}
}

Expand Down
Loading