From 4c689c247be6186562b376c306fb7e4a8fe4d3e6 Mon Sep 17 00:00:00 2001 From: Patrick Delorme Date: Sun, 12 Oct 2025 09:46:38 -0400 Subject: [PATCH 01/10] Update all package.json file to support node >= version 20 --- apps/tasks/package.json | 4 ++-- apps/web/package.json | 4 ++-- package.json | 4 ++-- packages/database/package.json | 4 ++-- packages/local-docker/package.json | 4 ++-- packages/ui/package.json | 4 ++-- packages/utils/package.json | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/tasks/package.json b/apps/tasks/package.json index 2010d34899..7cb7c9e775 100644 --- a/apps/tasks/package.json +++ b/apps/tasks/package.json @@ -41,6 +41,6 @@ "ts-jest": "^29.1.2" }, "engines": { - "node": "20" + "node": ">=20" } -} +} \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index f2f594873e..303a7443af 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -151,6 +151,6 @@ "typescript": "^5.8.3" }, "engines": { - "node": "20" + "node": ">=20" } -} +} \ No newline at end of file diff --git a/package.json b/package.json index 5f201da2f1..c058f05467 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,6 @@ "packageManager": "pnpm@10.5.2", "name": "cap", "engines": { - "node": "24" + "node": ">=20" } -} +} \ No newline at end of file diff --git a/packages/database/package.json b/packages/database/package.json index 67f007e373..4dfd055f4d 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -52,7 +52,7 @@ "typescript": "^5.8.3" }, "engines": { - "node": "20" + "node": ">=20" }, "exports": { ".": "./index.ts", @@ -75,4 +75,4 @@ "./helpers": "./dist/helpers.js" } } -} +} \ No newline at end of file diff --git a/packages/local-docker/package.json b/packages/local-docker/package.json index f4a274a680..ef83958db8 100644 --- a/packages/local-docker/package.json +++ b/packages/local-docker/package.json @@ -10,6 +10,6 @@ "docker:clean": "docker compose down -v" }, "engines": { - "node": "20" + "node": ">=20" } -} +} \ No newline at end of file diff --git a/packages/ui/package.json b/packages/ui/package.json index 83dd10de52..3f54b001c5 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -55,6 +55,6 @@ "zod": "^3" }, "engines": { - "node": "20" + "node": ">=20" } -} +} \ No newline at end of file diff --git a/packages/utils/package.json b/packages/utils/package.json index 4f201dfe51..b0acbe39dc 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -27,6 +27,6 @@ "zod": "^3" }, "engines": { - "node": "20" + "node": ">=20" } -} +} \ No newline at end of file From 237d6589e3c836a918aed95d37b96e80a54bfda6 Mon Sep 17 00:00:00 2001 From: Patrick Delorme Date: Sun, 12 Oct 2025 23:26:32 -0400 Subject: [PATCH 02/10] Initial Implementation --- .../desktop/src-tauri/src/deeplink_actions.rs | 1 + apps/desktop/src-tauri/src/lib.rs | 21 +- apps/desktop/src-tauri/src/recording.rs | 3 + .../src-tauri/src/recording_settings.rs | 1 + apps/desktop/src-tauri/src/upload.rs | 7 +- apps/desktop/src-tauri/src/upload_legacy.rs | 9 +- .../src/routes/editor/ExportDialog.tsx | 508 ++++++++++++------ .../src/routes/target-select-overlay.tsx | 187 +++++-- apps/desktop/src/utils/queries.ts | 27 +- apps/desktop/src/utils/tauri.ts | 11 +- apps/web/app/api/desktop/[...route]/root.ts | 33 +- packages/ui-solid/src/auto-imports.d.ts | 1 + packages/web-api-contract/src/desktop.ts | 13 + 13 files changed, 603 insertions(+), 219 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 7372da2b9b..8f21d1b761 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -136,6 +136,7 @@ impl DeepLinkAction { capture_target, capture_system_audio, mode, + organization_id: None, }; crate::recording::start_recording(app.clone(), state, inputs) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 05289bd2d9..1ad3d4e629 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -86,7 +86,7 @@ use tauri_specta::Event; #[cfg(target_os = "macos")] use tokio::sync::Mutex; use tokio::sync::{RwLock, oneshot}; -use tracing::{error, trace, warn}; +use tracing::{error, info, trace, warn}; use upload::{create_or_get_video, upload_image, upload_video}; use web_api::AuthedApiError; use web_api::ManagerExt as WebManagerExt; @@ -1063,6 +1063,7 @@ async fn upload_exported_video( path: PathBuf, mode: UploadMode, channel: Channel, + organization_id: Option, ) -> Result { let Ok(Some(auth)) = AuthStore::get(&app) else { AuthStore::set(&app, None).map_err(|e| e.to_string())?; @@ -1109,6 +1110,7 @@ async fn upload_exported_video( video_id, Some(meta.pretty_name.clone()), Some(metadata.clone()), + organization_id, ) .await } @@ -1962,6 +1964,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) { windows::position_traffic_lights, windows::set_theme, global_message_dialog, + log_message, show_window, write_clipboard_string, platform::perform_haptic_feedback, @@ -2062,8 +2065,8 @@ pub async fn run(recording_logging_handle: LoggingHandle) { tauri::async_runtime::set(tokio::runtime::Handle::current()); #[allow(unused_mut)] - let mut builder = - tauri::Builder::default().plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { + let mut builder = tauri::Builder::default() + .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { trace!("Single instance invoked with args {args:?}"); // This is also handled as a deeplink on some platforms (eg macOS), see deeplink_actions @@ -2274,6 +2277,7 @@ pub async fn run(recording_logging_handle: LoggingHandle) { }), capture_system_audio: settings.system_audio, mode: event.mode, + organization_id: settings.organization_id, }, ) .await; @@ -2651,6 +2655,17 @@ fn global_message_dialog(app: AppHandle, message: String) { app.dialog().message(message).show(|_| {}); } +#[tauri::command] +#[specta::specta] +fn log_message(level: String, message: String) { + match level.as_str() { + "info" => info!("{}", message), + "warn" => warn!("{}", message), + "error" => error!("{}", message), + _ => trace!("{}", message), + } +} + #[tauri::command] #[specta::specta] async fn write_clipboard_string( diff --git a/apps/desktop/src-tauri/src/recording.rs b/apps/desktop/src-tauri/src/recording.rs index c6d212540c..16c076e147 100644 --- a/apps/desktop/src-tauri/src/recording.rs +++ b/apps/desktop/src-tauri/src/recording.rs @@ -237,6 +237,8 @@ pub struct StartRecordingInputs { #[serde(default)] pub capture_system_audio: bool, pub mode: RecordingMode, + #[serde(default)] + pub organization_id: Option, } #[derive(tauri_specta::Event, specta::Type, Clone, Debug, serde::Serialize)] @@ -313,6 +315,7 @@ pub async fn start_recording( chrono::Local::now().format("%Y-%m-%d %H:%M:%S") )), None, + inputs.organization_id.clone(), ) .await { diff --git a/apps/desktop/src-tauri/src/recording_settings.rs b/apps/desktop/src-tauri/src/recording_settings.rs index a611f25698..9d33e1dcb2 100644 --- a/apps/desktop/src-tauri/src/recording_settings.rs +++ b/apps/desktop/src-tauri/src/recording_settings.rs @@ -21,6 +21,7 @@ pub struct RecordingSettingsStore { pub camera_id: Option, pub mode: Option, pub system_audio: bool, + pub organization_id: Option, } impl RecordingSettingsStore { diff --git a/apps/desktop/src-tauri/src/upload.rs b/apps/desktop/src-tauri/src/upload.rs index 556dfc2a78..58b7e1bcf7 100644 --- a/apps/desktop/src-tauri/src/upload.rs +++ b/apps/desktop/src-tauri/src/upload.rs @@ -195,7 +195,7 @@ pub async fn upload_image( .ok_or("Invalid file path")? .to_string(); - let s3_config = create_or_get_video(app, true, None, None, None).await?; + let s3_config = create_or_get_video(app, true, None, None, None, None).await?; let (stream, total_size) = file_reader_stream(file_path).await?; singlepart_uploader( @@ -223,6 +223,7 @@ pub async fn create_or_get_video( video_id: Option, name: Option, meta: Option, + organization_id: Option, ) -> Result { let mut s3_config_url = if let Some(id) = video_id { format!("/api/desktop/video/create?recordingMode=desktopMP4&videoId={id}") @@ -245,6 +246,10 @@ pub async fn create_or_get_video( } } + if let Some(org_id) = organization_id { + s3_config_url.push_str(&format!("&orgId={}", org_id)); + } + let response = app .authed_api_request(s3_config_url, |client, url| client.get(url)) .await?; diff --git a/apps/desktop/src-tauri/src/upload_legacy.rs b/apps/desktop/src-tauri/src/upload_legacy.rs index 223b8573b3..c8a7e953cf 100644 --- a/apps/desktop/src-tauri/src/upload_legacy.rs +++ b/apps/desktop/src-tauri/src/upload_legacy.rs @@ -216,7 +216,7 @@ pub async fn upload_video( let client = reqwest::Client::new(); let s3_config = match existing_config { Some(config) => config, - None => create_or_get_video(app, false, Some(video_id.clone()), None, meta).await?, + None => create_or_get_video(app, false, Some(video_id.clone()), None, meta, None).await?, }; let presigned_put = presigned_s3_put( @@ -331,7 +331,7 @@ pub async fn upload_image(app: &AppHandle, file_path: PathBuf) -> Result, name: Option, meta: Option, + organization_id: Option, ) -> Result { let mut s3_config_url = if let Some(id) = video_id { format!("/api/desktop/video/create?recordingMode=desktopMP4&videoId={id}") @@ -407,6 +408,10 @@ pub async fn create_or_get_video( } } + if let Some(org_id) = organization_id { + s3_config_url.push_str(&format!("&orgId={}", org_id)); + } + let response = app .authed_api_request(s3_config_url, |client, url| client.get(url)) .await diff --git a/apps/desktop/src/routes/editor/ExportDialog.tsx b/apps/desktop/src/routes/editor/ExportDialog.tsx index 40d6a75c62..57b87fe3c3 100644 --- a/apps/desktop/src/routes/editor/ExportDialog.tsx +++ b/apps/desktop/src/routes/editor/ExportDialog.tsx @@ -1,5 +1,6 @@ import { Button } from "@cap/ui-solid"; import { Select as KSelect } from "@kobalte/core/select"; +import { Tabs as KTabs } from "@kobalte/core/tabs"; import { makePersisted } from "@solid-primitives/storage"; import { createMutation, @@ -36,6 +37,7 @@ import { type FramesRendered, type UploadProgress, } from "~/utils/tauri"; +import { apiClient, protectedHeaders } from "~/utils/web-api"; import { type RenderState, useEditorContext } from "./context"; import { RESOLUTION_OPTIONS } from "./Header"; import { @@ -77,17 +79,17 @@ export const EXPORT_TO_OPTIONS = [ { label: "File", value: "file", - icon: , + icon: , }, { label: "Clipboard", value: "clipboard", - icon: , + icon: , }, { label: "Shareable link", value: "link", - icon: , + icon: , }, ] as const; @@ -106,6 +108,7 @@ interface Settings { exportTo: ExportToOption; resolution: { label: string; value: string; width: number; height: number }; compression: ExportCompression; + organizationId?: string | null; } export function ExportDialog() { const { @@ -120,6 +123,18 @@ export function ExportDialog() { const auth = authStore.createQuery(); + const organizations = createQuery(() => ({ + queryKey: ["organizations"], + queryFn: async () => { + const response = await apiClient.desktop.getOrganizations({ + headers: await protectedHeaders(), + }); + if (response.status === 200) return response.body; + return []; + }, + enabled: () => !!authStore.get(), + })); + const [settings, setSettings] = makePersisted( createStore({ format: "Mp4", @@ -127,12 +142,25 @@ export function ExportDialog() { exportTo: "file", resolution: { label: "720p", value: "720p", width: 1280, height: 720 }, compression: "Minimal", + organizationId: null, }), { name: "export_settings" }, ); if (!["Mp4", "Gif"].includes(settings.format)) setSettings("format", "Mp4"); + // Auto-select first organization if none selected and user is authenticated + createEffect(() => { + if ( + !settings.organizationId && + organizations.data && + organizations.data.length > 0 && + auth.data + ) { + setSettings("organizationId", organizations.data[0].id); + } + }); + const exportWithSettings = (onProgress: (progress: FramesRendered) => void) => exportVideo( projectPath, @@ -350,6 +378,7 @@ export function ExportDialog() { projectPath, "Reupload", uploadChannel, + settings.organizationId ?? null, ) : await commands.uploadExportedVideo( projectPath, @@ -357,6 +386,7 @@ export function ExportDialog() { Initial: { pre_created_video: null }, }, uploadChannel, + settings.organizationId ?? null, ); if (result === "NotAuthenticated") @@ -402,7 +432,7 @@ export function ExportDialog() { ) : ( + )} - - - - {/* Format */} -
-
-

