diff --git a/Cargo.lock b/Cargo.lock index 28f4d5b67..315ced7d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1730,6 +1730,7 @@ dependencies = [ "tauri-plugin-os", "tauri-plugin-process", "tauri-plugin-shell", + "tauri-plugin-single-instance", "tauri-plugin-store", "tauri-plugin-updater", "tauri-specta", @@ -6849,6 +6850,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "tauri-plugin-single-instance" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25ac834491d089699a2bc9266a662faf373c9f779f05a2235bc6e4d9e61769a" +dependencies = [ + "log", + "serde", + "serde_json", + "tauri", + "thiserror", + "windows-sys 0.59.0", + "zbus", +] + [[package]] name = "tauri-plugin-store" version = "2.1.0" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 1e836c845..8a33bf6aa 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -34,6 +34,7 @@ tauri-plugin-updater = "2.0.0" tauri-plugin-notification = "2.0.0-rc" tauri-plugin-oauth = { git = "https://github.com/FabianLars/tauri-plugin-oauth", branch = "v2" } tauri-plugin-global-shortcut = "2.0.0" +tauri-plugin-single-instance = "2.0.1" tauri-specta = { version = "=2.0.0-rc.20", features = ["derive", "typescript"] } serde = { version = "1", features = ["derive"] } diff --git a/apps/desktop/src-tauri/assets/nsis-header.bmp b/apps/desktop/src-tauri/assets/nsis-header.bmp new file mode 100644 index 000000000..4c075ee5f Binary files /dev/null and b/apps/desktop/src-tauri/assets/nsis-header.bmp differ diff --git a/apps/desktop/src-tauri/assets/nsis-sidebar.bmp b/apps/desktop/src-tauri/assets/nsis-sidebar.bmp new file mode 100644 index 000000000..8933c03c6 Binary files /dev/null and b/apps/desktop/src-tauri/assets/nsis-sidebar.bmp differ diff --git a/apps/desktop/src-tauri/assets/wix-banner.bmp b/apps/desktop/src-tauri/assets/wix-banner.bmp new file mode 100644 index 000000000..94369ead9 Binary files /dev/null and b/apps/desktop/src-tauri/assets/wix-banner.bmp differ diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index e14d899a3..8c38cd2c7 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -949,7 +949,7 @@ async fn copy_screenshot_to_clipboard(app: AppHandle, path: PathBuf) -> Result<( #[tauri::command] #[specta::specta] -async fn open_file_path(app: AppHandle, path: PathBuf) -> Result<(), String> { +async fn open_file_path(_app: AppHandle, path: PathBuf) -> Result<(), String> { let path_str = path.to_str().ok_or("Invalid path")?; #[cfg(target_os = "windows")] @@ -1693,16 +1693,6 @@ async fn set_project_config(app: AppHandle, video_id: String, config: ProjectCon editor_instance.project_config.0.send(config).ok(); } -#[tauri::command(async)] -#[specta::specta] -fn open_in_finder(path: PathBuf) { - Command::new("open") - .arg("-R") - .arg(path) - .spawn() - .expect("Failed to open in Finder"); -} - #[tauri::command] #[specta::specta] async fn list_audio_devices() -> Result, ()> { @@ -2441,7 +2431,6 @@ pub async fn run() { start_playback, stop_playback, set_playhead_position, - open_in_finder, set_project_config, open_editor, open_main_window, @@ -2519,6 +2508,9 @@ pub async fn run() { } builder + .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| { + let _ = CapWindow::Main.show(&app); + })) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_store::Builder::new().build()) diff --git a/apps/desktop/src-tauri/src/windows.rs b/apps/desktop/src-tauri/src/windows.rs index d64179237..c5520e20f 100644 --- a/apps/desktop/src-tauri/src/windows.rs +++ b/apps/desktop/src-tauri/src/windows.rs @@ -92,7 +92,7 @@ impl CapWindowId { pub fn traffic_lights_position(&self) -> Option>> { match self { Self::Camera | Self::WindowCaptureOccluder | Self::PrevRecordings => None, - Self::Editor { .. } => Some(Some(LogicalPosition::new(20.0, 48.0))), + Self::Editor { .. } => Some(Some(LogicalPosition::new(20.0, 40.5))), Self::InProgressRecording => Some(Some(LogicalPosition::new(-100.0, -100.0))), _ => Some(None), } @@ -263,6 +263,7 @@ impl CapWindow { ) .visible(false) .theme(Some(tauri::Theme::Dark)) + .skip_taskbar(true) .build()? } Self::PrevRecordings => { diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index d6911d64c..8773531fe 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -53,6 +53,16 @@ "y": 140 } } + }, + "windows": { + "nsis": { + "headerImage": "assets/nsis-header.bmp", + "sidebarImage": "assets/nsis-sidebar.bmp", + "installerIcon": "icons/icon.ico" + }, + "wix": { + "bannerPath": "assets/wix-banner.bmp" + } } } } diff --git a/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx b/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx index 7ccdd6be8..b4353d39e 100644 --- a/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx +++ b/apps/desktop/src/components/titlebar/controls/CaptionControlsWindows11.tsx @@ -7,7 +7,7 @@ import { cx } from "cva"; export default function (props: ComponentProps<"div">) { const [local, otherProps] = splitProps(props, ["class"]); const window = getCurrentWindow(); - + return (
) { ? "text-black/90 dark:text-white" : "text-gray-50 dark:text-white", titlebarState.minimizable - ? "hover:bg-[#0000000D] active:bg-[#00000008] dark:hover:bg-[#FFFFFF1A] dark:active:bg-[#FFFFFF0D]" + ? "hover:bg-[#0000000D] active:bg-[#00000008]" : "[&>*]:opacity-30" )} > @@ -42,7 +42,7 @@ export default function (props: ComponentProps<"div">) { ? "text-black/90 dark:text-white" : "text-gray-50 dark:text-white", titlebarState.maximizable - ? "hover:bg-[#0000000D] active:bg-[#00000008] dark:hover:bg-[#FFFFFF1A] dark:active:bg-[#FFFFFF0D]" + ? "hover:bg-[#0000000D] active:bg-[#00000008]" : "[&>*]:opacity-30" )} > diff --git a/apps/desktop/src/routes/(window-chrome)/settings/general.tsx b/apps/desktop/src/routes/(window-chrome)/settings/general.tsx index 4b912133c..2ab22cc21 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/general.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/general.tsx @@ -6,51 +6,54 @@ import { isPermissionGranted, requestPermission, } from "@tauri-apps/plugin-notification"; +import { OsType, Platform, type } from "@tauri-apps/plugin-os"; -const settingsList = [ - { - key: "uploadIndividualFiles", - label: "Upload individual recording files when creating shareable link", - description: - 'Warning: this will cause shareable link uploads to become significantly slower, since all individual recording files will be uploaded. Shows "Download Assets" button in Share page.', - }, - { - key: "openEditorAfterRecording", - label: "Open editor automatically after recording stops", - description: - "The editor will be shown immediately after you finish recording.", - }, - { - key: "hideDockIcon", - label: "Hide dock icon", - description: - "The dock icon will be hidden when there are no windows available to close.", - }, - { - key: "autoCreateShareableLink", - label: "Cap Pro: Automatically create shareable link after recording", - description: - "When enabled, a shareable link will be created automatically after stopping the recording. You'll be redirected to the URL while the upload continues in the background.", - }, - { - key: "disableAutoOpenLinks", - label: "Cap Pro: Disable automatic link opening", - description: - "When enabled, Cap will not automatically open links in your browser (e.g. after creating a shareable link).", - }, - { - key: "enableNotifications", - label: "Enable System Notifications", - 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.", - requiresPermission: true, - }, -] satisfies Array<{ +const settingsList: Array<{ key: keyof GeneralSettingsStore; label: string; description: string; + platforms?: OsType[], requiresPermission?: boolean; -}>; +}> = [ + { + key: "uploadIndividualFiles", + label: "Upload individual recording files when creating shareable link", + description: + 'Warning: this will cause shareable link uploads to become significantly slower, since all individual recording files will be uploaded. Shows "Download Assets" button in Share page.', + }, + { + key: "openEditorAfterRecording", + label: "Open editor automatically after recording stops", + description: + "The editor will be shown immediately after you finish recording.", + }, + { + key: "hideDockIcon", + label: "Hide dock icon", + platforms: ["macos"], + description: + "The dock icon will be hidden when there are no windows available to close.", + }, + { + key: "autoCreateShareableLink", + label: "Cap Pro: Automatically create shareable link after recording", + description: + "When enabled, a shareable link will be created automatically after stopping the recording. You'll be redirected to the URL while the upload continues in the background.", + }, + { + key: "disableAutoOpenLinks", + label: "Cap Pro: Disable automatic link opening", + description: + "When enabled, Cap will not automatically open links in your browser (e.g. after creating a shareable link).", + }, + { + key: "enableNotifications", + label: "Enable System Notifications", + 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.", + requiresPermission: true, + }, + ]; export default function GeneralSettings() { const [store] = createResource(() => generalSettingsStore.get()); @@ -101,61 +104,63 @@ function Inner(props: { initialStore: GeneralSettingsStore | null }) { generalSettingsStore.set({ [key]: value }); }; + const ostype: OsType = type(); + return (
{(setting) => ( -
-
-

{setting.label}

- + }`} + /> + +
+ {setting.description && ( +

{setting.description}

+ )}
- {setting.description && ( -

{setting.description}

- )} -
+ )}
diff --git a/apps/desktop/src/routes/(window-chrome)/setup.tsx b/apps/desktop/src/routes/(window-chrome)/setup.tsx index 4e5001930..ec76bd3b8 100644 --- a/apps/desktop/src/routes/(window-chrome)/setup.tsx +++ b/apps/desktop/src/routes/(window-chrome)/setup.tsx @@ -8,6 +8,8 @@ import { startTransition, onCleanup, onMount, + Match, + Switch, } from "solid-js"; import { createTimer } from "@solid-primitives/timer"; import { getCurrentWindow } from "@tauri-apps/api/window"; @@ -117,8 +119,8 @@ export default function () { {permissionCheck() === "granted" ? "Granted" : permissionCheck() !== "denied" - ? "Grant Permission" - : "Request Permission"} + ? "Grant Permission" + : "Request Permission"} @@ -270,9 +272,8 @@ function Startup(props: { onClose: () => void }) { > + + + + + + + + +
props.onClose() diff --git a/apps/desktop/src/routes/editor/Header.tsx b/apps/desktop/src/routes/editor/Header.tsx index 76bb0fa04..a80af55b1 100644 --- a/apps/desktop/src/routes/editor/Header.tsx +++ b/apps/desktop/src/routes/editor/Header.tsx @@ -21,7 +21,7 @@ export function Header() { onMount(async () => { unlistenTitlebar = await initializeTitlebar(); - commands.positionTrafficLights([20.0, 48.0]); + commands.positionTrafficLights([20.0, 40.5]); }); onCleanup(() => { @@ -29,7 +29,7 @@ export function Header() { }); setTitlebar("border", false); - setTitlebar("height", "4.5rem"); + setTitlebar("height", "4rem"); setTitlebar( "items",
({ open: false, type: "idle" }); return ( @@ -127,10 +127,10 @@ function ExportButton() { {(state) => ( )} diff --git a/apps/desktop/src/routes/prev-recordings.tsx b/apps/desktop/src/routes/prev-recordings.tsx index 4c2b7e283..339f0434d 100644 --- a/apps/desktop/src/routes/prev-recordings.tsx +++ b/apps/desktop/src/routes/prev-recordings.tsx @@ -37,23 +37,23 @@ type MediaEntry = { type ProgressState = | { - type: "idle"; - } + type: "idle"; + } | { - type: "copying" | "saving"; - progress: number; - message: string; - mediaPath?: string; - stage?: "rendering"; - } + type: "copying" | "saving"; + progress: number; + message: string; + mediaPath?: string; + stage?: "rendering"; + } | { - type: "uploading"; - renderProgress: number; - uploadProgress: number; - message: string; - mediaPath?: string; - stage: "rendering" | "uploading"; - }; + type: "uploading"; + renderProgress: number; + uploadProgress: number; + message: string; + mediaPath?: string; + stage: "rendering" | "uploading"; + }; export default function () { const presets = createPresets(); @@ -168,9 +168,9 @@ export default function () { type === "recording" ? mediaId : normalizedPath - .split("screenshots/")[1] - .split("/")[0] - .replace(".cap", ""); + .split("screenshots/")[1] + .split("/")[0] + .replace(".cap", ""); const isRecording = type !== "screenshot"; const recordingMeta = createQuery(() => ({ @@ -464,13 +464,12 @@ export default function () {
@@ -484,7 +483,7 @@ export default function () {
{ async setPlayheadPosition(videoId: string, frameNumber: number) : Promise { await TAURI_INVOKE("set_playhead_position", { videoId, frameNumber }); }, -async openInFinder(path: string) : Promise { - await TAURI_INVOKE("open_in_finder", { path }); -}, async setProjectConfig(videoId: string, config: ProjectConfiguration) : Promise { await TAURI_INVOKE("set_project_config", { videoId, config }); }, diff --git a/crates/ffmpeg-cli/src/lib.rs b/crates/ffmpeg-cli/src/lib.rs index 8b69dee63..e47a86e74 100644 --- a/crates/ffmpeg-cli/src/lib.rs +++ b/crates/ffmpeg-cli/src/lib.rs @@ -14,6 +14,13 @@ pub struct FFmpegProcess { impl FFmpegProcess { pub fn spawn(mut command: Command) -> Self { + #[cfg(target_os = "windows")] + { + use std::os::windows::process::CommandExt; + const CREATE_NO_WINDOW: u32 = 0x08000000; + command.creation_flags(CREATE_NO_WINDOW); + } + let mut cmd = command.stdin(Stdio::piped()).spawn().unwrap_or_else(|e| { println!("Failed to start FFmpeg: {}", e); println!("Command: {:?}", command);