diff --git a/.gitignore b/.gitignore index 40ea0b2797..62ede0e17e 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,4 @@ tauri.windows.conf.json # Cursor .cursor .env*.local +.docs/ diff --git a/apps/desktop/app.config.ts b/apps/desktop/app.config.ts index c7569b9349..5e88eaad0f 100644 --- a/apps/desktop/app.config.ts +++ b/apps/desktop/app.config.ts @@ -37,5 +37,21 @@ export default defineConfig({ define: { "import.meta.vitest": "undefined", }, + optimizeDeps: { + include: [ + "@tauri-apps/plugin-os", + "@tanstack/solid-query", + "@tauri-apps/api/webviewWindow", + "@tauri-apps/plugin-dialog", + "@tauri-apps/plugin-store", + "posthog-js", + "uuid", + "@tauri-apps/plugin-clipboard-manager", + "@tauri-apps/api/window", + "@tauri-apps/api/core", + "@tauri-apps/api/event", + "cva", + ], + }, }), }); diff --git a/apps/desktop/src-tauri/src/general_settings.rs b/apps/desktop/src-tauri/src/general_settings.rs index 9b215c7b58..4c1373d43c 100644 --- a/apps/desktop/src-tauri/src/general_settings.rs +++ b/apps/desktop/src-tauri/src/general_settings.rs @@ -134,7 +134,7 @@ fn default_enable_native_camera_preview() -> bool { } fn default_enable_new_recording_flow() -> bool { - true + false } fn no(_: &bool) -> bool { @@ -259,7 +259,7 @@ pub fn init(app: &AppHandle) { }; if !store.recording_picker_preference_set { - store.enable_new_recording_flow = true; + store.enable_new_recording_flow = false; store.recording_picker_preference_set = true; } diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 63e874c356..f5c19546d3 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -2699,6 +2699,8 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { async move { if !permissions.screen_recording.permitted() || !permissions.accessibility.permitted() + || !permissions.microphone.permitted() + || !permissions.camera.permitted() || GeneralSettingsStore::get(&app) .ok() .flatten() diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 5f12c3e422..f8fa974508 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -1,12 +1,11 @@ -import { Router, useCurrentMatches } from "@solidjs/router"; -import { FileRoutes } from "@solidjs/start/router"; +import { Route, Router, useCurrentMatches } from "@solidjs/router"; import { QueryClient, QueryClientProvider } from "@tanstack/solid-query"; import { getCurrentWebviewWindow, type WebviewWindow, } from "@tauri-apps/api/webviewWindow"; import { message } from "@tauri-apps/plugin-dialog"; -import { createEffect, onCleanup, onMount, Suspense } from "solid-js"; +import { createEffect, lazy, onCleanup, onMount, Suspense } from "solid-js"; import { Toaster } from "solid-toast"; import "@cap/ui-solid/main.css"; @@ -19,6 +18,61 @@ import { initAnonymousUser } from "./utils/analytics"; import { type AppTheme, commands } from "./utils/tauri"; import titlebar from "./utils/titlebar-state"; +const WindowChromeLayout = lazy(() => import("./routes/(window-chrome)")); +const MainPage = lazy(() => import("./routes/(window-chrome)/(main)")); +const NewMainPage = lazy(() => import("./routes/(window-chrome)/new-main")); +const SetupPage = lazy(() => import("./routes/(window-chrome)/setup")); +const SettingsLayout = lazy(() => import("./routes/(window-chrome)/settings")); +const SettingsGeneralPage = lazy( + () => import("./routes/(window-chrome)/settings/general"), +); +const SettingsRecordingsPage = lazy( + () => import("./routes/(window-chrome)/settings/recordings"), +); +const SettingsScreenshotsPage = lazy( + () => import("./routes/(window-chrome)/settings/screenshots"), +); +const SettingsHotkeysPage = lazy( + () => import("./routes/(window-chrome)/settings/hotkeys"), +); +const SettingsChangelogPage = lazy( + () => import("./routes/(window-chrome)/settings/changelog"), +); +const SettingsFeedbackPage = lazy( + () => import("./routes/(window-chrome)/settings/feedback"), +); +const SettingsExperimentalPage = lazy( + () => import("./routes/(window-chrome)/settings/experimental"), +); +const SettingsLicensePage = lazy( + () => import("./routes/(window-chrome)/settings/license"), +); +const SettingsIntegrationsPage = lazy( + () => import("./routes/(window-chrome)/settings/integrations"), +); +const SettingsS3ConfigPage = lazy( + () => import("./routes/(window-chrome)/settings/integrations/s3-config"), +); +const UpgradePage = lazy(() => import("./routes/(window-chrome)/upgrade")); +const UpdatePage = lazy(() => import("./routes/(window-chrome)/update")); +const CameraPage = lazy(() => import("./routes/camera")); +const CaptureAreaPage = lazy(() => import("./routes/capture-area")); +const DebugPage = lazy(() => import("./routes/debug")); +const EditorPage = lazy(() => import("./routes/editor")); +const InProgressRecordingPage = lazy( + () => import("./routes/in-progress-recording"), +); +const ModeSelectPage = lazy(() => import("./routes/mode-select")); +const NotificationsPage = lazy(() => import("./routes/notifications")); +const RecordingsOverlayPage = lazy(() => import("./routes/recordings-overlay")); +const ScreenshotEditorPage = lazy(() => import("./routes/screenshot-editor")); +const TargetSelectOverlayPage = lazy( + () => import("./routes/target-select-overlay"), +); +const WindowCaptureOccluderPage = lazy( + () => import("./routes/window-capture-occluder"), +); + const queryClient = new QueryClient({ defaultOptions: { queries: { @@ -97,7 +151,55 @@ function Inner() { ); }} > - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/desktop/src/entry-client.tsx b/apps/desktop/src/entry-client.tsx index c4f2cfe081..801872019c 100644 --- a/apps/desktop/src/entry-client.tsx +++ b/apps/desktop/src/entry-client.tsx @@ -1,6 +1,16 @@ // @refresh reload import { mount, StartClient } from "@solidjs/start/client"; -import { type } from "@tauri-apps/plugin-os"; -document.documentElement.classList.add(`platform-${type()}`); -mount(() => , document.getElementById("app")!); +async function initApp() { + try { + const { type } = await import("@tauri-apps/plugin-os"); + const osType = type(); + document.documentElement.classList.add(`platform-${osType}`); + } catch (error) { + console.error("Failed to get OS type:", error); + } + + mount(() => , document.getElementById("app")!); +} + +initApp(); diff --git a/apps/desktop/src/routes/(window-chrome).tsx b/apps/desktop/src/routes/(window-chrome).tsx index 6483767acb..f2a78bfc0a 100644 --- a/apps/desktop/src/routes/(window-chrome).tsx +++ b/apps/desktop/src/routes/(window-chrome).tsx @@ -13,12 +13,6 @@ import { WindowChromeContext, } from "./(window-chrome)/Context"; -export const route = { - info: { - AUTO_SHOW_WINDOW: false, - }, -}; - export default function (props: RouteSectionProps) { let unlistenResize: UnlistenFn | undefined; diff --git a/apps/desktop/src/routes/(window-chrome)/setup.tsx b/apps/desktop/src/routes/(window-chrome)/setup.tsx index 1b8fd5449b..cbf4113df9 100644 --- a/apps/desktop/src/routes/(window-chrome)/setup.tsx +++ b/apps/desktop/src/routes/(window-chrome)/setup.tsx @@ -39,6 +39,17 @@ const permissions = [ description: "During recording, Cap collects mouse activity locally to generate automatic zoom in segments.", }, + { + name: "Microphone", + key: "microphone" as const, + description: "This permission is required to record audio in your Caps.", + }, + { + name: "Camera", + key: "camera" as const, + description: + "This permission is required to record your camera in your Caps.", + }, ] as const; export default function () { @@ -83,7 +94,6 @@ export default function () { ); const handleContinue = () => { - // Just proceed to the main window without saving mode to store commands.showWindow({ Main: { init_target_mode: null } }).then(() => { getCurrentWindow().close(); }); diff --git a/apps/desktop/src/routes/editor/Editor.tsx b/apps/desktop/src/routes/editor/Editor.tsx index 4f9444228f..90f2b96d74 100644 --- a/apps/desktop/src/routes/editor/Editor.tsx +++ b/apps/desktop/src/routes/editor/Editor.tsx @@ -44,7 +44,15 @@ import { ExportDialog } from "./ExportDialog"; import { Header } from "./Header"; import { PlayerContent } from "./Player"; import { Timeline } from "./Timeline"; -import { Dialog, DialogContent, EditorButton, Input, Subfield } from "./ui"; +import { + Dialog, + DialogContent, + EditorButton, + Input, + Slider, + Subfield, +} from "./ui"; +import { formatTime } from "./utils"; const DEFAULT_TIMELINE_HEIGHT = 260; const MIN_PLAYER_CONTENT_HEIGHT = 320; @@ -414,13 +422,61 @@ function Dialogs() { })()} > {(dialog) => { - const { setProject: setState, editorInstance } = - useEditorContext(); + const { + setProject: setState, + editorInstance, + editorState, + totalDuration, + project, + } = useEditorContext(); const display = editorInstance.recordings.segments[0].display; let cropperRef: CropperRef | undefined; + let videoRef: HTMLVideoElement | undefined; const [crop, setCrop] = createSignal(CROP_ZERO); const [aspect, setAspect] = createSignal(null); + const [previewTime, setPreviewTime] = createSignal( + editorState.playbackTime, + ); + const [videoLoaded, setVideoLoaded] = createSignal(false); + + const currentSegment = createMemo(() => { + const time = previewTime(); + let elapsed = 0; + for (const seg of project.timeline?.segments ?? []) { + const segDuration = (seg.end - seg.start) / seg.timescale; + if (time < elapsed + segDuration) { + return { + index: seg.recordingSegment ?? 0, + localTime: seg.start / seg.timescale + (time - elapsed), + }; + } + elapsed += segDuration; + } + return { index: 0, localTime: 0 }; + }); + + const videoSrc = createMemo(() => + convertFileSrc( + `${editorInstance.path}/content/segments/segment-${currentSegment().index}/display.mp4`, + ), + ); + + createEffect( + on( + () => currentSegment().index, + () => { + setVideoLoaded(false); + }, + { defer: true }, + ), + ); + + createEffect(() => { + if (videoRef && videoLoaded()) { + videoRef.currentTime = currentSegment().localTime; + } + }); const initialBounds = { x: dialog().position.x, @@ -582,16 +638,60 @@ function Dialogs() { allowLightMode={true} onContextMenu={(e) => showCropOptionsMenu(e, true)} > - screenshot +
+ screenshot +
+
+ + {formatTime(previewTime())} + + setPreviewTime(v)} + aria-label="Video timeline" + /> + + {formatTime(totalDuration())} + +