Format

-
- - {(option) => ( - +
+ + {props.item.rawValue.name} + + {/* Show ownership indicator */} + + + Owner + + +
+ )} -
-
-
-
- {/* Frame rate */} -
-
-

Frame rate

- - options={ - settings.format === "Gif" ? GIF_FPS_OPTIONS : FPS_OPTIONS - } - optionValue="value" - optionTextValue="label" - placeholder="Select FPS" - value={(settings.format === "Gif" - ? GIF_FPS_OPTIONS - : FPS_OPTIONS - ).find((opt) => opt.value === settings.fps)} - onChange={(option) => { - const value = - option?.value ?? (settings.format === "Gif" ? 10 : 30); - trackEvent("export_fps_changed", { - fps: value, - }); - setSettings("fps", value); - }} - itemComponent={(props) => ( - - as={KSelect.Item} - item={props.item} - > - - {props.item.rawValue.label} - - - )} - > - - class="flex-1 text-sm text-left truncate tabular-nums text-[--gray-500]"> - {(state) => {state.selectedOption()?.label}} - - - as={(props) => ( - - )} - /> - - - - as={KSelect.Content} - class={cx(topSlideAnimateClasses, "z-50")} - > - - class="max-h-32 custom-scroll" - as={KSelect.Listbox} + > + + class="flex-1 text-sm text-left truncate text-gray-12"> + {(state) => ( + + {state.selectedOption()?.name ?? + "Select organization"} + + )} + + + as={(props) => ( + + )} /> - - - -
+ + + + as={KSelect.Content} + class={cx(topSlideAnimateClasses, "z-50")} + > + + class="max-h-32 custom-scroll" + as={KSelect.Listbox} + /> + + + +
+ - {/* Compression */} -
-
-

