diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs index eba4cf1d38..61bc67e039 100644 --- a/apps/desktop/src-tauri/src/general_settings.rs +++ b/apps/desktop/src-tauri/src/general_settings.rs @@ -120,6 +120,8 @@ pub struct GeneralSettingsStore { pub enable_new_uploader: bool, #[serde(default = "default_excluded_windows")] pub excluded_windows: Vec, + #[serde(default)] + pub delete_instant_recordings_after_upload: bool, } fn default_enable_native_camera_preview() -> bool { @@ -184,6 +186,7 @@ impl Default for GeneralSettingsStore { post_deletion_behaviour: PostDeletionBehaviour::DoNothing, enable_new_uploader: default_enable_new_uploader(), excluded_windows: default_excluded_windows(), + delete_instant_recordings_after_upload: false, } } } diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index a9617babf5..e2e8ffa56f 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -949,7 +949,7 @@ async fn handle_recording_finish( .map_err(|err| error!("Error compressing thumbnail for instant mode progressive upload: {err}") ) { - crate::upload::singlepart_uploader( + let res = crate::upload::singlepart_uploader( app.clone(), crate::api::PresignedS3PutRequest { video_id: video_upload_info.id.clone(), @@ -959,13 +959,19 @@ async fn handle_recording_finish( }, bytes.len() as u64, stream::once(async move { Ok::<_, std::io::Error>(bytes::Bytes::from(bytes)) }), - ) - .await - .map_err(|err| { - error!("Error updating thumbnail for instant mode progressive upload: {err}") - }) - .ok(); + .await; + if let Err(err) = res { + error!("Error updating thumbnail for instant mode progressive upload: {err}"); + return; + } + + if GeneralSettingsStore::get(&app).ok().flatten().unwrap_or_default().delete_instant_recordings_after_upload { + if let Err(err) = tokio::fs::remove_dir_all(&recording_dir).await { + error!("Failed to remove recording files after upload: {err:?}"); + return; + } + } } } else if let Ok(meta) = build_video_meta(&output_path) .map_err(|err| error!("Error getting video metadata: {}", err)) diff --git a/apps/desktop/src/routes/(window-chrome)/settings/Setting.tsx b/apps/desktop/src/routes/(window-chrome)/settings/Setting.tsx index 30b8eb7c33..9f182f4a66 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/Setting.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/Setting.tsx @@ -1,6 +1,6 @@ import { Toggle } from "~/components/Toggle"; -export function Setting(props: { +export function SettingItem(props: { pro?: boolean; label: string; description?: string; @@ -21,7 +21,7 @@ export function Setting(props: { ); } -export function ToggleSetting(props: { +export function ToggleSettingItem(props: { pro?: boolean; label: string; description?: string; @@ -29,12 +29,12 @@ export function ToggleSetting(props: { onChange(v: boolean): void; }) { return ( - + props.onChange(v)} /> - + ); } diff --git a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx index 114453a984..804773a072 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/experimental.tsx @@ -3,7 +3,7 @@ import { createStore } from "solid-js/store"; import { generalSettingsStore } from "~/store"; import type { GeneralSettingsStore } from "~/utils/tauri"; -import { ToggleSetting } from "./Setting"; +import { ToggleSettingItem } from "./Setting"; export default function ExperimentalSettings() { const [store] = createResource(() => generalSettingsStore.get()); @@ -55,7 +55,7 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) {

Recording Features

- - - - - entry.ownerName ?? entry.windowTitle ?? entry.bundleIdentifier ?? "Unknown"; @@ -299,224 +299,32 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { await applyExcludedWindows(defaults); }; - type ToggleSettingItem = { - label: string; - type: "toggle"; - description: string; - value: boolean; - onChange: (value: boolean) => void | Promise; - os?: "macos" | "windows" | "linux"; - }; - - type SelectSettingItem = { - label: string; - type: "select"; - description: string; - value: - | MainWindowRecordingStartBehaviour - | PostStudioRecordingBehaviour - | PostDeletionBehaviour - | number; - onChange: ( - value: - | MainWindowRecordingStartBehaviour - | PostStudioRecordingBehaviour - | PostDeletionBehaviour - | number, - ) => void | Promise; - }; - - type SettingItem = ToggleSettingItem | SelectSettingItem; - - type SettingsGroup = { - title: string; - os?: "macos" | "windows" | "linux"; - titleStyling?: string; - items: SettingItem[]; - }; - - // Static settings groups structure to preserve component identity - const settingsGroups: SettingsGroup[] = [ - { - title: "Cap Pro", - titleStyling: - "bg-blue-500 py-1.5 mb-4 text-white text-xs px-2 rounded-lg", - items: [ - { - label: "Disable automatic link opening", - type: "toggle", - description: - "When enabled, Cap will not automatically open links in your browser (e.g. after creating a shareable link).", - get value() { - return !!settings.disableAutoOpenLinks; - }, - onChange: (value: boolean) => - handleChange("disableAutoOpenLinks", value), - }, - ], - }, - { - title: "App", - os: "macos", - items: [ - { - label: "Hide dock icon", - type: "toggle", - os: "macos", - description: - "The dock icon will be hidden when there are no windows available to close.", - get value() { - return !!settings.hideDockIcon; - }, - onChange: (value: boolean) => handleChange("hideDockIcon", value), - }, - { - label: "Enable system notifications", - type: "toggle", - os: "macos", - description: - "Show system notifications for events like copying to clipboard, saving files, and more. You may need to manually allow Cap access via your system's notification settings.", - get value() { - return !!settings.enableNotifications; - }, - onChange: async (value: boolean) => { - if (value) { - // Check current permission state - console.log("Checking notification permission status"); - const permissionGranted = await isPermissionGranted(); - console.log(`Current permission status: ${permissionGranted}`); - - if (!permissionGranted) { - // Request permission if not granted - console.log("Permission not granted, requesting permission"); - const permission = await requestPermission(); - console.log(`Permission request result: ${permission}`); - if (permission !== "granted") { - // If permission denied, don't enable the setting - console.log("Permission denied, aborting setting change"); - return; - } - } - } - handleChange("enableNotifications", value); - }, - }, - { - label: "Enable haptics", - type: "toggle", - os: "macos", - description: "Use haptics on Force Touchâ„¢ trackpads", - get value() { - return !!settings.hapticsEnabled; - }, - onChange: (value: boolean) => handleChange("hapticsEnabled", value), - }, - ], - }, - { - title: "Recording", - items: [ - { - label: "Recording countdown", - description: "Countdown before recording starts", - type: "select", - get value() { - return settings.recordingCountdown ?? 0; - }, - onChange: ( - value: - | MainWindowRecordingStartBehaviour - | PostStudioRecordingBehaviour - | PostDeletionBehaviour - | number, - ) => handleChange("recordingCountdown", value as number), - }, - { - label: "Main window recording start behaviour", - description: "The main window recording start behaviour", - type: "select", - get value() { - return settings.mainWindowRecordingStartBehaviour ?? "close"; - }, - onChange: ( - value: - | MainWindowRecordingStartBehaviour - | PostStudioRecordingBehaviour - | PostDeletionBehaviour - | number, - ) => - handleChange( - "mainWindowRecordingStartBehaviour", - value as MainWindowRecordingStartBehaviour, - ), - }, - { - label: "Studio recording finish behaviour", - description: "The studio recording finish behaviour", - type: "select", - get value() { - return settings.postStudioRecordingBehaviour ?? "openEditor"; - }, - onChange: ( - value: - | MainWindowRecordingStartBehaviour - | PostStudioRecordingBehaviour - | PostDeletionBehaviour - | number, - ) => - handleChange( - "postStudioRecordingBehaviour", - value as PostStudioRecordingBehaviour, - ), - }, - { - label: "After deleting recording behaviour", - description: - "Should Cap reopen after deleting an in progress recording?", - type: "select", - get value() { - return settings.postDeletionBehaviour ?? "doNothing"; - }, - onChange: ( - value: - | MainWindowRecordingStartBehaviour - | PostStudioRecordingBehaviour - | PostDeletionBehaviour - | number, - ) => - handleChange( - "postDeletionBehaviour", - value as PostDeletionBehaviour, - ), - }, - ], - }, - ]; - // Helper function to render select dropdown for recording behaviors - const renderRecordingSelect = ( - label: string, - description: string, - getValue: () => + const SelectSettingItem = < + T extends | MainWindowRecordingStartBehaviour | PostStudioRecordingBehaviour | PostDeletionBehaviour | number, - onChange: (value: any) => void, - options: { text: string; value: any }[], - ) => { + >(props: { + label: string; + description: string; + value: T; + onChange: (value: T) => void; + options: { text: string; value: any }[]; + }) => { return ( - + - + ); }; @@ -548,109 +358,140 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { }} /> - - {(group) => ( - -
-

- {group.title} -

-
- - {(item) => { - // Check OS compatibility - if ( - item.type === "toggle" && - item.os && - item.os !== ostype - ) { - return null; - } - - if (item.type === "toggle") { - return ( - - ); - } else if (item.type === "select") { - if ( - item.label === "Main window recording start behaviour" - ) { - return renderRecordingSelect( - item.label, - item.description, - () => item.value, - item.onChange, - [ - { text: "Close", value: "close" }, - { text: "Minimise", value: "minimise" }, - ], - ); - } else if ( - item.label === "Studio recording finish behaviour" - ) { - return renderRecordingSelect( - item.label, - item.description, - () => item.value, - item.onChange, - [ - { text: "Open editor", value: "openEditor" }, - { - text: "Show in overlay", - value: "showOverlay", - }, - ], - ); - } else if (item.label === "Recording countdown") { - return renderRecordingSelect( - item.label, - item.description, - () => item.value, - item.onChange, - [ - { text: "Off", value: 0 }, - { text: "3 seconds", value: 3 }, - { text: "5 seconds", value: 5 }, - { text: "10 seconds", value: 10 }, - ], - ); - } else if ( - item.label === "After deleting recording behaviour" - ) { - return renderRecordingSelect( - item.label, - item.description, - () => item.value, - item.onChange, - [ - { text: "Do Nothing", value: "doNothing" }, - { - text: "Reopen Recording Window", - value: "reopenRecordingWindow", - }, - ], - ); - } - } - return null; - }} - -
-
-
- )} -
+ + handleChange("disableAutoOpenLinks", !v)} + /> + + + {ostype === "macos" && ( + + handleChange("hideDockIcon", !v)} + /> + { + if (value) { + // Check current permission state + console.log("Checking notification permission status"); + const permissionGranted = await isPermissionGranted(); + console.log( + `Current permission status: ${permissionGranted}`, + ); + + if (!permissionGranted) { + // Request permission if not granted + console.log( + "Permission not granted, requesting permission", + ); + const permission = await requestPermission(); + console.log(`Permission request result: ${permission}`); + if (permission !== "granted") { + // If permission denied, don't enable the setting + console.log("Permission denied, aborting setting change"); + return; + } + } + } + handleChange("enableNotifications", value); + }} + /> + handleChange("hapticsEnabled", v)} + /> + + )} + + + + handleChange("recordingCountdown", value as number) + } + options={[ + { text: "Off", value: 0 }, + { text: "3 seconds", value: 3 }, + { text: "5 seconds", value: 5 }, + { text: "10 seconds", value: 10 }, + ]} + /> + + handleChange( + "mainWindowRecordingStartBehaviour", + value as MainWindowRecordingStartBehaviour, + ) + } + options={[ + { text: "Close", value: "close" }, + { text: "Minimise", value: "minimise" }, + ]} + /> + + handleChange( + "postStudioRecordingBehaviour", + value as PostStudioRecordingBehaviour, + ) + } + options={[ + { text: "Open editor", value: "openEditor" }, + { + text: "Show in overlay", + value: "showOverlay", + }, + ]} + /> + + handleChange( + "postDeletionBehaviour", + value as PostDeletionBehaviour, + ) + } + options={[ + { text: "Do Nothing", value: "doNothing" }, + { + text: "Reopen Recording Window", + value: "reopenRecordingWindow", + }, + ]} + /> + + handleChange("deleteInstantRecordingsAfterUpload", v) + } + /> + , +) { + return ( +
+

+ {props.title} +

+
+ {props.children} +
+
+ ); +} + function ServerURLSetting(props: { value: string; onChange: (v: string) => void; @@ -696,7 +552,7 @@ function ServerURLSetting(props: {

Self host

- @@ -716,7 +572,7 @@ function ServerURLSetting(props: { Update
- +
); diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index da55ae0c0e..f5ccb1db43 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -393,7 +393,7 @@ export type ExportSettings = ({ format: "Mp4" } & Mp4ExportSettings) | ({ format export type FileType = "recording" | "screenshot" export type Flags = { captions: boolean } export type FramesRendered = { renderedCount: number; totalFrames: number; type: "FramesRendered" } -export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: boolean; hapticsEnabled?: boolean; autoCreateShareableLink?: boolean; enableNotifications?: boolean; disableAutoOpenLinks?: boolean; hasCompletedStartup?: boolean; theme?: AppTheme; commercialLicense?: CommercialLicense | null; lastVersion?: string | null; windowTransparency?: boolean; postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; custom_cursor_capture2?: boolean; serverUrl?: string; recordingCountdown?: number | null; enableNativeCameraPreview: boolean; autoZoomOnClicks?: boolean; enableNewRecordingFlow: boolean; postDeletionBehaviour?: PostDeletionBehaviour; enableNewUploader: boolean; excludedWindows?: WindowExclusion[] } +export type GeneralSettingsStore = { instanceId?: string; uploadIndividualFiles?: boolean; hideDockIcon?: boolean; hapticsEnabled?: boolean; autoCreateShareableLink?: boolean; enableNotifications?: boolean; disableAutoOpenLinks?: boolean; hasCompletedStartup?: boolean; theme?: AppTheme; commercialLicense?: CommercialLicense | null; lastVersion?: string | null; windowTransparency?: boolean; postStudioRecordingBehaviour?: PostStudioRecordingBehaviour; mainWindowRecordingStartBehaviour?: MainWindowRecordingStartBehaviour; custom_cursor_capture2?: boolean; serverUrl?: string; recordingCountdown?: number | null; enableNativeCameraPreview: boolean; autoZoomOnClicks?: boolean; enableNewRecordingFlow: boolean; postDeletionBehaviour?: PostDeletionBehaviour; enableNewUploader: boolean; excludedWindows?: WindowExclusion[]; deleteInstantRecordingsAfterUpload?: boolean } export type GifExportSettings = { fps: number; resolution_base: XY; quality: GifQuality | null } export type GifQuality = { /**