diff --git a/Cargo.lock b/Cargo.lock index f521f0bb9..39327b784 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -952,6 +952,7 @@ dependencies = [ "device_query 2.1.0", "dirs", "dotenvy_macro", + "either", "ffmpeg-next", "flume 0.11.0", "futures", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 004892db7..dedabded8 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -70,6 +70,7 @@ device_query = "2.1.0" base64 = "0.22.1" reqwest = { version = "0.12.7", features = ["json", "stream", "multipart"] } dotenvy_macro = "0.15.7" +either = "1.13.0" global-hotkey = "0.6.3" rand = "0.8.5" cpal.workspace = true diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index d199d89d3..041ddc455 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -51,6 +51,7 @@ use scap::frame::VideoFrame; use serde::{Deserialize, Serialize}; use serde_json::json; use specta::Type; +use either::Either; use std::collections::BTreeMap; use std::time::Duration; use std::{ @@ -1170,6 +1171,22 @@ async fn set_project_config( Ok(()) } +#[tauri::command] +#[specta::specta] +async fn rename_project( + editor_instance: WindowEditorInstance, + name: String, +) -> Result<(), String> { + let mut meta = + RecordingMeta::load_for_project(&editor_instance.project_path).map_err(|e| e.to_string())?; + meta.pretty_name = name; + meta.save_for_project() + .map_err(|e| match e { + Either::Left(e) => e.to_string(), + Either::Right(e) => e.to_string(), + }) +} + #[tauri::command] #[specta::specta] async fn list_audio_devices() -> Result, ()> { @@ -1981,6 +1998,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) { stop_playback, set_playhead_position, set_project_config, + rename_project, permissions::open_permission_settings, permissions::do_permissions_check, permissions::request_permission, diff --git a/apps/desktop/src/routes/editor/Editor.tsx b/apps/desktop/src/routes/editor/Editor.tsx index d2169eb65..68e0ca7f7 100644 --- a/apps/desktop/src/routes/editor/Editor.tsx +++ b/apps/desktop/src/routes/editor/Editor.tsx @@ -20,7 +20,7 @@ import { cx } from "cva"; import Cropper, { cropToFloor } from "~/components/Cropper"; import { Toggle } from "~/components/Toggle"; import Tooltip from "~/components/Tooltip"; -import { events, type Crop } from "~/utils/tauri"; +import { events, commands, type Crop } from "~/utils/tauri"; import { ConfigSidebar } from "./ConfigSidebar"; import { EditorContextProvider, @@ -34,13 +34,7 @@ import { ExportDialog } from "./ExportDialog"; import { Header } from "./Header"; import { Player } from "./Player"; import { Timeline } from "./Timeline"; -import { - Dialog, - DialogContent, - EditorButton, - Input, - Subfield, -} from "./ui"; +import { Dialog, DialogContent, EditorButton, Input, Subfield } from "./ui"; export function Editor() { return ( @@ -58,7 +52,7 @@ export function Editor() { const d = ctx.metaQuery.data; if (!d) throw new Error( - "metaQuery.data is undefined - how did this happen?" + "metaQuery.data is undefined - how did this happen?", ); return d; }, @@ -85,7 +79,7 @@ function Inner() { events.editorStateChanged.listen((e) => { renderFrame.clear(); setEditorState("playbackTime", e.payload.playhead_position / FPS); - }) + }), ); const renderFrame = throttle((time: number) => { @@ -108,14 +102,14 @@ function Inner() { on(frameNumberToRender, (number) => { if (editorState.playing) return; renderFrame(number); - }) + }), ); createEffect( on( () => trackDeep(project), - () => renderFrame(editorState.playbackTime) - ) + () => renderFrame(editorState.playbackTime), + ), ); return ( @@ -222,7 +216,7 @@ function Dialogs() { > {(dialog) => { const [name, setName] = createSignal( - presets.query.data?.presets[dialog().presetIndex].name! + presets.query.data?.presets[dialog().presetIndex].name!, ); const renamePreset = createMutation(() => ({ @@ -255,6 +249,48 @@ function Dialogs() { ); }} + { + const d = dialog(); + if (d.type === "renameProject") return d; + })()} + > + {() => { + const { meta, refetchMeta } = useEditorContext(); + const [name, setName] = createSignal(meta().prettyName); + + const renameProject = createMutation(() => ({ + mutationFn: async () => { + await commands.renameProject(name()); + await refetchMeta(); + }, + onSuccess: () => { + setDialog((d) => ({ ...d, open: false })); + }, + })); + + return ( + renameProject.mutate()} + > + Rename + + } + > + + setName(e.currentTarget.value)} + /> + + ); + }} + { const d = dialog(); @@ -309,7 +345,7 @@ function Dialogs() { createStore({ showGrid: false, }), - { name: "cropOptionsState" } + { name: "cropOptionsState" }, ); const display = editorInstance.recordings.segments[0].display; @@ -398,7 +434,7 @@ function Dialogs() { "flex items-center bg-gray-3 justify-center text-center rounded-[0.5rem] h-[2rem] w-[2rem] border text-[0.875rem] focus:border-blue-9 outline-none transition-colors duration-200", cropOptions.showGrid ? "bg-gray-3 text-blue-9 border-blue-9" - : "text-gray-12" + : "text-gray-12", )} onClick={() => setCropOptions("showGrid", (s) => !s) @@ -440,7 +476,7 @@ function Dialogs() { class="shadow pointer-events-none max-h-[70vh]" alt="screenshot" src={convertFileSrc( - `${editorInstance.path}/screenshots/display.jpg` + `${editorInstance.path}/screenshots/display.jpg`, )} /> diff --git a/apps/desktop/src/routes/editor/Header.tsx b/apps/desktop/src/routes/editor/Header.tsx index 240dd123d..36bce26da 100644 --- a/apps/desktop/src/routes/editor/Header.tsx +++ b/apps/desktop/src/routes/editor/Header.tsx @@ -81,6 +81,12 @@ export function Header() { leftIcon={} /> + setDialog({ type: "renameProject", open: true })} + tooltipText="Rename project" + leftIcon={} + /> +

{meta().prettyName} .cap @@ -131,7 +137,7 @@ export function Header() { data-tauri-drag-region class={cx( "flex-1 h-full flex flex-row items-center gap-2 pl-2", - ostype() !== "windows" && "pr-2" + ostype() !== "windows" && "pr-2", )} > ) => { stroke-linecap="round" stroke-linejoin="round" class={cx( - exportState.type !== "idle" && exportState.type !== "done" && "bounce" + exportState.type !== "idle" && + exportState.type !== "done" && + "bounce", )} /> diff --git a/apps/desktop/src/routes/editor/context.ts b/apps/desktop/src/routes/editor/context.ts index 94b50ff01..bab6cdfda 100644 --- a/apps/desktop/src/routes/editor/context.ts +++ b/apps/desktop/src/routes/editor/context.ts @@ -34,6 +34,7 @@ export type CurrentDialog = | { type: "createPreset" } | { type: "renamePreset"; presetIndex: number } | { type: "deletePreset"; presetIndex: number } + | { type: "renameProject" } | { type: "crop"; position: XY; size: XY } | { type: "export" }; @@ -60,7 +61,7 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider( }) => { const editorInstanceContext = useEditorInstanceContext(); const [project, setProject] = createStore( - props.editorInstance.savedProjectConfig + props.editorInstance.savedProjectConfig, ); createEffect( @@ -71,8 +72,8 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider( debounce(() => { commands.setProjectConfig(project); }), - { defer: true } - ) + { defer: true }, + ), ); const [dialog, setDialog] = createSignal({ @@ -105,7 +106,7 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider( ? (exportState.progress.renderedCount / exportState.progress.totalFrames) * 100 - : undefined + : undefined, ); createEffect( @@ -114,16 +115,16 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider( (active) => { if (!active) commands.setPlayheadPosition( - Math.floor(editorState.playbackTime * FPS) + Math.floor(editorState.playbackTime * FPS), ); - } - ) + }, + ), ); const totalDuration = () => project.timeline?.segments.reduce( (acc, s) => acc + (s.end - s.start) / s.timescale, - 0 + 0, ) ?? props.editorInstance.recordingDuration; type State = { @@ -169,7 +170,7 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider( position: editorState.timeline.transform.position, }, z, - origin + origin, ); const transform = editorState.timeline.transform; @@ -190,8 +191,8 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider( Math.max(p, 0), Math.max(zoomOutLimit(), totalDuration()) + 4 - - editorState.timeline.transform.zoom - ) + editorState.timeline.transform.zoom, + ), ); }, }, @@ -219,7 +220,7 @@ export const [EditorContextProvider, useEditorContext] = createContextProvider( }; }, // biome-ignore lint/style/noNonNullAssertion: it's ok - null! + null!, ); export type FrameData = { width: number; height: number; data: ImageData }; @@ -276,7 +277,7 @@ export const [EditorInstanceContextProvider, useEditorInstanceContext] = const [_ws, isConnected] = createImageDataWS( instance.framesSocketUrl, - setLatestFrame + setLatestFrame, ); createEffect(() => { @@ -380,7 +381,7 @@ export const [TimelineContextProvider, useTimelineContext] = timelineBounds: props.timelineBounds, }; }, - null! + null!, ); export const [TrackContextProvider, useTrackContext] = createContextProvider( @@ -402,7 +403,7 @@ export const [TrackContextProvider, useTrackContext] = createContextProvider( setTrackState, }; }, - null! + null!, ); export const [SegmentContextProvider, useSegmentContext] = diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index 11186204b..72e74580a 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -89,6 +89,9 @@ async setPlayheadPosition(frameNumber: number) : Promise { async setProjectConfig(config: ProjectConfiguration) : Promise { return await TAURI_INVOKE("set_project_config", { config }); }, +async renameProject(name: string) : Promise { + return await TAURI_INVOKE("rename_project", { name }); +}, async openPermissionSettings(permission: OSPermission) : Promise { await TAURI_INVOKE("open_permission_settings", { permission }); },