Compression

-
- - {(option) => ( - + + {props.item.rawValue.label} + + )} - + > + + class="flex-1 text-xs text-left truncate tabular-nums text-gray-12"> + {(state) => ( + {state.selectedOption()?.label} + )} + + + as={(props) => ( + + )} + /> + + + + as={KSelect.Content} + class={cx(topSlideAnimateClasses, "z-50")} + > + + class="max-h-32 custom-scroll" + as={KSelect.Listbox} + /> + + +
-
- {/* Resolution */} -
-
-

Resolution

-
- +

+ Resolution +

+ { + const option = ( settings.format === "Gif" ? [RESOLUTION_OPTIONS._720p, RESOLUTION_OPTIONS._1080p] : [ @@ -663,22 +779,68 @@ export function ExportDialog() { RESOLUTION_OPTIONS._1080p, RESOLUTION_OPTIONS._4k, ] - } - > - {(option) => ( - - )} -
-
+ ).find((opt) => opt.value === v); + if (option) setSettings("resolution", option); + }} + > + + + {(option) => ( + + {option.label} + + )} + + +
+ + + +
+ + {/* Quality Row */} +
+

+ Quality +

+ { + setSettings("compression", v as ExportCompression); + }} + > + + + {(option) => ( + + {option.label} + + )} + + +
+ + +
diff --git a/apps/desktop/src/routes/target-select-overlay.tsx b/apps/desktop/src/routes/target-select-overlay.tsx index e0c44bf295..5908c4bc37 100644 --- a/apps/desktop/src/routes/target-select-overlay.tsx +++ b/apps/desktop/src/routes/target-select-overlay.tsx @@ -1,10 +1,12 @@ import { Button } from "@cap/ui-solid"; +import { Select as KSelect } from "@kobalte/core/select"; import { createEventListener, createEventListenerMap, } from "@solid-primitives/event-listener"; import { useSearchParams } from "@solidjs/router"; import { createQuery } from "@tanstack/solid-query"; +import { invoke } from "@tauri-apps/api/core"; import { emit } from "@tauri-apps/api/event"; import { CheckMenuItem, Menu, Submenu } from "@tauri-apps/api/menu"; import * as dialog from "@tauri-apps/plugin-dialog"; @@ -20,11 +22,12 @@ import { Show, Suspense, Switch, + type ValidComponent, } from "solid-js"; import { createStore, reconcile } from "solid-js/store"; import ModeSelect from "~/components/ModeSelect"; import { authStore, generalSettingsStore } from "~/store"; -import { createOptionsQuery } from "~/utils/queries"; +import { createOptionsQuery, createOrganizationsQuery } from "~/utils/queries"; import { handleRecordingResult } from "~/utils/recording"; import { commands, @@ -37,6 +40,12 @@ import { RecordingOptionsProvider, useRecordingOptions, } from "./(window-chrome)/OptionsContext"; +import { + MenuItem, + MenuItemList, + PopperContent, + topSlideAnimateClasses, +} from "./editor/ui"; const capitalize = (str: string) => { return str.charAt(0).toUpperCase() + str.slice(1); @@ -54,6 +63,7 @@ function Inner() { const [params] = useSearchParams<{ displayId: DisplayId }>(); const { rawOptions, setOptions } = createOptionsQuery(); const [toggleModeSelect, setToggleModeSelect] = createSignal(false); + const organizations = createOrganizationsQuery(); const [targetUnderCursor, setTargetUnderCursor] = createStore({ @@ -168,6 +178,29 @@ function Inner() { // Eg. on Windows Ctrl+P would open the print dialog without this createEventListener(document, "keydown", (e) => e.preventDefault()); + // Auto-select first organization if none is selected + const auth = authStore.createQuery(); + createEffect(() => { + // Log to terminal via Tauri + invoke("log_message", { + level: "info", + message: `Auto-selection check: organizationId=${rawOptions.organizationId}, organizationsLength=${organizations.data?.length}, authData=${!!auth.data}`, + }).catch(console.error); + + if ( + !rawOptions.organizationId && + organizations.data && + organizations.data.length > 0 && + auth.data + ) { + invoke("log_message", { + level: "info", + message: `Auto-selecting organization: ${organizations.data[0].id}`, + }).catch(console.error); + setOptions("organizationId", organizations.data[0].id); + } + }); + return ( @@ -210,6 +243,7 @@ function Inner() {
@@ -265,6 +299,7 @@ function Inner() { variant: "window", id: windowUnderCursor.id, }} + organizations={organizations} /> + + + {/* Backdrop to close dropdown when clicking outside */} +
setIsOpen(false)} /> + {/* Dropdown menu */} +
+ {props.options.map((option) => ( + + ))} +
+ +
+ ); +} + function RecordingControls(props: { target: ScreenCaptureTarget; setToggleModeSelect?: (value: boolean) => void; - organizations: () => Array<{ id: string; name: string; ownerId: string }>; + organizations: Array; }) { const auth = authStore.createQuery(); const { setOptions, rawOptions } = useRecordingOptions(); @@ -841,32 +897,34 @@ function RecordingControls(props: { return; } - handleRecordingResult( + invoke("log_message", { + level: "info", + message: `Starting recording with: target=${JSON.stringify(props.target)}, mode=${rawOptions.mode}, systemAudio=${rawOptions.captureSystemAudio}, organizationId=${rawOptions.organizationId ?? null}`, + }).catch(console.error); commands.startRecording({ capture_target: props.target, mode: rawOptions.mode, capture_system_audio: rawOptions.captureSystemAudio, organization_id: rawOptions.organizationId ?? null, - }), - setOptions, - ); - }} - > -
- {rawOptions.mode === "studio" ? ( - - ) : ( - - )} -
- - {rawOptions.mode === "instant" && !auth.data - ? "Sign In To Use" - : "Start Recording"} - - - {`${capitalize(rawOptions.mode)} Mode`} - + }); + }} + > +
+ {rawOptions.mode === "studio" ? ( + + ) : ( + + )} +
+ + {rawOptions.mode === "instant" && !auth.data + ? "Sign In To Use" + : "Start Recording"} + + + {`${capitalize(rawOptions.mode)} Mode`} + +
- {/* Organization selector - appears when instant mode is selected and user has organizations */} 1 + props.organizations.length > 1 } >
@@ -903,77 +960,17 @@ function RecordingControls(props: { Organization - - options={props.organizations()} - optionValue="id" - optionTextValue="name" - placeholder="Select organization" - value={props - .organizations() - .find( - (org: { id: string; name: string; ownerId: string }) => - org.id === rawOptions.organizationId, - )} - onChange={(option) => - setOptions("organizationId", option?.id ?? null) + + setOptions("organizationId", value || null) } - disabled={props.organizations().length === 1} - itemComponent={(props) => ( - - as={KSelect.Item} - item={props.item} - > -
- - {props.item.rawValue.name} - - {/* Show ownership indicator */} - - - Owner - - -
- - )} - > - - class="flex-1 text-xs text-left truncate text-gray-12"> - {(state) => ( - - {state.selectedOption()?.name ?? "Select organization"} - - )} - - - as={(props: ComponentProps<"svg">) => ( - - )} - /> - - - - as={KSelect.Content} - class={cx(topSlideAnimateClasses, "z-50")} - > - - class="max-h-32 custom-scroll" - as={KSelect.Listbox} - /> - - - + options={props.organizations.map((org) => ({ + value: org.id, + label: `${org.name}${org.ownerId === auth.data?.user_id ? " (Owner)" : ""}`, + }))} + disabled={props.organizations.length === 1} + />
diff --git a/apps/desktop/src/utils/queries.ts b/apps/desktop/src/utils/queries.ts index fca79d1364..8a1caf8b9d 100644 --- a/apps/desktop/src/utils/queries.ts +++ b/apps/desktop/src/utils/queries.ts @@ -243,11 +243,14 @@ export function createCustomDomainQuery() { } export function createOrganizationsQuery() { - // Organizations are now cached in the auth store - // This is much more efficient than fetching them repeatedly - const auth = authStore.createQuery(); - - return createMemo(() => { - return auth.data?.organizations ?? []; - }); -} + const auth = authStore.createQuery(); + + // Refresh organizations if they're missing + createEffect(() => { + if (auth.data?.user_id && (!auth.data?.organizations || auth.data.organizations.length === 0)) { + commands.refreshOrganizations().catch(console.error); + } + }); + + return createMemo(() => auth.data?.organizations ?? []); +} \ No newline at end of file From eebb922cb6847df57577254466cc13ee9a6ba65b Mon Sep 17 00:00:00 2001 From: Oscar Beaumont Date: Tue, 28 Oct 2025 16:04:27 +0800 Subject: [PATCH 08/10] using `api` module --- apps/desktop/src-tauri/src/api.rs | 39 ++++++++++++++++++++++++++---- apps/desktop/src-tauri/src/auth.rs | 39 +++++------------------------- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/apps/desktop/src-tauri/src/api.rs b/apps/desktop/src-tauri/src/api.rs index fdfab9c4c2..7bcd2c8b4e 100644 --- a/apps/desktop/src-tauri/src/api.rs +++ b/apps/desktop/src-tauri/src/api.rs @@ -3,6 +3,7 @@ use serde::{Deserialize, Serialize}; use serde_json::json; +use specta::Type; use tauri::AppHandle; use crate::web_api::{AuthedApiError, ManagerExt}; @@ -30,7 +31,7 @@ pub async fn upload_multipart_initiate( .map_err(|err| format!("api/upload_multipart_initiate/request: {err}"))?; if !resp.status().is_success() { - let status = resp.status(); + let status = resp.status().as_u16(); let error_body = resp .text() .await @@ -72,7 +73,7 @@ pub async fn upload_multipart_presign_part( .map_err(|err| format!("api/upload_multipart_presign_part/request: {err}"))?; if !resp.status().is_success() { - let status = resp.status(); + let status = resp.status().as_u16(); let error_body = resp .text() .await @@ -144,7 +145,7 @@ pub async fn upload_multipart_complete( .map_err(|err| format!("api/upload_multipart_complete/request: {err}"))?; if !resp.status().is_success() { - let status = resp.status(); + let status = resp.status().as_u16(); let error_body = resp .text() .await @@ -199,7 +200,7 @@ pub async fn upload_signed( .map_err(|err| format!("api/upload_signed/request: {err}"))?; if !resp.status().is_success() { - let status = resp.status(); + let status = resp.status().as_u16(); let error_body = resp .text() .await @@ -232,7 +233,7 @@ pub async fn desktop_video_progress( .map_err(|err| format!("api/desktop_video_progress/request: {err}"))?; if !resp.status().is_success() { - let status = resp.status(); + let status = resp.status().as_u16(); let error_body = resp .text() .await @@ -242,3 +243,31 @@ pub async fn desktop_video_progress( Ok(()) } + +#[derive(Serialize, Deserialize, Type, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Organization { + pub id: String, + pub name: String, + pub owner_id: String, +} + +pub async fn fetch_organizations(app: &AppHandle) -> Result, AuthedApiError> { + let resp = app + .authed_api_request("/api/desktop/organizations", |client, url| client.get(url)) + .await + .map_err(|err| format!("api/fetch_organizations/request: {err}"))?; + + if !resp.status().is_success() { + let status = resp.status().as_u16(); + let error_body = resp + .text() + .await + .unwrap_or_else(|_| "".to_string()); + return Err(format!("api/fetch_organizations/{status}: {error_body}").into()); + } + + resp.json() + .await + .map_err(|err| format!("api/fetch_organizations/response: {err}").into()) +} diff --git a/apps/desktop/src-tauri/src/auth.rs b/apps/desktop/src-tauri/src/auth.rs index e904d5e1d8..90e48eb31e 100644 --- a/apps/desktop/src-tauri/src/auth.rs +++ b/apps/desktop/src-tauri/src/auth.rs @@ -6,7 +6,10 @@ use tauri_plugin_store::StoreExt; use web_api::ManagerExt; -use crate::web_api; +use crate::{ + api::{self, Organization}, + web_api, +}; #[derive(Serialize, Deserialize, Type, Debug)] pub struct AuthStore { @@ -18,14 +21,6 @@ pub struct AuthStore { pub organizations: Vec, } -#[derive(Serialize, Deserialize, Type, Debug, Clone)] -pub struct Organization { - pub id: String, - pub name: String, - #[serde(rename = "ownerId")] - pub owner_id: String, -} - #[derive(Serialize, Deserialize, Type, Debug)] #[serde(untagged)] pub enum AuthSecret { @@ -106,31 +101,9 @@ impl AuthStore { manual: auth.plan.as_ref().is_some_and(|p| p.manual), }); auth.intercom_hash = Some(plan_response.intercom_hash.unwrap_or_default()); - - // Fetch organizations - println!("Fetching organizations for user"); - match app - .authed_api_request("/api/desktop/organizations", |client, url| client.get(url)) + auth.organizations = api::fetch_organizations(&app) .await - { - Ok(response) if response.status().is_success() => { - match response.json::>().await { - Ok(orgs) => { - println!("Fetched {} organizations", orgs.len()); - auth.organizations = orgs; - } - Err(e) => { - println!("Failed to parse organizations: {e}"); - } - } - } - Err(e) => { - println!("Failed to fetch organizations: {e}"); - } - Ok(response) => { - println!("Failed to fetch organizations: status {}", response.status()); - } - } + .map_err(|e| e.to_string())?; Self::set(app, Some(auth))?; From 0ce878b55c5caabcc0174f29b0ad8f59992bf267 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 31 Oct 2025 17:05:34 +0800 Subject: [PATCH 09/10] formatting --- .../src/routes/editor/ExportDialog.tsx | 4 ++-- .../src/routes/target-select-overlay.tsx | 2 +- apps/desktop/src/utils/queries.ts | 19 +++++++++++-------- apps/web/app/api/desktop/[...route]/root.ts | 4 +--- apps/web/package.json | 2 +- package.json | 2 +- packages/database/package.json | 2 +- packages/local-docker/package.json | 2 +- packages/ui/package.json | 2 +- packages/utils/package.json | 2 +- 10 files changed, 21 insertions(+), 20 deletions(-) diff --git a/apps/desktop/src/routes/editor/ExportDialog.tsx b/apps/desktop/src/routes/editor/ExportDialog.tsx index d99f041f58..e9a54fd611 100644 --- a/apps/desktop/src/routes/editor/ExportDialog.tsx +++ b/apps/desktop/src/routes/editor/ExportDialog.tsx @@ -7,6 +7,7 @@ import { keepPreviousData, } from "@tanstack/solid-query"; import { Channel } from "@tauri-apps/api/core"; +import { CheckMenuItem, Menu } from "@tauri-apps/api/menu"; import { save as saveDialog } from "@tauri-apps/plugin-dialog"; import { cx } from "cva"; import { @@ -31,6 +32,7 @@ import { authStore } from "~/store"; import { trackEvent } from "~/utils/analytics"; import { createSignInMutation } from "~/utils/auth"; import { exportVideo } from "~/utils/export"; +import { createOrganizationsQuery } from "~/utils/queries"; import { commands, type ExportCompression, @@ -48,8 +50,6 @@ import { PopperContent, topSlideAnimateClasses, } from "./ui"; -import { createOrganizationsQuery } from "~/utils/queries"; -import { CheckMenuItem, Menu } from "@tauri-apps/api/menu"; class SilentError extends Error {} diff --git a/apps/desktop/src/routes/target-select-overlay.tsx b/apps/desktop/src/routes/target-select-overlay.tsx index 4191981344..ee540a3dac 100644 --- a/apps/desktop/src/routes/target-select-overlay.tsx +++ b/apps/desktop/src/routes/target-select-overlay.tsx @@ -30,6 +30,7 @@ import { createStore, reconcile } from "solid-js/store"; import ModeSelect from "~/components/ModeSelect"; import { authStore, generalSettingsStore } from "~/store"; import { createOptionsQuery, createOrganizationsQuery } from "~/utils/queries"; +import { handleRecordingResult } from "~/utils/recording"; import { commands, type DisplayId, @@ -42,7 +43,6 @@ import { RecordingOptionsProvider, useRecordingOptions, } from "./(window-chrome)/OptionsContext"; -import { handleRecordingResult } from "~/utils/recording"; const MIN_WIDTH = 200; const MIN_HEIGHT = 100; diff --git a/apps/desktop/src/utils/queries.ts b/apps/desktop/src/utils/queries.ts index bef57e83a9..c9462c5c98 100644 --- a/apps/desktop/src/utils/queries.ts +++ b/apps/desktop/src/utils/queries.ts @@ -243,14 +243,17 @@ export function createCustomDomainQuery() { } export function createOrganizationsQuery() { - const auth = authStore.createQuery(); + const auth = authStore.createQuery(); - // Refresh organizations if they're missing - createEffect(() => { - if (auth.data?.user_id && (!auth.data?.organizations || auth.data.organizations.length === 0)) { - commands.updateAuthPlan().catch(console.error); - } - }); + // Refresh organizations if they're missing + createEffect(() => { + if ( + auth.data?.user_id && + (!auth.data?.organizations || auth.data.organizations.length === 0) + ) { + commands.updateAuthPlan().catch(console.error); + } + }); - return () => auth.data?.organizations ?? []; + return () => auth.data?.organizations ?? []; } diff --git a/apps/web/app/api/desktop/[...route]/root.ts b/apps/web/app/api/desktop/[...route]/root.ts index 2db26416c0..6d697d9c64 100644 --- a/apps/web/app/api/desktop/[...route]/root.ts +++ b/apps/web/app/api/desktop/[...route]/root.ts @@ -197,9 +197,7 @@ app.get("/plan", withAuth, async (c) => { }); }); -app.get("/organizations", - withAuth, - async (c) => { +app.get("/organizations", withAuth, async (c) => { const user = c.get("user"); const orgs = await db() diff --git a/apps/web/package.json b/apps/web/package.json index 8fcf89933e..ae311637ca 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -153,4 +153,4 @@ "engines": { "node": ">=20" } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 76447e7de0..0f7c91b6b0 100644 --- a/package.json +++ b/package.json @@ -41,4 +41,4 @@ "engines": { "node": ">=20" } -} \ No newline at end of file +} diff --git a/packages/database/package.json b/packages/database/package.json index 569b7751e2..42074bb491 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -77,4 +77,4 @@ "./helpers": "./dist/helpers.js" } } -} \ No newline at end of file +} diff --git a/packages/local-docker/package.json b/packages/local-docker/package.json index ef83958db8..0bbc840a60 100644 --- a/packages/local-docker/package.json +++ b/packages/local-docker/package.json @@ -12,4 +12,4 @@ "engines": { "node": ">=20" } -} \ No newline at end of file +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 3f54b001c5..3e90ae271b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -57,4 +57,4 @@ "engines": { "node": ">=20" } -} \ No newline at end of file +} diff --git a/packages/utils/package.json b/packages/utils/package.json index 4a65ceedc5..82f842d4c5 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -30,4 +30,4 @@ "engines": { "node": ">=20" } -} \ No newline at end of file +} From 5f63f670eaa7164f0f5822bf2c45ffdac4399e61 Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Fri, 31 Oct 2025 17:11:23 +0800 Subject: [PATCH 10/10] types --- .../src/routes/(window-chrome)/settings/recordings.tsx | 1 + apps/desktop/src/routes/editor/ShareButton.tsx | 2 ++ apps/desktop/src/routes/recordings-overlay.tsx | 5 ++--- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx index 49a92a4acc..75f24dff38 100644 --- a/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx +++ b/apps/desktop/src/routes/(window-chrome)/settings/recordings.tsx @@ -350,6 +350,7 @@ function RecordingItem(props: { props.recording.path, "Reupload", new Channel((progress) => {}), + null, ), })); diff --git a/apps/desktop/src/routes/editor/ShareButton.tsx b/apps/desktop/src/routes/editor/ShareButton.tsx index fbd3883849..03793ff3a4 100644 --- a/apps/desktop/src/routes/editor/ShareButton.tsx +++ b/apps/desktop/src/routes/editor/ShareButton.tsx @@ -100,6 +100,7 @@ function ShareButton() { projectPath, "Reupload", uploadChannel, + null, ) : await commands.uploadExportedVideo( projectPath, @@ -107,6 +108,7 @@ function ShareButton() { Initial: { pre_created_video: null }, }, uploadChannel, + null, ); if (result === "NotAuthenticated") { diff --git a/apps/desktop/src/routes/recordings-overlay.tsx b/apps/desktop/src/routes/recordings-overlay.tsx index 01a7812889..fd20488659 100644 --- a/apps/desktop/src/routes/recordings-overlay.tsx +++ b/apps/desktop/src/routes/recordings-overlay.tsx @@ -772,10 +772,9 @@ function createRecordingMutations( res = await commands.uploadExportedVideo( media.path, - { - Initial: { pre_created_video: null }, - }, + { Initial: { pre_created_video: null } }, uploadChannel, + null, ); } else { setActionState({