From 046a696ef2773540a72db731b02a8517e09c013b Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Sat, 2 Nov 2024 01:49:38 +0800 Subject: [PATCH 1/5] fix microphone select --- .github/ISSUE_TEMPLATE/bug_report.md | 5 +- apps/desktop/src-tauri/src/lib.rs | 7 +- apps/desktop/src/auto-imports.d.ts | 92 +- .../src/routes/(window-chrome)/(main).tsx | 666 +++++++++++ .../src/routes/(window-chrome)/index.tsx | 639 ---------- apps/desktop/src/routes/permissions.tsx | 9 +- apps/desktop/src/utils/plans.ts | 7 +- apps/desktop/src/utils/tauri.ts | 1062 ++++++++++------- apps/tasks/src/interfaces/ErrorResponse.ts | 4 +- apps/tasks/src/middlewares.ts | 13 +- apps/web/app/api/caps/share/route.ts | 7 +- apps/web/app/api/changelog/route.ts | 28 +- apps/web/app/api/changelog/status/route.ts | 30 +- apps/web/app/api/desktop/feedback/route.ts | 106 +- apps/web/app/api/invite/accept/route.ts | 124 +- apps/web/app/api/invite/decline/route.ts | 7 +- .../settings/workspace/invite/remove/route.ts | 29 +- .../settings/workspace/invite/send/route.ts | 4 +- apps/web/app/api/video/individual/route.ts | 4 +- apps/web/app/api/video/transcribe/route.ts | 5 +- apps/web/app/api/webhooks/stripe/route.ts | 46 +- apps/web/app/sitemap.ts | 116 +- packages/database/schema.ts | 4 +- packages/ui-solid/src/auto-imports.d.ts | 108 +- packages/utils/src/types/database.ts | 470 ++++---- 25 files changed, 1969 insertions(+), 1623 deletions(-) create mode 100644 apps/desktop/src/routes/(window-chrome)/(main).tsx delete mode 100644 apps/desktop/src/routes/(window-chrome)/index.tsx diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 3a9cb5a15..716311190 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,10 +1,9 @@ --- name: Bug report about: Report a crash, error, or other unusual behaviour by Cap -title: '' +title: "" labels: bug -assignees: '' - +assignees: "" --- ### Description diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index d9d122f6e..46cb06071 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -29,6 +29,7 @@ use cap_project::{ }; use cap_rendering::ProjectUniforms; use cap_utils::create_named_pipe; +use cocoa::foundation::{NSBundle, NSString}; // use display::{list_capture_windows, Bounds, CaptureTarget, FPS}; use general_settings::GeneralSettingsStore; use image::{ImageBuffer, Rgba}; @@ -43,6 +44,7 @@ use scap::frame::Frame; use serde::{Deserialize, Serialize}; use serde_json::json; use specta::Type; +use std::ffi::{CStr, OsStr}; use std::fs::File; use std::io::BufWriter; use std::io::{BufReader, Write}; @@ -2300,7 +2302,7 @@ async fn reset_camera_permissions(_app: AppHandle) -> Result<(), ()> { #[specta::specta] async fn reset_microphone_permissions(_app: AppHandle) -> Result<(), ()> { #[cfg(debug_assertions)] - let bundle_id = "com.apple.Terminal"; + let bundle_id = "dev.warp.Warp-Stable"; #[cfg(not(debug_assertions))] let bundle_id = "so.cap.desktop"; @@ -2401,6 +2403,7 @@ pub async fn run() { open_main_window, permissions::open_permission_settings, permissions::do_permissions_check, + permissions::request_permission, upload_rendered_video, upload_screenshot, get_recording_meta, @@ -2668,8 +2671,6 @@ pub async fn remove_editor_instance( let mut map = map.lock().await; - - if let Some(editor) = map.remove(&video_id) { editor.dispose().await; Some(editor) diff --git a/apps/desktop/src/auto-imports.d.ts b/apps/desktop/src/auto-imports.d.ts index fc54ca398..984e9de67 100644 --- a/apps/desktop/src/auto-imports.d.ts +++ b/apps/desktop/src/auto-imports.d.ts @@ -5,50 +5,50 @@ // Generated by unplugin-auto-import export {} declare global { - const IconHugeiconsDashedLine02: typeof import('~icons/hugeicons/dashed-line02.jsx')['default'] - const IconIcRoundBlurOn: typeof import('~icons/ic/round-blur-on.jsx')['default'] - const IconLucideCamera: typeof import('~icons/lucide/camera.jsx')['default'] - const IconLucideCheck: typeof import('~icons/lucide/check.jsx')['default'] - const IconLucideCheckIcon: typeof import('~icons/lucide/check-icon.jsx')['default'] - const IconLucideChevronDown: typeof import('~icons/lucide/chevron-down.jsx')['default'] - const IconLucideCircle: typeof import('~icons/lucide/circle.jsx')['default'] - const IconLucideCircleCheck: typeof import('~icons/lucide/circle-check.jsx')['default'] - const IconLucideCircleDashed: typeof import('~icons/lucide/circle-dashed.jsx')['default'] - const IconLucideCircleFastForward: typeof import('~icons/lucide/circle-fast-forward.jsx')['default'] - const IconLucideCircleForward: typeof import('~icons/lucide/circle-forward.jsx')['default'] - const IconLucideCirclePlus: typeof import('~icons/lucide/circle-plus.jsx')['default'] - const IconLucideCircleStop: typeof import('~icons/lucide/circle-stop.jsx')['default'] - const IconLucideCommand: typeof import('~icons/lucide/command.jsx')['default'] - const IconLucideCopy: typeof import('~icons/lucide/copy.jsx')['default'] - const IconLucideCrop: typeof import('~icons/lucide/crop.jsx')['default'] - const IconLucideEye: typeof import('~icons/lucide/eye.jsx')['default'] - const IconLucideFaceTime: typeof import('~icons/lucide/face-time.jsx')['default'] - const IconLucideFastForward: typeof import('~icons/lucide/fast-forward.jsx')['default'] - const IconLucideImage: typeof import('~icons/lucide/image.jsx')['default'] - const IconLucideLayoutDashboard: typeof import('~icons/lucide/layout-dashboard.jsx')['default'] - const IconLucideLoaderCircle: typeof import('~icons/lucide/loader-circle.jsx')['default'] - const IconLucideMessageSquareMore: typeof import('~icons/lucide/message-square-more.jsx')['default'] - const IconLucideMousePointer2: typeof import('~icons/lucide/mouse-pointer2.jsx')['default'] - const IconLucidePencil: typeof import('~icons/lucide/pencil.jsx')['default'] - const IconLucidePenicl: typeof import('~icons/lucide/penicl.jsx')['default'] - const IconLucidePlay: typeof import('~icons/lucide/play.jsx')['default'] - const IconLucidePlus: typeof import('~icons/lucide/plus.jsx')['default'] - const IconLucideRedo: typeof import('~icons/lucide/redo.jsx')['default'] - const IconLucideRedo2: typeof import('~icons/lucide/redo2.jsx')['default'] - const IconLucideRewind: typeof import('~icons/lucide/rewind.jsx')['default'] - const IconLucideScissors: typeof import('~icons/lucide/scissors.jsx')['default'] - const IconLucideSettings: typeof import('~icons/lucide/settings.jsx')['default'] - const IconLucideShare: typeof import('~icons/lucide/share.jsx')['default'] - const IconLucideSquare: typeof import('~icons/lucide/square.jsx')['default'] - const IconLucideStop: typeof import('~icons/lucide/stop.jsx')['default'] - const IconLucideUndo: typeof import('~icons/lucide/undo.jsx')['default'] - const IconLucideUndo2: typeof import('~icons/lucide/undo2.jsx')['default'] - const IconLucideVideo: typeof import('~icons/lucide/video.jsx')['default'] - const IconLucideVolume1: typeof import('~icons/lucide/volume1.jsx')['default'] - const IconLucideWandSparkles: typeof import('~icons/lucide/wand-sparkles.jsx')['default'] - const IconLucideWbecam: typeof import('~icons/lucide/wbecam.jsx')['default'] - const IconLucideWebcam: typeof import('~icons/lucide/webcam.jsx')['default'] - const IconMdiBlur: typeof import('~icons/mdi/blur.jsx')['default'] - const IconMiExpand: typeof import('~icons/mi/expand.jsx')['default'] - const IconTablerShadow: typeof import('~icons/tabler/shadow.jsx')['default'] + const IconHugeiconsDashedLine02: typeof import("~icons/hugeicons/dashed-line02.jsx")["default"]; + const IconIcRoundBlurOn: typeof import("~icons/ic/round-blur-on.jsx")["default"]; + const IconLucideCamera: typeof import("~icons/lucide/camera.jsx")["default"]; + const IconLucideCheck: typeof import("~icons/lucide/check.jsx")["default"]; + const IconLucideCheckIcon: typeof import("~icons/lucide/check-icon.jsx")["default"]; + const IconLucideChevronDown: typeof import("~icons/lucide/chevron-down.jsx")["default"]; + const IconLucideCircle: typeof import("~icons/lucide/circle.jsx")["default"]; + const IconLucideCircleCheck: typeof import("~icons/lucide/circle-check.jsx")["default"]; + const IconLucideCircleDashed: typeof import("~icons/lucide/circle-dashed.jsx")["default"]; + const IconLucideCircleFastForward: typeof import("~icons/lucide/circle-fast-forward.jsx")["default"]; + const IconLucideCircleForward: typeof import("~icons/lucide/circle-forward.jsx")["default"]; + const IconLucideCirclePlus: typeof import("~icons/lucide/circle-plus.jsx")["default"]; + const IconLucideCircleStop: typeof import("~icons/lucide/circle-stop.jsx")["default"]; + const IconLucideCommand: typeof import("~icons/lucide/command.jsx")["default"]; + const IconLucideCopy: typeof import("~icons/lucide/copy.jsx")["default"]; + const IconLucideCrop: typeof import("~icons/lucide/crop.jsx")["default"]; + const IconLucideEye: typeof import("~icons/lucide/eye.jsx")["default"]; + const IconLucideFaceTime: typeof import("~icons/lucide/face-time.jsx")["default"]; + const IconLucideFastForward: typeof import("~icons/lucide/fast-forward.jsx")["default"]; + const IconLucideImage: typeof import("~icons/lucide/image.jsx")["default"]; + const IconLucideLayoutDashboard: typeof import("~icons/lucide/layout-dashboard.jsx")["default"]; + const IconLucideLoaderCircle: typeof import("~icons/lucide/loader-circle.jsx")["default"]; + const IconLucideMessageSquareMore: typeof import("~icons/lucide/message-square-more.jsx")["default"]; + const IconLucideMousePointer2: typeof import("~icons/lucide/mouse-pointer2.jsx")["default"]; + const IconLucidePencil: typeof import("~icons/lucide/pencil.jsx")["default"]; + const IconLucidePenicl: typeof import("~icons/lucide/penicl.jsx")["default"]; + const IconLucidePlay: typeof import("~icons/lucide/play.jsx")["default"]; + const IconLucidePlus: typeof import("~icons/lucide/plus.jsx")["default"]; + const IconLucideRedo: typeof import("~icons/lucide/redo.jsx")["default"]; + const IconLucideRedo2: typeof import("~icons/lucide/redo2.jsx")["default"]; + const IconLucideRewind: typeof import("~icons/lucide/rewind.jsx")["default"]; + const IconLucideScissors: typeof import("~icons/lucide/scissors.jsx")["default"]; + const IconLucideSettings: typeof import("~icons/lucide/settings.jsx")["default"]; + const IconLucideShare: typeof import("~icons/lucide/share.jsx")["default"]; + const IconLucideSquare: typeof import("~icons/lucide/square.jsx")["default"]; + const IconLucideStop: typeof import("~icons/lucide/stop.jsx")["default"]; + const IconLucideUndo: typeof import("~icons/lucide/undo.jsx")["default"]; + const IconLucideUndo2: typeof import("~icons/lucide/undo2.jsx")["default"]; + const IconLucideVideo: typeof import("~icons/lucide/video.jsx")["default"]; + const IconLucideVolume1: typeof import("~icons/lucide/volume1.jsx")["default"]; + const IconLucideWandSparkles: typeof import("~icons/lucide/wand-sparkles.jsx")["default"]; + const IconLucideWbecam: typeof import("~icons/lucide/wbecam.jsx")["default"]; + const IconLucideWebcam: typeof import("~icons/lucide/webcam.jsx")["default"]; + const IconMdiBlur: typeof import("~icons/mdi/blur.jsx")["default"]; + const IconMiExpand: typeof import("~icons/mi/expand.jsx")["default"]; + const IconTablerShadow: typeof import("~icons/tabler/shadow.jsx")["default"]; } diff --git a/apps/desktop/src/routes/(window-chrome)/(main).tsx b/apps/desktop/src/routes/(window-chrome)/(main).tsx new file mode 100644 index 000000000..6feb6b9c1 --- /dev/null +++ b/apps/desktop/src/routes/(window-chrome)/(main).tsx @@ -0,0 +1,666 @@ +import { Button } from "@cap/ui-solid"; +import { Select as KSelect } from "@kobalte/core/select"; +import { cache, createAsync, redirect, useNavigate } from "@solidjs/router"; +import { + createMutation, + createQuery, + useQueryClient, +} from "@tanstack/solid-query"; +import { getVersion } from "@tauri-apps/api/app"; +import { cx } from "cva"; +import { + Show, + type ValidComponent, + createEffect, + createMemo, + createResource, + createSignal, + onMount, +} from "solid-js"; +import { createStore } from "solid-js/store"; +import { fetch } from "@tauri-apps/plugin-http"; + +import { authStore } from "~/store"; +import { clientEnv } from "~/utils/env"; +import { + createCurrentRecordingQuery, + createOptionsQuery, + listWindows, + listAudioDevices, + getPermissions, + createVideoDevicesQuery, + listScreens, +} from "~/utils/queries"; +import { + CaptureScreen, + type CaptureWindow, + commands, + events, +} from "~/utils/tauri"; +import { + MenuItem, + MenuItemList, + PopperContent, + topLeftAnimateClasses, + topRightAnimateClasses, +} from "../editor/ui"; + +const getAuth = cache(async () => { + const value = await authStore.get(); + if (!value) return redirect("/signin"); + const res = await fetch(`${clientEnv.VITE_SERVER_URL}/api/desktop/plan`, { + headers: { authorization: `Bearer ${value.token}` }, + }); + if (res.status !== 200) return redirect("/signin"); + return value; +}, "getAuth"); + +export const route = { + load: () => getAuth(), +}; + +export default function () { + const { options, setOptions } = createOptionsQuery(); + const currentRecording = createCurrentRecordingQuery(); + + events.showCapturesPanel.listen(() => { + commands.showPreviousRecordingsWindow(); + }); + + onMount(async () => { + await commands.showPreviousRecordingsWindow(); + // await commands.showNotificationsWindow(); + }); + + const isRecording = () => !!currentRecording.data; + + const toggleRecording = createMutation(() => ({ + mutationFn: async () => { + if (!isRecording()) { + await commands.startRecording(); + } else { + await commands.stopRecording(); + } + }, + })); + + createAsync(() => getAuth()); + + createUpdateCheck(); + + onMount(async () => { + if (options.data?.cameraLabel && options.data.cameraLabel !== "No Camera") { + const cameraWindowActive = await commands.isCameraWindowOpen(); + + if (!cameraWindowActive) { + console.log("cameraWindow not found"); + setOptions({ + ...options.data, + }); + } + } + }); + + return ( +
+
+
+ +
+ +
+
+
+
+ + +
+ + + +
+ + +
+ + Open Cap on Web + +
+ ); +} + +function useRequestPermission() { + const queryClient = useQueryClient(); + + async function requestPermission(type: "camera" | "microphone") { + try { + if (type === "camera") { + await commands.resetCameraPermissions(); + } else if (type === "microphone") { + console.log("wowzers"); + await commands.resetMicrophonePermissions(); + } + await commands.requestPermission(type); + await queryClient.refetchQueries(getPermissions); + } catch (error) { + console.error(`Failed to get ${type} permission:`, error); + } + } + + return requestPermission; +} + +import * as dialog from "@tauri-apps/plugin-dialog"; +import * as updater from "@tauri-apps/plugin-updater"; +import { makePersisted } from "@solid-primitives/storage"; + +let hasChecked = false; +function createUpdateCheck() { + if (import.meta.env.DEV) return; + + const navigate = useNavigate(); + + onMount(async () => { + if (hasChecked) return; + hasChecked = true; + + await new Promise((res) => setTimeout(res, 1000)); + + const update = await updater.check(); + if (!update) return; + + const shouldUpdate = await dialog.confirm( + `Version ${update.version} of Cap is available, would you like to install it?`, + { title: "Update Cap", okLabel: "Update", cancelLabel: "Ignore" } + ); + + if (!shouldUpdate) return; + navigate("/update"); + }); +} + +function TargetSelects(props: { + options: ReturnType["options"]["data"]; +}) { + const screens = createQuery(() => listScreens); + const windows = createQuery(() => listWindows); + + return ( +
+
+
+
+ + options={screens.data ?? []} + onChange={(value) => { + if (!value || !props.options) return; + + commands.setRecordingOptions({ + ...props.options, + captureTarget: { ...value, variant: "screen" }, + }); + }} + value={ + props.options?.captureTarget.variant === "screen" + ? props.options.captureTarget + : null + } + placeholder="Screen" + optionsEmptyText="No screens found" + selected={props.options?.captureTarget.variant === "screen"} + /> + + options={windows.data ?? []} + onChange={(value) => { + if (!props.options) return; + + commands.setRecordingOptions({ + ...props.options, + captureTarget: { ...value, variant: "window" }, + }); + }} + value={ + props.options?.captureTarget.variant === "window" + ? props.options.captureTarget + : null + } + placeholder="Window" + optionsEmptyText="No windows found" + selected={props.options?.captureTarget.variant === "window"} + /> +
+ ); +} + +function CameraSelect(props: { + options: ReturnType["options"]["data"]; + setOptions: ReturnType["setOptions"]; +}) { + const videoDevices = createVideoDevicesQuery(); + const currentRecording = createCurrentRecordingQuery(); + const permissions = createQuery(() => getPermissions); + const requestPermission = useRequestPermission(); + + const [open, setOpen] = createSignal(false); + + const permissionGranted = () => + permissions?.data?.camera === "granted" || + permissions?.data?.camera === "notNeeded"; + + type Option = { isCamera: boolean; name: string }; + + const onChange = async (item: Option | null) => { + if (!item && permissions?.data?.camera !== "granted") { + return requestPermission("camera"); + } + if (!props.options) return; + + if (!item || !item.isCamera) { + props.setOptions({ + ...props.options, + cameraLabel: null, + }); + } else { + props.setOptions({ + ...props.options, + cameraLabel: item.name, + }); + } + }; + + const selectOptions = createMemo(() => [ + { name: "No Camera", isCamera: false }, + ...videoDevices.map((d) => ({ isCamera: true, name: d })), + ]); + + const value = () => + selectOptions()?.find((o) => o.name === props.options?.cameraLabel) ?? null; + + return ( +
+ + + options={selectOptions()} + optionValue="name" + optionTextValue="name" + placeholder="No Camera" + value={value()} + disabled={!!currentRecording.data} + onChange={onChange} + itemComponent={(props) => ( + as={KSelect.Item} item={props.item}> + + {props.item.rawValue.name} + + + )} + open={open()} + onOpenChange={(isOpen) => { + if (!permissionGranted()) { + requestPermission("microphone"); + return; + } + + setOpen(isOpen); + }} + > + + + class="flex-1 text-left truncate"> + {(state) => {state.selectedOption().name}} + + requestPermission("camera")} + onClear={() => { + if (!props.options) return; + props.setOptions({ + ...props.options, + cameraLabel: null, + }); + }} + /> + + + + as={KSelect.Content} + class={topLeftAnimateClasses} + > + + class="max-h-36 overflow-y-auto" + as={KSelect.Listbox} + /> + + + +
+ ); +} + +function MicrophoneSelect(props: { + options: ReturnType["options"]["data"]; + setOptions: ReturnType["setOptions"]; +}) { + const devices = createQuery(() => listAudioDevices); + const permissions = createQuery(() => getPermissions); + const currentRecording = createCurrentRecordingQuery(); + + const [open, setOpen] = createSignal(false); + + const value = () => + devices?.data?.find((d) => d.name === props.options?.audioInputName) ?? + null; + + const requestPermission = useRequestPermission(); + + const permissionGranted = () => + permissions?.data?.microphone === "granted" || + permissions?.data?.microphone === "notNeeded"; + + type Option = { name: string; deviceId: string }; + + const handleMicrophoneChange = async (item: Option | null) => { + if (!item || !props.options) return; + + props.setOptions({ + ...props.options, + audioInputName: item.deviceId !== "" ? item.name : null, + }); + }; + + return ( +
+ + + options={[{ name: "No Audio", deviceId: "" }, ...(devices.data ?? [])]} + optionValue="deviceId" + optionTextValue="name" + placeholder="No Audio" + value={value()} + disabled={!!currentRecording.data} + onChange={handleMicrophoneChange} + itemComponent={(props) => ( + as={KSelect.Item} item={props.item}> + + {props.item.rawValue.name} + + + )} + open={open()} + onOpenChange={(isOpen) => { + if (!permissionGranted()) { + requestPermission("microphone"); + return; + } + + setOpen(isOpen); + }} + > + + + class="flex-1 text-left truncate"> + {(state) => ( + {state.selectedOption()?.name ?? "No Audio"} + )} + + requestPermission("microphone")} + onClear={() => { + if (!props.options) return; + props.setOptions({ + ...props.options, + audioInputName: null, + }); + }} + /> + + + + as={KSelect.Content} + class={topLeftAnimateClasses} + > + + class="max-h-36 overflow-y-auto" + as={KSelect.Listbox} + /> + + + +
+ ); +} + +function TargetSelect(props: { + options: Array; + onChange: (value: T) => void; + value: T | null; + selected: boolean; + optionsEmptyText: string; + placeholder: string; +}) { + return ( + + options={props.options ?? []} + optionValue="id" + optionTextValue="name" + gutter={8} + itemComponent={(props) => ( + as={KSelect.Item} item={props.item}> + + {props.item.rawValue?.name} + + + )} + placement="bottom" + class="max-w-[50%] w-full z-10" + placeholder={props.placeholder} + onChange={(value) => { + if (!value) return; + props.onChange(value); + }} + value={props.value} + > + + as={ + props.options.length === 1 + ? (p) => ( + + ) + : undefined + } + class="flex-1 text-gray-400 py-1 z-10 data-[selected='true']:text-gray-500 peer focus:outline-none transition-colors duration-100 w-full text-nowrap overflow-hidden px-2 flex gap-2 items-center justify-center" + data-selected={props.selected} + onClick={(e) => { + if (props.options.length === 1) { + e.preventDefault(); + props.onChange(props.options[0]); + } + }} + > + class="truncate"> + {(value) => value.selectedOption()?.name} + + {props.options.length > 1 && ( + + + + )} + + + + as={KSelect.Content} + class={topRightAnimateClasses} + > + 0} + fallback={ +
{props.optionsEmptyText}
+ } + > + +
+ +
+ + ); +} + +function TargetSelectInfoPill(props: { + value: T | null; + permissionGranted: boolean; + requestPermission: () => void; + onClear: () => void; +}) { + return ( + + ); +} + +function ChangelogButton() { + const [changelogState, setChangelogState] = makePersisted( + createStore({ + hasUpdate: false, + lastOpenedVersion: "", + changelogClicked: false, + }), + { name: "changelogState" } + ); + + const [currentVersion] = createResource(() => getVersion()); + + const [changelogStatus] = createResource( + () => currentVersion(), + async (version) => { + if (!version) { + return { hasUpdate: false }; + } + const response = await fetch( + `${clientEnv.VITE_SERVER_URL}/api/changelog/status?version=${version}` + ); + return await response.json(); + } + ); + + const handleChangelogClick = () => { + commands.openChangelogWindow(); + const version = currentVersion(); + if (version) { + setChangelogState({ + hasUpdate: false, + lastOpenedVersion: version, + changelogClicked: true, + }); + } + }; + + createEffect(() => { + if (changelogStatus.state === "ready" && currentVersion()) { + const hasUpdate = changelogStatus()?.hasUpdate || false; + if ( + hasUpdate === true && + changelogState.lastOpenedVersion !== currentVersion() + ) { + setChangelogState({ + hasUpdate: true, + lastOpenedVersion: currentVersion(), + changelogClicked: false, + }); + } + } + }); + + return ( + + ); +} diff --git a/apps/desktop/src/routes/(window-chrome)/index.tsx b/apps/desktop/src/routes/(window-chrome)/index.tsx deleted file mode 100644 index 60acc1db4..000000000 --- a/apps/desktop/src/routes/(window-chrome)/index.tsx +++ /dev/null @@ -1,639 +0,0 @@ -import { Button } from "@cap/ui-solid"; -import { Select as KSelect } from "@kobalte/core/select"; -import { cache, createAsync, redirect, useNavigate } from "@solidjs/router"; -import { createMutation, createQuery } from "@tanstack/solid-query"; -import { getVersion } from "@tauri-apps/api/app"; -import { cx } from "cva"; -import { - Show, - type ValidComponent, - createEffect, - createMemo, - createResource, - createSignal, - onMount, -} from "solid-js"; -import { createStore } from "solid-js/store"; -import { fetch } from "@tauri-apps/plugin-http"; - -import { authStore } from "~/store"; -import { clientEnv } from "~/utils/env"; -import { - createCurrentRecordingQuery, - createOptionsQuery, - listWindows, - listAudioDevices, - getPermissions, - createVideoDevicesQuery, - listScreens, -} from "~/utils/queries"; -import { - CaptureScreen, - type CaptureWindow, - commands, - events, -} from "~/utils/tauri"; -import { - MenuItem, - MenuItemList, - PopperContent, - topLeftAnimateClasses, - topRightAnimateClasses, -} from "../editor/ui"; - -const getAuth = cache(async () => { - const value = await authStore.get(); - if (!value) return redirect("/signin"); - const res = await fetch(`${clientEnv.VITE_SERVER_URL}/api/desktop/plan`, { - headers: { authorization: `Bearer ${value.token}` }, - }); - if (res.status !== 200) return redirect("/signin"); - return value; -}, "getAuth"); - -export const route = { - load: () => getAuth(), -}; - -export default function () { - const screens = createQuery(() => listScreens); - const { options, setOptions } = createOptionsQuery(); - const windows = createQuery(() => listWindows); - const videoDevices = createVideoDevicesQuery(); - const audioDevices = createQuery(() => listAudioDevices); - const currentRecording = createCurrentRecordingQuery(); - - const permissions = createQuery(() => getPermissions); - - const [microphoneSelectOpen, setMicrophoneSelectOpen] = createSignal(false); - - events.showCapturesPanel.listen(() => { - commands.showPreviousRecordingsWindow(); - }); - - onMount(async () => { - await commands.showPreviousRecordingsWindow(); - // await commands.showNotificationsWindow(); - }); - - const isRecording = () => !!currentRecording.data; - - const toggleRecording = createMutation(() => ({ - mutationFn: async () => { - if (!isRecording()) { - await commands.startRecording(); - } else { - await commands.stopRecording(); - } - }, - })); - - createAsync(() => getAuth()); - - createUpdateCheck(); - - const [changelogState, setChangelogState] = makePersisted( - createStore({ - hasUpdate: false, - lastOpenedVersion: "", - changelogClicked: false, - }), - { name: "changelogState" } - ); - - const [currentVersion] = createResource(() => getVersion()); - - const [changelogStatus] = createResource( - () => currentVersion(), - async (version) => { - if (!version) { - return { hasUpdate: false }; - } - const response = await fetch( - `${clientEnv.VITE_SERVER_URL}/api/changelog/status?version=${version}` - ); - return await response.json(); - } - ); - - createEffect(() => { - if (changelogStatus.state === "ready" && currentVersion()) { - const hasUpdate = changelogStatus()?.hasUpdate || false; - if ( - hasUpdate === true && - changelogState.lastOpenedVersion !== currentVersion() - ) { - setChangelogState({ - hasUpdate: true, - lastOpenedVersion: currentVersion(), - changelogClicked: false, - }); - } - } - }); - - const handleChangelogClick = () => { - commands.openChangelogWindow(); - const version = currentVersion(); - if (version) { - setChangelogState({ - hasUpdate: false, - lastOpenedVersion: version, - changelogClicked: true, - }); - } - }; - - const audioDevice = () => - audioDevices?.data?.find( - (d) => d.name === options.data?.audioInputName - ) ?? { name: "No Audio", deviceId: "" }; - - const requestPermission = async (type: "camera" | "microphone") => { - try { - if (type === "camera") { - await commands.resetCameraPermissions(); - } else if (type === "microphone") { - await commands.resetMicrophonePermissions(); - } - await commands.requestPermission(type); - // Refresh permissions after request - await permissions.refetch(); - } catch (error) { - console.error(`Failed to get ${type} permission:`, error); - } - }; - - const handleMicrophoneChange = async ( - item: { name: string; deviceId: string } | null - ) => { - if (!item && permissions?.data?.microphone !== "granted") { - return requestPermission("microphone"); - } - - if (!item || !options.data) return; - - setOptions({ - ...options.data, - audioInputName: item.name !== "No Audio" ? item.name : null, - }); - }; - - onMount(async () => { - if (options.data?.cameraLabel && options.data.cameraLabel !== "No Camera") { - const cameraWindowActive = await commands.isCameraWindowOpen(); - - if (!cameraWindowActive) { - console.log("cameraWindow not found"); - setOptions({ - ...options.data, - }); - } - } - }); - - return ( -
-
-
- -
- -
-
-
-
- - -
-
-
-
-
- - options={screens.data ?? []} - onChange={(value) => { - if (!value || !options.data) return; - - commands.setRecordingOptions({ - ...options.data, - captureTarget: { ...value, variant: "screen" }, - }); - }} - value={ - options.data?.captureTarget.variant === "screen" - ? options.data.captureTarget - : null - } - placeholder="Screen" - optionsEmptyText="No screens found" - selected={options.data?.captureTarget.variant === "screen"} - /> - - options={windows.data ?? []} - onChange={(value) => { - if (!options.data) return; - - commands.setRecordingOptions({ - ...options.data, - captureTarget: { ...value, variant: "window" }, - }); - }} - value={ - options.data?.captureTarget.variant === "window" - ? options.data.captureTarget - : null - } - placeholder="Window" - optionsEmptyText="No windows found" - selected={options.data?.captureTarget.variant === "window"} - /> -
-
- - - {(_) => { - type Option = { isCamera: boolean; name: string }; - - const onChange = async (item: Option | null) => { - if (!item && permissions?.data?.camera !== "granted") { - return requestPermission("camera"); - } - - if (!options.data) return; - - if (!item || !item.isCamera) { - setOptions({ - ...options.data, - cameraLabel: null, - }); - } else { - setOptions({ - ...options.data, - cameraLabel: item.name, - }); - } - }; - const selectOptions = createMemo(() => [ - { name: "No Camera", isCamera: false }, - ...videoDevices.map((d) => ({ isCamera: true, name: d })), - ]); - - const value = () => - selectOptions()?.find( - (o) => o.name === options.data?.cameraLabel - ) ?? null; - - return ( - - options={selectOptions()} - optionValue="name" - optionTextValue="name" - placeholder="No Camera" - value={value()} - disabled={isRecording()} - onChange={onChange} - itemComponent={(props) => ( - - as={KSelect.Item} - item={props.item} - > - - {props.item.rawValue.name} - - - )} - > - { - if (permissions?.data?.camera !== "granted") { - requestPermission("camera"); - } - }} - > - - class="flex-1 text-left truncate"> - {(state) => {state.selectedOption().name}} - - - - - - as={KSelect.Content} - class={topLeftAnimateClasses} - > - - class="max-h-36 overflow-y-auto" - as={KSelect.Listbox} - /> - - - - ); - }} - -
-
- - - options={[ - { name: "No Audio", deviceId: "" }, - ...(audioDevices.data ?? []), - ]} - optionValue="deviceId" - optionTextValue="name" - placeholder="No Audio" - value={audioDevice()} - disabled={isRecording()} - onChange={handleMicrophoneChange} - itemComponent={(props) => ( - as={KSelect.Item} item={props.item}> - - {props.item.rawValue.name} - - - )} - open={microphoneSelectOpen()} - onOpenChange={async (isOpen: boolean) => { - if (isOpen) { - if (audioDevice().name === "No Audio") { - setMicrophoneSelectOpen(false); - await audioDevices.refetch(); - } - setMicrophoneSelectOpen(true); - } else { - setMicrophoneSelectOpen(false); - } - }} - > - { - if (permissions?.data?.microphone !== "granted") { - requestPermission("microphone"); - } - }} - > - - class="flex-1 text-left truncate"> - {(state) => ( - {state.selectedOption()?.name ?? "No Audio"} - )} - - - - - - as={KSelect.Content} - class={topLeftAnimateClasses} - > - - class="max-h-36 overflow-y-auto" - as={KSelect.Listbox} - /> - - - -
-
- - -
- - Open Cap on Web - -
- ); -} - -export const searchParams = "hello!!!"; - -import * as dialog from "@tauri-apps/plugin-dialog"; -import * as updater from "@tauri-apps/plugin-updater"; -import { makePersisted } from "@solid-primitives/storage"; -import { createEventListener } from "@solid-primitives/event-listener"; - -let hasChecked = false; -function createUpdateCheck() { - if (import.meta.env.DEV) return; - - const navigate = useNavigate(); - - onMount(async () => { - if (hasChecked) return; - hasChecked = true; - - await new Promise((res) => setTimeout(res, 1000)); - - const update = await updater.check(); - if (!update) return; - - const shouldUpdate = await dialog.confirm( - `Version ${update.version} of Cap is available, would you like to install it?`, - { title: "Update Cap", okLabel: "Update", cancelLabel: "Ignore" } - ); - - if (!shouldUpdate) return; - navigate("/update"); - }); -} - -function TargetSelect(props: { - options: Array; - onChange: (value: T) => void; - value: T | null; - selected: boolean; - optionsEmptyText: string; - placeholder: string; -}) { - return ( - - options={props.options ?? []} - optionValue="id" - optionTextValue="name" - gutter={8} - itemComponent={(props) => ( - as={KSelect.Item} item={props.item}> - - {props.item.rawValue?.name} - - - )} - placement="bottom" - class="max-w-[50%] w-full z-10" - placeholder={props.placeholder} - onChange={(value) => { - if (!value) return; - props.onChange(value); - }} - value={props.value} - > - - as={ - props.options.length === 1 - ? (p) => ( - - ) - : undefined - } - class="flex-1 text-gray-400 py-1 z-10 data-[selected='true']:text-gray-500 peer focus:outline-none transition-colors duration-100 w-full text-nowrap overflow-hidden px-2 flex gap-2 items-center justify-center" - data-selected={props.selected} - onClick={(e) => { - if (props.options.length === 1) { - e.preventDefault(); - props.onChange(props.options[0]); - } - }} - > - class="truncate"> - {(value) => value.selectedOption()?.name} - - {props.options.length > 1 && ( - - - - )} - - - - as={KSelect.Content} - class={topRightAnimateClasses} - > - 0} - fallback={ -
{props.optionsEmptyText}
- } - > - -
- -
- - ); -} diff --git a/apps/desktop/src/routes/permissions.tsx b/apps/desktop/src/routes/permissions.tsx index c142ae35d..0d31e4d7d 100644 --- a/apps/desktop/src/routes/permissions.tsx +++ b/apps/desktop/src/routes/permissions.tsx @@ -44,10 +44,11 @@ export default function (props: RouteSectionProps) { const neededStep = steps.findIndex((step) => !isPermitted(c?.[step.key])); if (neededStep === -1) { - // All permissions now granted - commands.openMainWindow(); - const window = getCurrentWindow(); - window.close(); + // We wait for the window to open as closing immediately after seems to cause an unlabeled crash + commands.openMainWindow().then(() => { + const window = getCurrentWindow(); + window.close(); + }); } else { setCurrentStepIndex(neededStep); } diff --git a/apps/desktop/src/utils/plans.ts b/apps/desktop/src/utils/plans.ts index dec332013..0ddb16760 100644 --- a/apps/desktop/src/utils/plans.ts +++ b/apps/desktop/src/utils/plans.ts @@ -10,7 +10,10 @@ const planIds = { }; export const getProPlanId = (billingCycle: "yearly" | "monthly") => { - const environment = import.meta.env.VITE_ENVIRONMENT === "development" ? "development" : "production"; + const environment = + import.meta.env.VITE_ENVIRONMENT === "development" + ? "development" + : "production"; return planIds[environment]?.[billingCycle] || ""; }; @@ -29,4 +32,4 @@ export const isUserOnProPlan = ({ } return false; -}; \ No newline at end of file +}; diff --git a/apps/desktop/src/utils/tauri.ts b/apps/desktop/src/utils/tauri.ts index 5077916ab..29302a97a 100644 --- a/apps/desktop/src/utils/tauri.ts +++ b/apps/desktop/src/utils/tauri.ts @@ -1,491 +1,739 @@ - // This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually. /** user-defined commands **/ - export const commands = { -async getRecordingOptions() : Promise> { + async getRecordingOptions(): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("get_recording_options") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async setRecordingOptions(options: RecordingOptions) : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("get_recording_options"), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async setRecordingOptions( + options: RecordingOptions + ): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("set_recording_options", { options }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async startRecording() : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("set_recording_options", { options }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async startRecording(): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("start_recording") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async stopRecording() : Promise> { + return { status: "ok", data: await TAURI_INVOKE("start_recording") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async stopRecording(): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("stop_recording") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async pauseRecording() : Promise> { + return { status: "ok", data: await TAURI_INVOKE("stop_recording") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async pauseRecording(): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("pause_recording") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async resumeRecording() : Promise> { + return { status: "ok", data: await TAURI_INVOKE("pause_recording") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async resumeRecording(): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("resume_recording") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async takeScreenshot() : Promise> { + return { status: "ok", data: await TAURI_INVOKE("resume_recording") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async takeScreenshot(): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("take_screenshot") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async listCameras() : Promise { + return { status: "ok", data: await TAURI_INVOKE("take_screenshot") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async listCameras(): Promise { return await TAURI_INVOKE("list_cameras"); -}, -async listCaptureWindows() : Promise { + }, + async listCaptureWindows(): Promise { return await TAURI_INVOKE("list_capture_windows"); -}, -async listCaptureScreens() : Promise { + }, + async listCaptureScreens(): Promise { return await TAURI_INVOKE("list_capture_screens"); -}, -async listAudioDevices() : Promise> { + }, + async listAudioDevices(): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("list_audio_devices") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async showPreviousRecordingsWindow() : Promise { + return { status: "ok", data: await TAURI_INVOKE("list_audio_devices") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async showPreviousRecordingsWindow(): Promise { await TAURI_INVOKE("show_previous_recordings_window"); -}, -async closePreviousRecordingsWindow() : Promise { + }, + async closePreviousRecordingsWindow(): Promise { await TAURI_INVOKE("close_previous_recordings_window"); -}, -async setFakeWindowBounds(name: string, bounds: Bounds) : Promise> { + }, + async setFakeWindowBounds( + name: string, + bounds: Bounds + ): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("set_fake_window_bounds", { name, bounds }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async removeFakeWindow(name: string) : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("set_fake_window_bounds", { name, bounds }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async removeFakeWindow(name: string): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("remove_fake_window", { name }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async focusCapturesPanel() : Promise { + return { + status: "ok", + data: await TAURI_INVOKE("remove_fake_window", { name }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async focusCapturesPanel(): Promise { await TAURI_INVOKE("focus_captures_panel"); -}, -async getCurrentRecording() : Promise, null>> { + }, + async getCurrentRecording(): Promise< + Result, null> + > { try { - return { status: "ok", data: await TAURI_INVOKE("get_current_recording") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async renderToFile(outputPath: string, videoId: string, project: ProjectConfiguration, progressChannel: TAURI_CHANNEL) : Promise { - await TAURI_INVOKE("render_to_file", { outputPath, videoId, project, progressChannel }); -}, -async getRenderedVideo(videoId: string, project: ProjectConfiguration) : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("get_current_recording"), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async renderToFile( + outputPath: string, + videoId: string, + project: ProjectConfiguration, + progressChannel: TAURI_CHANNEL + ): Promise { + await TAURI_INVOKE("render_to_file", { + outputPath, + videoId, + project, + progressChannel, + }); + }, + async getRenderedVideo( + videoId: string, + project: ProjectConfiguration + ): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("get_rendered_video", { videoId, project }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async copyFileToPath(src: string, dst: string) : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("get_rendered_video", { videoId, project }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async copyFileToPath( + src: string, + dst: string + ): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("copy_file_to_path", { src, dst }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async copyRenderedVideoToClipboard(videoId: string, project: ProjectConfiguration) : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("copy_file_to_path", { src, dst }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async copyRenderedVideoToClipboard( + videoId: string, + project: ProjectConfiguration + ): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("copy_rendered_video_to_clipboard", { videoId, project }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async copyScreenshotToClipboard(path: string) : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("copy_rendered_video_to_clipboard", { + videoId, + project, + }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async copyScreenshotToClipboard(path: string): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("copy_screenshot_to_clipboard", { path }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async openFilePath(path: string) : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("copy_screenshot_to_clipboard", { path }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async openFilePath(path: string): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("open_file_path", { path }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async getVideoMetadata(videoId: string, videoType: VideoType | null) : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("open_file_path", { path }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getVideoMetadata( + videoId: string, + videoType: VideoType | null + ): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("get_video_metadata", { videoId, videoType }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async createEditorInstance(videoId: string) : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("get_video_metadata", { videoId, videoType }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async createEditorInstance( + videoId: string + ): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("create_editor_instance", { videoId }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async startPlayback(videoId: string) : Promise { + return { + status: "ok", + data: await TAURI_INVOKE("create_editor_instance", { videoId }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async startPlayback(videoId: string): Promise { await TAURI_INVOKE("start_playback", { videoId }); -}, -async stopPlayback(videoId: string) : Promise { + }, + async stopPlayback(videoId: string): Promise { await TAURI_INVOKE("stop_playback", { videoId }); -}, -async setPlayheadPosition(videoId: string, frameNumber: number) : Promise { + }, + async setPlayheadPosition( + videoId: string, + frameNumber: number + ): Promise { await TAURI_INVOKE("set_playhead_position", { videoId, frameNumber }); -}, -async openInFinder(path: string) : Promise { + }, + async openInFinder(path: string): Promise { await TAURI_INVOKE("open_in_finder", { path }); -}, -async setProjectConfig(videoId: string, config: ProjectConfiguration) : Promise { + }, + async setProjectConfig( + videoId: string, + config: ProjectConfiguration + ): Promise { await TAURI_INVOKE("set_project_config", { videoId, config }); -}, -async openEditor(id: string) : Promise { + }, + async openEditor(id: string): Promise { await TAURI_INVOKE("open_editor", { id }); -}, -async openMainWindow() : Promise { + }, + async openMainWindow(): Promise { await TAURI_INVOKE("open_main_window"); -}, -async openPermissionSettings(permission: OSPermission) : Promise { + }, + async openPermissionSettings(permission: OSPermission): Promise { await TAURI_INVOKE("open_permission_settings", { permission }); -}, -async doPermissionsCheck(initialCheck: boolean) : Promise { + }, + async doPermissionsCheck(initialCheck: boolean): Promise { return await TAURI_INVOKE("do_permissions_check", { initialCheck }); -}, -async uploadRenderedVideo(videoId: string, project: ProjectConfiguration, preCreatedVideo: PreCreatedVideo | null) : Promise> { + }, + async requestPermission(permission: OSPermission): Promise { + await TAURI_INVOKE("request_permission", { permission }); + }, + async uploadRenderedVideo( + videoId: string, + project: ProjectConfiguration, + preCreatedVideo: PreCreatedVideo | null + ): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("upload_rendered_video", { videoId, project, preCreatedVideo }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async uploadScreenshot(screenshotPath: string) : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("upload_rendered_video", { + videoId, + project, + preCreatedVideo, + }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async uploadScreenshot( + screenshotPath: string + ): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("upload_screenshot", { screenshotPath }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async getRecordingMeta(id: string, fileType: string) : Promise { + return { + status: "ok", + data: await TAURI_INVOKE("upload_screenshot", { screenshotPath }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async getRecordingMeta(id: string, fileType: string): Promise { return await TAURI_INVOKE("get_recording_meta", { id, fileType }); -}, -async openFeedbackWindow() : Promise { + }, + async openFeedbackWindow(): Promise { await TAURI_INVOKE("open_feedback_window"); -}, -async openSettingsWindow(page: string) : Promise { + }, + async openSettingsWindow(page: string): Promise { await TAURI_INVOKE("open_settings_window", { page }); -}, -async openChangelogWindow() : Promise { + }, + async openChangelogWindow(): Promise { await TAURI_INVOKE("open_changelog_window"); -}, -async openUpgradeWindow() : Promise { + }, + async openUpgradeWindow(): Promise { await TAURI_INVOKE("open_upgrade_window"); -}, -async saveFileDialog(fileName: string, fileType: string) : Promise> { + }, + async saveFileDialog( + fileName: string, + fileType: string + ): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("save_file_dialog", { fileName, fileType }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async listRecordings() : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("save_file_dialog", { fileName, fileType }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async listRecordings(): Promise< + Result<[string, string, RecordingMeta][], string> + > { try { - return { status: "ok", data: await TAURI_INVOKE("list_recordings") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async listScreenshots() : Promise> { + return { status: "ok", data: await TAURI_INVOKE("list_recordings") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async listScreenshots(): Promise< + Result<[string, string, RecordingMeta][], string> + > { try { - return { status: "ok", data: await TAURI_INVOKE("list_screenshots") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async checkUpgradedAndUpdate() : Promise> { + return { status: "ok", data: await TAURI_INVOKE("list_screenshots") }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async checkUpgradedAndUpdate(): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("check_upgraded_and_update") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async openExternalLink(url: string) : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("check_upgraded_and_update"), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async openExternalLink(url: string): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("open_external_link", { url }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async setHotkey(action: HotkeyAction, hotkey: Hotkey | null) : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("open_external_link", { url }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async setHotkey( + action: HotkeyAction, + hotkey: Hotkey | null + ): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("set_hotkey", { action, hotkey }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async setGeneralSettings(settings: GeneralSettingsStore) : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("set_hotkey", { action, hotkey }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async setGeneralSettings( + settings: GeneralSettingsStore + ): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("set_general_settings", { settings }) }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async deleteAuthOpenSignin() : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("set_general_settings", { settings }), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async deleteAuthOpenSignin(): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("delete_auth_open_signin") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async resetCameraPermissions() : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("delete_auth_open_signin"), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async resetCameraPermissions(): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("reset_camera_permissions") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async resetMicrophonePermissions() : Promise> { + return { + status: "ok", + data: await TAURI_INVOKE("reset_camera_permissions"), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async resetMicrophonePermissions(): Promise> { try { - return { status: "ok", data: await TAURI_INVOKE("reset_microphone_permissions") }; -} catch (e) { - if(e instanceof Error) throw e; - else return { status: "error", error: e as any }; -} -}, -async isCameraWindowOpen() : Promise { + return { + status: "ok", + data: await TAURI_INVOKE("reset_microphone_permissions"), + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: "error", error: e as any }; + } + }, + async isCameraWindowOpen(): Promise { return await TAURI_INVOKE("is_camera_window_open"); -}, -async seekTo(videoId: string, frameNumber: number) : Promise { + }, + async seekTo(videoId: string, frameNumber: number): Promise { await TAURI_INVOKE("seek_to", { videoId, frameNumber }); -} -} + }, +}; /** user-defined events **/ - export const events = __makeEvents__<{ -authenticationInvalid: AuthenticationInvalid, -currentRecordingChanged: CurrentRecordingChanged, -editorStateChanged: EditorStateChanged, -newNotification: NewNotification, -newRecordingAdded: NewRecordingAdded, -newScreenshotAdded: NewScreenshotAdded, -recordingMetaChanged: RecordingMetaChanged, -recordingOptionsChanged: RecordingOptionsChanged, -recordingStarted: RecordingStarted, -recordingStopped: RecordingStopped, -renderFrameEvent: RenderFrameEvent, -requestNewScreenshot: RequestNewScreenshot, -requestOpenSettings: RequestOpenSettings, -requestRestartRecording: RequestRestartRecording, -requestStartRecording: RequestStartRecording, -requestStopRecording: RequestStopRecording, -showCapturesPanel: ShowCapturesPanel + authenticationInvalid: AuthenticationInvalid; + currentRecordingChanged: CurrentRecordingChanged; + editorStateChanged: EditorStateChanged; + newNotification: NewNotification; + newRecordingAdded: NewRecordingAdded; + newScreenshotAdded: NewScreenshotAdded; + recordingMetaChanged: RecordingMetaChanged; + recordingOptionsChanged: RecordingOptionsChanged; + recordingStarted: RecordingStarted; + recordingStopped: RecordingStopped; + renderFrameEvent: RenderFrameEvent; + requestNewScreenshot: RequestNewScreenshot; + requestOpenSettings: RequestOpenSettings; + requestRestartRecording: RequestRestartRecording; + requestStartRecording: RequestStartRecording; + requestStopRecording: RequestStopRecording; + showCapturesPanel: ShowCapturesPanel; }>({ -authenticationInvalid: "authentication-invalid", -currentRecordingChanged: "current-recording-changed", -editorStateChanged: "editor-state-changed", -newNotification: "new-notification", -newRecordingAdded: "new-recording-added", -newScreenshotAdded: "new-screenshot-added", -recordingMetaChanged: "recording-meta-changed", -recordingOptionsChanged: "recording-options-changed", -recordingStarted: "recording-started", -recordingStopped: "recording-stopped", -renderFrameEvent: "render-frame-event", -requestNewScreenshot: "request-new-screenshot", -requestOpenSettings: "request-open-settings", -requestRestartRecording: "request-restart-recording", -requestStartRecording: "request-start-recording", -requestStopRecording: "request-stop-recording", -showCapturesPanel: "show-captures-panel" -}) + authenticationInvalid: "authentication-invalid", + currentRecordingChanged: "current-recording-changed", + editorStateChanged: "editor-state-changed", + newNotification: "new-notification", + newRecordingAdded: "new-recording-added", + newScreenshotAdded: "new-screenshot-added", + recordingMetaChanged: "recording-meta-changed", + recordingOptionsChanged: "recording-options-changed", + recordingStarted: "recording-started", + recordingStopped: "recording-stopped", + renderFrameEvent: "render-frame-event", + requestNewScreenshot: "request-new-screenshot", + requestOpenSettings: "request-open-settings", + requestRestartRecording: "request-restart-recording", + requestStartRecording: "request-start-recording", + requestStopRecording: "request-stop-recording", + showCapturesPanel: "show-captures-panel", +}); /** user-defined constants **/ - - /** user-defined types **/ -export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall" -export type Audio = { duration: number; sample_rate: number; channels: number } -export type AudioConfiguration = { mute: boolean; improve: boolean } -export type AudioMeta = { path: string } -export type AuthStore = { token: string; expires: number; plan: Plan | null } -export type AuthenticationInvalid = null -export type BackgroundConfiguration = { source: BackgroundSource; blur: number; padding: number; rounding: number; inset: number; crop: Crop | null } -export type BackgroundSource = { type: "wallpaper"; id: number } | { type: "image"; path: string | null } | { type: "color"; value: [number, number, number] } | { type: "gradient"; from: [number, number, number]; to: [number, number, number]; angle?: number } -export type Bounds = { x: number; y: number; width: number; height: number } -export type CameraConfiguration = { hide: boolean; mirror: boolean; position: CameraPosition; rounding: number; shadow: number; size: number } -export type CameraMeta = { path: string } -export type CameraPosition = { x: CameraXPosition; y: CameraYPosition } -export type CameraXPosition = "left" | "center" | "right" -export type CameraYPosition = "top" | "bottom" -export type CaptureScreen = { id: number; name: string } -export type CaptureWindow = { id: number; name: string; bounds: Bounds } -export type Crop = { position: XY; size: XY } -export type CurrentRecordingChanged = JsonValue -export type CursorConfiguration = { hideWhenIdle: boolean; size: number; type: CursorType } -export type CursorType = "pointer" | "circle" -export type Display = { path: string } -export type EditorStateChanged = { playhead_position: number } -export type Flags = { recordMouse: boolean; split: boolean; pauseResume: boolean; zoom: boolean } -export type GeneralSettingsStore = { upload_individual_files: boolean; open_editor_after_recording: boolean; hide_dock_icon?: boolean; auto_create_shareable_link?: boolean; enable_notifications?: boolean } -export type Hotkey = { code: string; meta: boolean; ctrl: boolean; alt: boolean; shift: boolean } -export type HotkeyAction = "startRecording" | "stopRecording" | "restartRecording" | "takeScreenshot" -export type HotkeysConfiguration = { show: boolean } -export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } } -export type InProgressRecording = { recordingDir: string; displaySource: ScreenCaptureTarget; segments: number[] } -export type JsonValue = [T] -export type NewNotification = { title: string; body: string; is_error: boolean } -export type NewRecordingAdded = { path: string } -export type NewScreenshotAdded = { path: string } -export type OSPermission = "screenRecording" | "camera" | "microphone" | "accessibility" -export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied" -export type OSPermissionsCheck = { screenRecording: OSPermissionStatus; microphone: OSPermissionStatus; camera: OSPermissionStatus; accessibility: OSPermissionStatus } -export type Plan = { upgraded: boolean; last_checked: number } -export type PreCreatedVideo = { id: string; link: string; config: S3UploadMeta } -export type ProjectConfiguration = { aspectRatio: AspectRatio | null; background: BackgroundConfiguration; camera: CameraConfiguration; audio: AudioConfiguration; cursor: CursorConfiguration; hotkeys: HotkeysConfiguration; timeline?: TimelineConfiguration | null } -export type ProjectRecordings = { display: Video; camera: Video | null; audio: Audio | null } -export type RecordingMeta = { pretty_name: string; sharing?: SharingMeta | null; display: Display; camera?: CameraMeta | null; audio?: AudioMeta | null; segments?: RecordingSegment[]; cursor: string | null } -export type RecordingMetaChanged = { id: string } -export type RecordingOptions = { captureTarget: ScreenCaptureTarget; cameraLabel: string | null; audioInputName: string | null } -export type RecordingOptionsChanged = null -export type RecordingSegment = { start: number; end: number } -export type RecordingStarted = null -export type RecordingStopped = { path: string } -export type RenderFrameEvent = { frame_number: number } -export type RenderProgress = { type: "Starting"; total_frames: number } | { type: "EstimatedTotalFrames"; total_frames: number } | { type: "FrameRendered"; current_frame: number } -export type RequestNewScreenshot = null -export type RequestOpenSettings = { page: string } -export type RequestRestartRecording = null -export type RequestStartRecording = null -export type RequestStopRecording = null -export type S3UploadMeta = { id: string; user_id: string; aws_region: string; aws_bucket: string } -export type ScreenCaptureTarget = ({ variant: "window" } & CaptureWindow) | ({ variant: "screen" } & CaptureScreen) -export type SerializedEditorInstance = { framesSocketUrl: string; recordingDuration: number; savedProjectConfig: ProjectConfiguration; recordings: ProjectRecordings; path: string; prettyName: string } -export type SharingMeta = { id: string; link: string } -export type ShowCapturesPanel = null -export type TimelineConfiguration = { segments: TimelineSegment[]; zoomSegments?: ZoomSegment[] } -export type TimelineSegment = { timescale: number; start: number; end: number } -export type UploadResult = { Success: string } | "NotAuthenticated" | "PlanCheckFailed" | "UpgradeRequired" -export type Video = { duration: number; width: number; height: number; fps: number } -export type VideoType = "screen" | "output" -export type XY = { x: T; y: T } -export type ZoomSegment = { start: number; end: number; amount: number } +export type AspectRatio = "wide" | "vertical" | "square" | "classic" | "tall"; +export type Audio = { duration: number; sample_rate: number; channels: number }; +export type AudioConfiguration = { mute: boolean; improve: boolean }; +export type AudioMeta = { path: string }; +export type AuthStore = { token: string; expires: number; plan: Plan | null }; +export type AuthenticationInvalid = null; +export type BackgroundConfiguration = { + source: BackgroundSource; + blur: number; + padding: number; + rounding: number; + inset: number; + crop: Crop | null; +}; +export type BackgroundSource = + | { type: "wallpaper"; id: number } + | { type: "image"; path: string | null } + | { type: "color"; value: [number, number, number] } + | { + type: "gradient"; + from: [number, number, number]; + to: [number, number, number]; + angle?: number; + }; +export type Bounds = { x: number; y: number; width: number; height: number }; +export type CameraConfiguration = { + hide: boolean; + mirror: boolean; + position: CameraPosition; + rounding: number; + shadow: number; + size: number; +}; +export type CameraMeta = { path: string }; +export type CameraPosition = { x: CameraXPosition; y: CameraYPosition }; +export type CameraXPosition = "left" | "center" | "right"; +export type CameraYPosition = "top" | "bottom"; +export type CaptureScreen = { id: number; name: string }; +export type CaptureWindow = { id: number; name: string; bounds: Bounds }; +export type Crop = { position: XY; size: XY }; +export type CurrentRecordingChanged = JsonValue; +export type CursorConfiguration = { + hideWhenIdle: boolean; + size: number; + type: CursorType; +}; +export type CursorType = "pointer" | "circle"; +export type Display = { path: string }; +export type EditorStateChanged = { playhead_position: number }; +export type Flags = { + recordMouse: boolean; + split: boolean; + pauseResume: boolean; + zoom: boolean; +}; +export type GeneralSettingsStore = { + upload_individual_files: boolean; + open_editor_after_recording: boolean; + hide_dock_icon?: boolean; + auto_create_shareable_link?: boolean; + enable_notifications?: boolean; +}; +export type Hotkey = { + code: string; + meta: boolean; + ctrl: boolean; + alt: boolean; + shift: boolean; +}; +export type HotkeyAction = + | "startRecording" + | "stopRecording" + | "restartRecording" + | "takeScreenshot"; +export type HotkeysConfiguration = { show: boolean }; +export type HotkeysStore = { hotkeys: { [key in HotkeyAction]: Hotkey } }; +export type InProgressRecording = { + recordingDir: string; + displaySource: ScreenCaptureTarget; + segments: number[]; +}; +export type JsonValue = [T]; +export type NewNotification = { + title: string; + body: string; + is_error: boolean; +}; +export type NewRecordingAdded = { path: string }; +export type NewScreenshotAdded = { path: string }; +export type OSPermission = + | "screenRecording" + | "camera" + | "microphone" + | "accessibility"; +export type OSPermissionStatus = "notNeeded" | "empty" | "granted" | "denied"; +export type OSPermissionsCheck = { + screenRecording: OSPermissionStatus; + microphone: OSPermissionStatus; + camera: OSPermissionStatus; + accessibility: OSPermissionStatus; +}; +export type Plan = { upgraded: boolean; last_checked: number }; +export type PreCreatedVideo = { + id: string; + link: string; + config: S3UploadMeta; +}; +export type ProjectConfiguration = { + aspectRatio: AspectRatio | null; + background: BackgroundConfiguration; + camera: CameraConfiguration; + audio: AudioConfiguration; + cursor: CursorConfiguration; + hotkeys: HotkeysConfiguration; + timeline?: TimelineConfiguration | null; +}; +export type ProjectRecordings = { + display: Video; + camera: Video | null; + audio: Audio | null; +}; +export type RecordingMeta = { + pretty_name: string; + sharing?: SharingMeta | null; + display: Display; + camera?: CameraMeta | null; + audio?: AudioMeta | null; + segments?: RecordingSegment[]; + cursor: string | null; +}; +export type RecordingMetaChanged = { id: string }; +export type RecordingOptions = { + captureTarget: ScreenCaptureTarget; + cameraLabel: string | null; + audioInputName: string | null; +}; +export type RecordingOptionsChanged = null; +export type RecordingSegment = { start: number; end: number }; +export type RecordingStarted = null; +export type RecordingStopped = { path: string }; +export type RenderFrameEvent = { frame_number: number }; +export type RenderProgress = + | { type: "Starting"; total_frames: number } + | { type: "EstimatedTotalFrames"; total_frames: number } + | { type: "FrameRendered"; current_frame: number }; +export type RequestNewScreenshot = null; +export type RequestOpenSettings = { page: string }; +export type RequestRestartRecording = null; +export type RequestStartRecording = null; +export type RequestStopRecording = null; +export type S3UploadMeta = { + id: string; + user_id: string; + aws_region: string; + aws_bucket: string; +}; +export type ScreenCaptureTarget = + | ({ variant: "window" } & CaptureWindow) + | ({ variant: "screen" } & CaptureScreen); +export type SerializedEditorInstance = { + framesSocketUrl: string; + recordingDuration: number; + savedProjectConfig: ProjectConfiguration; + recordings: ProjectRecordings; + path: string; + prettyName: string; +}; +export type SharingMeta = { id: string; link: string }; +export type ShowCapturesPanel = null; +export type TimelineConfiguration = { + segments: TimelineSegment[]; + zoomSegments?: ZoomSegment[]; +}; +export type TimelineSegment = { timescale: number; start: number; end: number }; +export type UploadResult = + | { Success: string } + | "NotAuthenticated" + | "PlanCheckFailed" + | "UpgradeRequired"; +export type Video = { + duration: number; + width: number; + height: number; + fps: number; +}; +export type VideoType = "screen" | "output"; +export type XY = { x: T; y: T }; +export type ZoomSegment = { start: number; end: number; amount: number }; /** tauri-specta globals **/ import { - invoke as TAURI_INVOKE, - Channel as TAURI_CHANNEL, + invoke as TAURI_INVOKE, + Channel as TAURI_CHANNEL, } from "@tauri-apps/api/core"; import * as TAURI_API_EVENT from "@tauri-apps/api/event"; import { type WebviewWindow as __WebviewWindow__ } from "@tauri-apps/api/webviewWindow"; type __EventObj__ = { - listen: ( - cb: TAURI_API_EVENT.EventCallback, - ) => ReturnType>; - once: ( - cb: TAURI_API_EVENT.EventCallback, - ) => ReturnType>; - emit: null extends T - ? (payload?: T) => ReturnType - : (payload: T) => ReturnType; + listen: ( + cb: TAURI_API_EVENT.EventCallback + ) => ReturnType>; + once: ( + cb: TAURI_API_EVENT.EventCallback + ) => ReturnType>; + emit: null extends T + ? (payload?: T) => ReturnType + : (payload: T) => ReturnType; }; export type Result = - | { status: "ok"; data: T } - | { status: "error"; error: E }; + | { status: "ok"; data: T } + | { status: "error"; error: E }; function __makeEvents__>( - mappings: Record, + mappings: Record ) { - return new Proxy( - {} as unknown as { - [K in keyof T]: __EventObj__ & { - (handle: __WebviewWindow__): __EventObj__; - }; - }, - { - get: (_, event) => { - const name = mappings[event as keyof T]; + return new Proxy( + {} as unknown as { + [K in keyof T]: __EventObj__ & { + (handle: __WebviewWindow__): __EventObj__; + }; + }, + { + get: (_, event) => { + const name = mappings[event as keyof T]; - return new Proxy((() => {}) as any, { - apply: (_, __, [window]: [__WebviewWindow__]) => ({ - listen: (arg: any) => window.listen(name, arg), - once: (arg: any) => window.once(name, arg), - emit: (arg: any) => window.emit(name, arg), - }), - get: (_, command: keyof __EventObj__) => { - switch (command) { - case "listen": - return (arg: any) => TAURI_API_EVENT.listen(name, arg); - case "once": - return (arg: any) => TAURI_API_EVENT.once(name, arg); - case "emit": - return (arg: any) => TAURI_API_EVENT.emit(name, arg); - } - }, - }); - }, - }, - ); + return new Proxy((() => {}) as any, { + apply: (_, __, [window]: [__WebviewWindow__]) => ({ + listen: (arg: any) => window.listen(name, arg), + once: (arg: any) => window.once(name, arg), + emit: (arg: any) => window.emit(name, arg), + }), + get: (_, command: keyof __EventObj__) => { + switch (command) { + case "listen": + return (arg: any) => TAURI_API_EVENT.listen(name, arg); + case "once": + return (arg: any) => TAURI_API_EVENT.once(name, arg); + case "emit": + return (arg: any) => TAURI_API_EVENT.emit(name, arg); + } + }, + }); + }, + } + ); } diff --git a/apps/tasks/src/interfaces/ErrorResponse.ts b/apps/tasks/src/interfaces/ErrorResponse.ts index 170768a21..d01a26682 100644 --- a/apps/tasks/src/interfaces/ErrorResponse.ts +++ b/apps/tasks/src/interfaces/ErrorResponse.ts @@ -1,5 +1,5 @@ -import MessageResponse from './MessageResponse'; +import MessageResponse from "./MessageResponse"; export default interface ErrorResponse extends MessageResponse { stack?: string; -} \ No newline at end of file +} diff --git a/apps/tasks/src/middlewares.ts b/apps/tasks/src/middlewares.ts index 249ff67cc..a8c9aec18 100644 --- a/apps/tasks/src/middlewares.ts +++ b/apps/tasks/src/middlewares.ts @@ -1,6 +1,6 @@ -import { NextFunction, Request, Response } from 'express'; +import { NextFunction, Request, Response } from "express"; -import ErrorResponse from './interfaces/ErrorResponse'; +import ErrorResponse from "./interfaces/ErrorResponse"; export function notFound(req: Request, res: Response, next: NextFunction) { res.status(404); @@ -9,11 +9,16 @@ export function notFound(req: Request, res: Response, next: NextFunction) { } // eslint-disable-next-line @typescript-eslint/no-unused-vars -export function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) { +export function errorHandler( + err: Error, + req: Request, + res: Response, + next: NextFunction +) { const statusCode = res.statusCode !== 200 ? res.statusCode : 500; res.status(statusCode); res.json({ message: err.message, - stack: process.env.NODE_ENV === 'production' ? '🥞' : err.stack, + stack: process.env.NODE_ENV === "production" ? "🥞" : err.stack, }); } diff --git a/apps/web/app/api/caps/share/route.ts b/apps/web/app/api/caps/share/route.ts index 26a4c6c2d..e596bbf66 100644 --- a/apps/web/app/api/caps/share/route.ts +++ b/apps/web/app/api/caps/share/route.ts @@ -58,6 +58,9 @@ export async function POST(request: NextRequest) { return NextResponse.json({ success: true }); } catch (error) { console.error("Error updating shared spaces:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); } -} \ No newline at end of file +} diff --git a/apps/web/app/api/changelog/route.ts b/apps/web/app/api/changelog/route.ts index 7a256b604..568ba5175 100644 --- a/apps/web/app/api/changelog/route.ts +++ b/apps/web/app/api/changelog/route.ts @@ -1,38 +1,38 @@ -import { NextResponse } from 'next/server'; +import { NextResponse } from "next/server"; import { getChangelogPosts } from "../../../utils/changelog"; -export const dynamic = 'force-dynamic'; +export const dynamic = "force-dynamic"; export const revalidate = 0; export async function GET() { const allUpdates = getChangelogPosts(); const changelogs = allUpdates - .map(post => ({ + .map((post) => ({ metadata: post.metadata, content: post.content, - slug: parseInt(post.slug) + slug: parseInt(post.slug), })) .sort((a, b) => b.slug - a.slug) .map(({ metadata, content }) => ({ ...metadata, content })); const response = NextResponse.json(changelogs); - + // Set CORS headers - response.headers.set('Access-Control-Allow-Origin', '*'); - response.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS'); - response.headers.set('Access-Control-Allow-Headers', 'Content-Type'); + response.headers.set("Access-Control-Allow-Origin", "*"); + response.headers.set("Access-Control-Allow-Methods", "GET, OPTIONS"); + response.headers.set("Access-Control-Allow-Headers", "Content-Type"); return response; } export async function OPTIONS() { const response = new NextResponse(null, { status: 204 }); - + // Set CORS headers for preflight requests - response.headers.set('Access-Control-Allow-Origin', '*'); - response.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS'); - response.headers.set('Access-Control-Allow-Headers', 'Content-Type'); - + response.headers.set("Access-Control-Allow-Origin", "*"); + response.headers.set("Access-Control-Allow-Methods", "GET, OPTIONS"); + response.headers.set("Access-Control-Allow-Headers", "Content-Type"); + return response; -} \ No newline at end of file +} diff --git a/apps/web/app/api/changelog/status/route.ts b/apps/web/app/api/changelog/status/route.ts index 353424edf..d794e16ee 100644 --- a/apps/web/app/api/changelog/status/route.ts +++ b/apps/web/app/api/changelog/status/route.ts @@ -1,20 +1,20 @@ -import { NextResponse } from 'next/server'; +import { NextResponse } from "next/server"; import { getChangelogPosts } from "../../../../utils/changelog"; -export const dynamic = 'force-dynamic'; +export const dynamic = "force-dynamic"; export const revalidate = 0; export async function GET(request: Request) { const { searchParams } = new URL(request.url); - const version = searchParams.get('version'); + const version = searchParams.get("version"); const allUpdates = getChangelogPosts(); const changelogs = allUpdates - .map(post => ({ + .map((post) => ({ metadata: post.metadata, content: post.content, - slug: parseInt(post.slug) + slug: parseInt(post.slug), })) .sort((a, b) => b.slug - a.slug) .map(({ metadata, content }) => ({ ...metadata, content })); @@ -23,22 +23,22 @@ export async function GET(request: Request) { const hasUpdate = version ? latestVersion === version : false; const response = NextResponse.json({ hasUpdate }); - + // Set CORS headers - response.headers.set('Access-Control-Allow-Origin', '*'); - response.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS'); - response.headers.set('Access-Control-Allow-Headers', 'Content-Type'); + response.headers.set("Access-Control-Allow-Origin", "*"); + response.headers.set("Access-Control-Allow-Methods", "GET, OPTIONS"); + response.headers.set("Access-Control-Allow-Headers", "Content-Type"); return response; } export async function OPTIONS() { const response = new NextResponse(null, { status: 204 }); - + // Set CORS headers for preflight requests - response.headers.set('Access-Control-Allow-Origin', '*'); - response.headers.set('Access-Control-Allow-Methods', 'GET, OPTIONS'); - response.headers.set('Access-Control-Allow-Headers', 'Content-Type'); - + response.headers.set("Access-Control-Allow-Origin", "*"); + response.headers.set("Access-Control-Allow-Methods", "GET, OPTIONS"); + response.headers.set("Access-Control-Allow-Headers", "Content-Type"); + return response; -} \ No newline at end of file +} diff --git a/apps/web/app/api/desktop/feedback/route.ts b/apps/web/app/api/desktop/feedback/route.ts index a9d339581..b10c8d983 100644 --- a/apps/web/app/api/desktop/feedback/route.ts +++ b/apps/web/app/api/desktop/feedback/route.ts @@ -27,13 +27,13 @@ export async function OPTIONS(req: NextRequest) { : "null", "Access-Control-Allow-Credentials": "true", "Access-Control-Allow-Methods": "POST, OPTIONS", - "Access-Control-Allow-Headers": "Content-Type, Authorization, sentry-trace, baggage", + "Access-Control-Allow-Headers": + "Content-Type, Authorization, sentry-trace, baggage", }, }); } export async function POST(req: NextRequest) { - const token = req.headers.get("authorization")?.split(" ")[1]; if (token) { cookies().set({ @@ -51,7 +51,6 @@ export async function POST(req: NextRequest) { const origin = params.get("origin") || null; const originalOrigin = req.nextUrl.origin; - if (!user) { return new Response(JSON.stringify({ error: "User not authenticated" }), { status: 401, @@ -71,21 +70,23 @@ export async function POST(req: NextRequest) { const formData = await req.formData(); const feedbackText = formData.get("feedback") as string; - if (!feedbackText) { - return new Response(JSON.stringify({ error: "Feedback text is required" }), { - status: 400, - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": - origin && allowedOrigins.includes(origin) - ? origin - : allowedOrigins.includes(originalOrigin) - ? originalOrigin - : "null", - "Access-Control-Allow-Credentials": "true", - }, - }); + return new Response( + JSON.stringify({ error: "Feedback text is required" }), + { + status: 400, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": + origin && allowedOrigins.includes(origin) + ? origin + : allowedOrigins.includes(originalOrigin) + ? originalOrigin + : "null", + "Access-Control-Allow-Credentials": "true", + }, + } + ); } try { @@ -96,9 +97,9 @@ export async function POST(req: NextRequest) { } const response = await fetch(discordWebhookUrl, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ content: `New feedback from ${user.email}:\n${feedbackText}`, @@ -106,35 +107,46 @@ export async function POST(req: NextRequest) { }); if (!response.ok) { - throw new Error(`Failed to send feedback to Discord: ${response.statusText}`); + throw new Error( + `Failed to send feedback to Discord: ${response.statusText}` + ); } - return new Response(JSON.stringify({ success: true, message: "Feedback submitted successfully" }), { - status: 200, - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": - origin && allowedOrigins.includes(origin) - ? origin - : allowedOrigins.includes(originalOrigin) - ? originalOrigin - : "null", - "Access-Control-Allow-Credentials": "true", - }, - }); + return new Response( + JSON.stringify({ + success: true, + message: "Feedback submitted successfully", + }), + { + status: 200, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": + origin && allowedOrigins.includes(origin) + ? origin + : allowedOrigins.includes(originalOrigin) + ? originalOrigin + : "null", + "Access-Control-Allow-Credentials": "true", + }, + } + ); } catch (error) { - return new Response(JSON.stringify({ error: "Failed to submit feedback" }), { - status: 500, - headers: { - "Content-Type": "application/json", - "Access-Control-Allow-Origin": - origin && allowedOrigins.includes(origin) - ? origin - : allowedOrigins.includes(originalOrigin) - ? originalOrigin - : "null", - "Access-Control-Allow-Credentials": "true", - }, - }); + return new Response( + JSON.stringify({ error: "Failed to submit feedback" }), + { + status: 500, + headers: { + "Content-Type": "application/json", + "Access-Control-Allow-Origin": + origin && allowedOrigins.includes(origin) + ? origin + : allowedOrigins.includes(originalOrigin) + ? originalOrigin + : "null", + "Access-Control-Allow-Credentials": "true", + }, + } + ); } -} \ No newline at end of file +} diff --git a/apps/web/app/api/invite/accept/route.ts b/apps/web/app/api/invite/accept/route.ts index c2087ef4c..404f32dce 100644 --- a/apps/web/app/api/invite/accept/route.ts +++ b/apps/web/app/api/invite/accept/route.ts @@ -3,66 +3,72 @@ import { db } from "@cap/database"; import { getCurrentUser } from "@cap/database/auth/session"; import { spaceInvites, spaceMembers, users } from "@cap/database/schema"; import { eq } from "drizzle-orm"; -import { nanoId } from "@cap/database/helpers" +import { nanoId } from "@cap/database/helpers"; export async function POST(request: NextRequest) { - const user = await getCurrentUser(); - if (!user) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const { inviteId } = await request.json(); + + try { + // Find the invite + const [invite] = await db + .select() + .from(spaceInvites) + .where(eq(spaceInvites.id, inviteId)); + + if (!invite) { + return NextResponse.json({ error: "Invite not found" }, { status: 404 }); } - - const { inviteId } = await request.json(); - - try { - // Find the invite - const [invite] = await db - .select() - .from(spaceInvites) - .where(eq(spaceInvites.id, inviteId)); - - if (!invite) { - return NextResponse.json({ error: "Invite not found" }, { status: 404 }); - } - - // Check if the user's email matches the invited email - if (user.email !== invite.invitedEmail) { - return NextResponse.json({ error: "Email mismatch" }, { status: 403 }); - } - - // Get the space owner's subscription ID - const [spaceOwner] = await db - .select({ - stripeSubscriptionId: users.stripeSubscriptionId, - }) - .from(users) - .where(eq(users.id, invite.invitedByUserId)); - - if (!spaceOwner || !spaceOwner.stripeSubscriptionId) { - return NextResponse.json({ error: "Space owner not found or has no subscription" }, { status: 404 }); - } - - // Create a new space member - await db.insert(spaceMembers).values({ - id: nanoId(), - spaceId: invite.spaceId, - userId: user.id, - role: invite.role, - }); - - // Update the user's thirdPartyStripeSubscriptionId - await db - .update(users) - .set({ - thirdPartyStripeSubscriptionId: spaceOwner.stripeSubscriptionId, - }) - .where(eq(users.id, user.id)); - - // Delete the invite - await db.delete(spaceInvites).where(eq(spaceInvites.id, inviteId)); - - return NextResponse.json({ success: true }); - } catch (error) { - console.error("Error accepting invite:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + + // Check if the user's email matches the invited email + if (user.email !== invite.invitedEmail) { + return NextResponse.json({ error: "Email mismatch" }, { status: 403 }); } - } \ No newline at end of file + + // Get the space owner's subscription ID + const [spaceOwner] = await db + .select({ + stripeSubscriptionId: users.stripeSubscriptionId, + }) + .from(users) + .where(eq(users.id, invite.invitedByUserId)); + + if (!spaceOwner || !spaceOwner.stripeSubscriptionId) { + return NextResponse.json( + { error: "Space owner not found or has no subscription" }, + { status: 404 } + ); + } + + // Create a new space member + await db.insert(spaceMembers).values({ + id: nanoId(), + spaceId: invite.spaceId, + userId: user.id, + role: invite.role, + }); + + // Update the user's thirdPartyStripeSubscriptionId + await db + .update(users) + .set({ + thirdPartyStripeSubscriptionId: spaceOwner.stripeSubscriptionId, + }) + .where(eq(users.id, user.id)); + + // Delete the invite + await db.delete(spaceInvites).where(eq(spaceInvites.id, inviteId)); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Error accepting invite:", error); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); + } +} diff --git a/apps/web/app/api/invite/decline/route.ts b/apps/web/app/api/invite/decline/route.ts index b4a392479..b0b995b64 100644 --- a/apps/web/app/api/invite/decline/route.ts +++ b/apps/web/app/api/invite/decline/route.ts @@ -13,6 +13,9 @@ export async function POST(request: NextRequest) { return NextResponse.json({ success: true }); } catch (error) { console.error("Error declining invite:", error); - return NextResponse.json({ error: "Internal server error" }, { status: 500 }); + return NextResponse.json( + { error: "Internal server error" }, + { status: 500 } + ); } -} \ No newline at end of file +} diff --git a/apps/web/app/api/settings/workspace/invite/remove/route.ts b/apps/web/app/api/settings/workspace/invite/remove/route.ts index 9dd2ff79f..f30048575 100644 --- a/apps/web/app/api/settings/workspace/invite/remove/route.ts +++ b/apps/web/app/api/settings/workspace/invite/remove/route.ts @@ -22,7 +22,11 @@ export async function POST(request: NextRequest) { console.log(`User found: ${user.id}`); - const space = await db.select().from(spaces).where(eq(spaces.id, spaceId)).limit(1); + const space = await db + .select() + .from(spaces) + .where(eq(spaces.id, spaceId)) + .limit(1); console.log(`Space query result:`, space); if (!space || space.length === 0) { @@ -45,12 +49,10 @@ export async function POST(request: NextRequest) { }); } - const result = await db.delete(spaceInvites) + const result = await db + .delete(spaceInvites) .where( - and( - eq(spaceInvites.id, inviteId), - eq(spaceInvites.spaceId, spaceId) - ) + and(eq(spaceInvites.id, inviteId), eq(spaceInvites.spaceId, spaceId)) ); if (result.rowsAffected === 0) { @@ -64,13 +66,10 @@ export async function POST(request: NextRequest) { } console.log("Workspace invite removed successfully"); - return new Response( - JSON.stringify({ success: true }), - { - status: 200, - headers: { - "Content-Type": "application/json", - }, - } - ); + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { + "Content-Type": "application/json", + }, + }); } diff --git a/apps/web/app/api/settings/workspace/invite/send/route.ts b/apps/web/app/api/settings/workspace/invite/send/route.ts index 4131bd4c4..5d438a58f 100644 --- a/apps/web/app/api/settings/workspace/invite/send/route.ts +++ b/apps/web/app/api/settings/workspace/invite/send/route.ts @@ -49,7 +49,9 @@ export async function POST(request: NextRequest) { } const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - const validEmails = invitedEmails.filter((email: string) => emailRegex.test(email.trim())); + const validEmails = invitedEmails.filter((email: string) => + emailRegex.test(email.trim()) + ); for (const email of validEmails) { const inviteId = nanoId(); diff --git a/apps/web/app/api/video/individual/route.ts b/apps/web/app/api/video/individual/route.ts index 363b8ed48..b5d76ac08 100644 --- a/apps/web/app/api/video/individual/route.ts +++ b/apps/web/app/api/video/individual/route.ts @@ -82,7 +82,7 @@ export async function GET(request: NextRequest) { const individualFiles = objects.Contents.map((object) => { const key = object.Key as string; - const fileName = key.split('/').pop(); + const fileName = key.split("/").pop(); return { fileName, url: `https://v.cap.so/${key}`, @@ -103,4 +103,4 @@ export async function GET(request: NextRequest) { { status: 500, headers: getHeaders(origin) } ); } -} \ No newline at end of file +} diff --git a/apps/web/app/api/video/transcribe/route.ts b/apps/web/app/api/video/transcribe/route.ts index f78f12602..8fbb9e30f 100644 --- a/apps/web/app/api/video/transcribe/route.ts +++ b/apps/web/app/api/video/transcribe/route.ts @@ -122,7 +122,10 @@ export async function GET(request: NextRequest) { if (!awsRegion || !awsBucket) { console.error("AWS region or bucket information is missing"); return new Response( - JSON.stringify({ error: true, message: "AWS region or bucket information is missing" }), + JSON.stringify({ + error: true, + message: "AWS region or bucket information is missing", + }), { status: 500, headers: getHeaders(origin), diff --git a/apps/web/app/api/webhooks/stripe/route.ts b/apps/web/app/api/webhooks/stripe/route.ts index 12f372781..3b497c4ff 100644 --- a/apps/web/app/api/webhooks/stripe/route.ts +++ b/apps/web/app/api/webhooks/stripe/route.ts @@ -45,7 +45,7 @@ export const POST = async (req: Request) => { .from(users) .where(eq(users.email, customer.email)) .limit(1); - + if (userByEmail && userByEmail.length > 0 && userByEmail[0]) { foundUserId = userByEmail[0].id; console.log(`User found by email: ${foundUserId}`); @@ -73,14 +73,21 @@ export const POST = async (req: Request) => { .where(eq(users.id, foundUserId)); if (!user) { - console.log("No user found in database for checkout.session.completed event"); + console.log( + "No user found in database for checkout.session.completed event" + ); return new Response("No user found", { status: 400, }); } - const subscription = await stripe.subscriptions.retrieve(event.data.object.subscription as string); - const inviteQuota = subscription.items.data.reduce((total, item) => total + (item.quantity || 1), 0); + const subscription = await stripe.subscriptions.retrieve( + event.data.object.subscription as string + ); + const inviteQuota = subscription.items.data.reduce( + (total, item) => total + (item.quantity || 1), + 0 + ); await db .update(users) @@ -90,7 +97,9 @@ export const POST = async (req: Request) => { inviteQuota: inviteQuota, }) .where(eq(users.id, foundUserId)); - console.log("User updated successfully for checkout.session.completed event"); + console.log( + "User updated successfully for checkout.session.completed event" + ); } if (event.type === "customer.subscription.updated") { @@ -110,7 +119,7 @@ export const POST = async (req: Request) => { .from(users) .where(eq(users.email, customer.email)) .limit(1); - + if (userByEmail && userByEmail.length > 0 && userByEmail[0]) { foundUserId = userByEmail[0].id; console.log(`User found by email: ${foundUserId}`); @@ -138,14 +147,19 @@ export const POST = async (req: Request) => { .where(eq(users.id, foundUserId)); if (!user) { - console.log("No user found in database for customer.subscription.updated event"); + console.log( + "No user found in database for customer.subscription.updated event" + ); return new Response("No user found", { status: 400, }); } const subscription = event.data.object as Stripe.Subscription; - const inviteQuota = subscription.items.data.reduce((total, item) => total + (item.quantity || 1), 0); + const inviteQuota = subscription.items.data.reduce( + (total, item) => total + (item.quantity || 1), + 0 + ); await db .update(users) @@ -155,7 +169,9 @@ export const POST = async (req: Request) => { inviteQuota: inviteQuota, }) .where(eq(users.id, foundUserId)); - console.log("User updated successfully for customer.subscription.updated event"); + console.log( + "User updated successfully for customer.subscription.updated event" + ); } if (event.type === "customer.subscription.deleted") { @@ -175,7 +191,7 @@ export const POST = async (req: Request) => { .from(users) .where(eq(users.email, customer.email)) .limit(1); - + if (userByEmail && userByEmail.length > 0 && userByEmail[0]) { foundUserId = userByEmail[0].id; console.log(`User found by email: ${foundUserId}`); @@ -203,7 +219,9 @@ export const POST = async (req: Request) => { .where(eq(users.id, foundUserId)); if (!user) { - console.log("No user found in database for customer.subscription.deleted event"); + console.log( + "No user found in database for customer.subscription.deleted event" + ); return new Response("No user found", { status: 400, }); @@ -217,10 +235,12 @@ export const POST = async (req: Request) => { inviteQuota: 1, // Reset to default quota when subscription is deleted }) .where(eq(users.id, foundUserId)); - console.log("User updated successfully for customer.subscription.deleted event"); + console.log( + "User updated successfully for customer.subscription.deleted event" + ); } } catch (error) { - console.log('❌ Webhook handler failed. View logs.'); + console.log("❌ Webhook handler failed. View logs."); return new Response( 'Webhook error: "Webhook handler failed. View logs."', { diff --git a/apps/web/app/sitemap.ts b/apps/web/app/sitemap.ts index da4ac2510..e6ef90850 100644 --- a/apps/web/app/sitemap.ts +++ b/apps/web/app/sitemap.ts @@ -1,59 +1,71 @@ -import { promises as fs } from 'fs'; -import path from 'path'; +import { promises as fs } from "fs"; +import path from "path"; import { getBlogPosts } from "@/utils/updates"; -async function getPagePaths(dir: string): Promise<{ path: string; lastModified: string }[]> { - const entries = await fs.readdir(dir, { withFileTypes: true }); - const paths: { path: string; lastModified: string }[] = []; - - for (const entry of entries) { - const fullPath = path.join(dir, entry.name); - if (entry.isDirectory() && entry.name !== 'dashboard' && !entry.name.startsWith('s') && entry.name !== 'updates') { - const subPaths = await getPagePaths(fullPath); - paths.push(...subPaths); - } else if (entry.isFile() && (entry.name === 'page.tsx' || entry.name === 'page.mdx')) { - const relativePath = path.relative(process.cwd(), dir); - const routePath = '/' + relativePath.split(path.sep).slice(1).join('/'); - if (!routePath.includes('/dashboard') && !routePath.split('/').some(segment => segment.startsWith('s'))) { - const stats = await fs.stat(fullPath); - paths.push({ - path: routePath === '/app' ? '/' : routePath, - lastModified: stats.mtime.toISOString() - }); - } +async function getPagePaths( + dir: string +): Promise<{ path: string; lastModified: string }[]> { + const entries = await fs.readdir(dir, { withFileTypes: true }); + const paths: { path: string; lastModified: string }[] = []; + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if ( + entry.isDirectory() && + entry.name !== "dashboard" && + !entry.name.startsWith("s") && + entry.name !== "updates" + ) { + const subPaths = await getPagePaths(fullPath); + paths.push(...subPaths); + } else if ( + entry.isFile() && + (entry.name === "page.tsx" || entry.name === "page.mdx") + ) { + const relativePath = path.relative(process.cwd(), dir); + const routePath = "/" + relativePath.split(path.sep).slice(1).join("/"); + if ( + !routePath.includes("/dashboard") && + !routePath.split("/").some((segment) => segment.startsWith("s")) + ) { + const stats = await fs.stat(fullPath); + paths.push({ + path: routePath === "/app" ? "/" : routePath, + lastModified: stats.mtime.toISOString(), + }); } } - - return paths; } - - export default async function sitemap() { - const appDirectory = path.join(process.cwd(), 'app'); - const pagePaths = await getPagePaths(appDirectory); - - // Add blog post routes - const blogPosts = getBlogPosts(); - const blogRoutes = blogPosts.map(post => { - const publishDate = new Date(post.metadata.publishedAt); - publishDate.setHours(9, 0, 0, 0); // Set time to 9:00 AM - return { - path: `/updates/${post.slug}`, - lastModified: publishDate.toISOString() - }; - }); - - // Combine routes and ensure '/' is first - const allRoutes = [...pagePaths, ...blogRoutes]; - const homeRoute = allRoutes.find(route => route.path === '/'); - const otherRoutes = allRoutes.filter(route => route.path !== '/'); - - const routes = [ - ...(homeRoute ? [homeRoute] : []), - ...otherRoutes - ].map((route) => ({ + + return paths; +} + +export default async function sitemap() { + const appDirectory = path.join(process.cwd(), "app"); + const pagePaths = await getPagePaths(appDirectory); + + // Add blog post routes + const blogPosts = getBlogPosts(); + const blogRoutes = blogPosts.map((post) => { + const publishDate = new Date(post.metadata.publishedAt); + publishDate.setHours(9, 0, 0, 0); // Set time to 9:00 AM + return { + path: `/updates/${post.slug}`, + lastModified: publishDate.toISOString(), + }; + }); + + // Combine routes and ensure '/' is first + const allRoutes = [...pagePaths, ...blogRoutes]; + const homeRoute = allRoutes.find((route) => route.path === "/"); + const otherRoutes = allRoutes.filter((route) => route.path !== "/"); + + const routes = [...(homeRoute ? [homeRoute] : []), ...otherRoutes].map( + (route) => ({ url: `https://cap.so${route.path}`, lastModified: route.lastModified, - })); - - return routes; - } \ No newline at end of file + }) + ); + + return routes; +} diff --git a/packages/database/schema.ts b/packages/database/schema.ts index 9e89df903..20da9568e 100644 --- a/packages/database/schema.ts +++ b/packages/database/schema.ts @@ -163,7 +163,9 @@ export const spaceInvites = mysqlTable( (table) => ({ spaceIdIndex: index("space_id_idx").on(table.spaceId), invitedEmailIndex: index("invited_email_idx").on(table.invitedEmail), - invitedByUserIdIndex: index("invited_by_user_id_idx").on(table.invitedByUserId), + invitedByUserIdIndex: index("invited_by_user_id_idx").on( + table.invitedByUserId + ), statusIndex: index("status_idx").on(table.status), }) ); diff --git a/packages/ui-solid/src/auto-imports.d.ts b/packages/ui-solid/src/auto-imports.d.ts index 9d90f041c..b85717c51 100644 --- a/packages/ui-solid/src/auto-imports.d.ts +++ b/packages/ui-solid/src/auto-imports.d.ts @@ -6,58 +6,58 @@ // biome-ignore lint: disable export {} declare global { - const IconCapArrows: typeof import('~icons/cap/arrows.jsx')['default'] - const IconCapAudioOn: typeof import('~icons/cap/audio-on.jsx')['default'] - const IconCapBell: typeof import('~icons/cap/bell.jsx')['default'] - const IconCapBlur: typeof import('~icons/cap/blur.jsx')['default'] - const IconCapCamera: typeof import('~icons/cap/camera.jsx')['default'] - const IconCapChevronDown: typeof import('~icons/cap/chevron-down.jsx')['default'] - const IconCapCircle: typeof import('~icons/cap/circle.jsx')['default'] - const IconCapCircleCheck: typeof import('~icons/cap/circle-check.jsx')['default'] - const IconCapCirclePlus: typeof import('~icons/cap/circle-plus.jsx')['default'] - const IconCapCircleX: typeof import('~icons/cap/circle-x.jsx')['default'] - const IconCapCopy: typeof import('~icons/cap/copy.jsx')['default'] - const IconCapCorners: typeof import('~icons/cap/corners.jsx')['default'] - const IconCapCrop: typeof import('~icons/cap/crop.jsx')['default'] - const IconCapCursor: typeof import('~icons/cap/cursor.jsx')['default'] - const IconCapEditor: typeof import('~icons/cap/editor.jsx')['default'] - const IconCapEnlarge: typeof import('~icons/cap/enlarge.jsx')['default'] - const IconCapEye: typeof import('~icons/cap/eye.jsx')['default'] - const IconCapFrameFirst: typeof import('~icons/cap/frame-first.jsx')['default'] - const IconCapFrameLast: typeof import('~icons/cap/frame-last.jsx')['default'] - const IconCapHotkeys: typeof import('~icons/cap/hotkeys.jsx')['default'] - const IconCapImage: typeof import('~icons/cap/image.jsx')['default'] - const IconCapInset: typeof import('~icons/cap/inset.jsx')['default'] - const IconCapLayout: typeof import('~icons/cap/layout.jsx')['default'] - const IconCapLogo: typeof import('~icons/cap/logo.jsx')['default'] - const IconCapLogoFull: typeof import('~icons/cap/logo-full.jsx')['default'] - const IconCapMessageBubble: typeof import('~icons/cap/message-bubble.jsx')['default'] - const IconCapMicrophone: typeof import('~icons/cap/microphone.jsx')['default'] - const IconCapMoreVertical: typeof import('~icons/cap/more-vertical.jsx')['default'] - const IconCapPadding: typeof import('~icons/cap/padding.jsx')['default'] - const IconCapPauseCircle: typeof import('~icons/cap/pause-circle.jsx')['default'] - const IconCapPlayCircle: typeof import('~icons/cap/play-circle.jsx')['default'] - const IconCapPresets: typeof import('~icons/cap/presets.jsx')['default'] - const IconCapRedo: typeof import('~icons/cap/redo.jsx')['default'] - const IconCapRestart: typeof import('~icons/cap/restart.jsx')['default'] - const IconCapScissors: typeof import('~icons/cap/scissors.jsx')['default'] - const IconCapSettings: typeof import('~icons/cap/settings.jsx')['default'] - const IconCapShadow: typeof import('~icons/cap/shadow.jsx')['default'] - const IconCapSquare: typeof import('~icons/cap/square.jsx')['default'] - const IconCapStopCircle: typeof import('~icons/cap/stop-circle.jsx')['default'] - const IconCapTrash: typeof import('~icons/cap/trash.jsx')['default'] - const IconCapUndo: typeof import('~icons/cap/undo.jsx')['default'] - const IconCapUpload: typeof import('~icons/cap/upload.jsx')['default'] - const IconLucideBell: typeof import('~icons/lucide/bell.jsx')['default'] - const IconLucideCamera: typeof import('~icons/lucide/camera.jsx')['default'] - const IconLucideCheck: typeof import('~icons/lucide/check.jsx')['default'] - const IconLucideEdit: typeof import('~icons/lucide/edit.jsx')['default'] - const IconLucideEye: typeof import('~icons/lucide/eye.jsx')['default'] - const IconLucideFolder: typeof import('~icons/lucide/folder.jsx')['default'] - const IconLucideFoler: typeof import('~icons/lucide/foler.jsx')['default'] - const IconLucideLoaderCircle: typeof import('~icons/lucide/loader-circle.jsx')['default'] - const IconLucideRewind: typeof import('~icons/lucide/rewind.jsx')['default'] - const IconLucideSettings: typeof import('~icons/lucide/settings.jsx')['default'] - const IconLucideSmile: typeof import('~icons/lucide/smile.jsx')['default'] - const IconLucideVideo: typeof import('~icons/lucide/video.jsx')['default'] + const IconCapArrows: typeof import("~icons/cap/arrows.jsx")["default"]; + const IconCapAudioOn: typeof import("~icons/cap/audio-on.jsx")["default"]; + const IconCapBell: typeof import("~icons/cap/bell.jsx")["default"]; + const IconCapBlur: typeof import("~icons/cap/blur.jsx")["default"]; + const IconCapCamera: typeof import("~icons/cap/camera.jsx")["default"]; + const IconCapChevronDown: typeof import("~icons/cap/chevron-down.jsx")["default"]; + const IconCapCircle: typeof import("~icons/cap/circle.jsx")["default"]; + const IconCapCircleCheck: typeof import("~icons/cap/circle-check.jsx")["default"]; + const IconCapCirclePlus: typeof import("~icons/cap/circle-plus.jsx")["default"]; + const IconCapCircleX: typeof import("~icons/cap/circle-x.jsx")["default"]; + const IconCapCopy: typeof import("~icons/cap/copy.jsx")["default"]; + const IconCapCorners: typeof import("~icons/cap/corners.jsx")["default"]; + const IconCapCrop: typeof import("~icons/cap/crop.jsx")["default"]; + const IconCapCursor: typeof import("~icons/cap/cursor.jsx")["default"]; + const IconCapEditor: typeof import("~icons/cap/editor.jsx")["default"]; + const IconCapEnlarge: typeof import("~icons/cap/enlarge.jsx")["default"]; + const IconCapEye: typeof import("~icons/cap/eye.jsx")["default"]; + const IconCapFrameFirst: typeof import("~icons/cap/frame-first.jsx")["default"]; + const IconCapFrameLast: typeof import("~icons/cap/frame-last.jsx")["default"]; + const IconCapHotkeys: typeof import("~icons/cap/hotkeys.jsx")["default"]; + const IconCapImage: typeof import("~icons/cap/image.jsx")["default"]; + const IconCapInset: typeof import("~icons/cap/inset.jsx")["default"]; + const IconCapLayout: typeof import("~icons/cap/layout.jsx")["default"]; + const IconCapLogo: typeof import("~icons/cap/logo.jsx")["default"]; + const IconCapLogoFull: typeof import("~icons/cap/logo-full.jsx")["default"]; + const IconCapMessageBubble: typeof import("~icons/cap/message-bubble.jsx")["default"]; + const IconCapMicrophone: typeof import("~icons/cap/microphone.jsx")["default"]; + const IconCapMoreVertical: typeof import("~icons/cap/more-vertical.jsx")["default"]; + const IconCapPadding: typeof import("~icons/cap/padding.jsx")["default"]; + const IconCapPauseCircle: typeof import("~icons/cap/pause-circle.jsx")["default"]; + const IconCapPlayCircle: typeof import("~icons/cap/play-circle.jsx")["default"]; + const IconCapPresets: typeof import("~icons/cap/presets.jsx")["default"]; + const IconCapRedo: typeof import("~icons/cap/redo.jsx")["default"]; + const IconCapRestart: typeof import("~icons/cap/restart.jsx")["default"]; + const IconCapScissors: typeof import("~icons/cap/scissors.jsx")["default"]; + const IconCapSettings: typeof import("~icons/cap/settings.jsx")["default"]; + const IconCapShadow: typeof import("~icons/cap/shadow.jsx")["default"]; + const IconCapSquare: typeof import("~icons/cap/square.jsx")["default"]; + const IconCapStopCircle: typeof import("~icons/cap/stop-circle.jsx")["default"]; + const IconCapTrash: typeof import("~icons/cap/trash.jsx")["default"]; + const IconCapUndo: typeof import("~icons/cap/undo.jsx")["default"]; + const IconCapUpload: typeof import("~icons/cap/upload.jsx")["default"]; + const IconLucideBell: typeof import("~icons/lucide/bell.jsx")["default"]; + const IconLucideCamera: typeof import("~icons/lucide/camera.jsx")["default"]; + const IconLucideCheck: typeof import("~icons/lucide/check.jsx")["default"]; + const IconLucideEdit: typeof import("~icons/lucide/edit.jsx")["default"]; + const IconLucideEye: typeof import("~icons/lucide/eye.jsx")["default"]; + const IconLucideFolder: typeof import("~icons/lucide/folder.jsx")["default"]; + const IconLucideFoler: typeof import("~icons/lucide/foler.jsx")["default"]; + const IconLucideLoaderCircle: typeof import("~icons/lucide/loader-circle.jsx")["default"]; + const IconLucideRewind: typeof import("~icons/lucide/rewind.jsx")["default"]; + const IconLucideSettings: typeof import("~icons/lucide/settings.jsx")["default"]; + const IconLucideSmile: typeof import("~icons/lucide/smile.jsx")["default"]; + const IconLucideVideo: typeof import("~icons/lucide/video.jsx")["default"]; } diff --git a/packages/utils/src/types/database.ts b/packages/utils/src/types/database.ts index 5a68e8cae..4e07a9d3d 100644 --- a/packages/utils/src/types/database.ts +++ b/packages/utils/src/types/database.ts @@ -4,299 +4,299 @@ export type Json = | boolean | null | { [key: string]: Json | undefined } - | Json[] + | Json[]; export interface Database { public: { Tables: { shared_videos: { Row: { - shared_at: string - shared_by_user_id: string | null - space_id: string - video_id: string - } + shared_at: string; + shared_by_user_id: string | null; + space_id: string; + video_id: string; + }; Insert: { - shared_at?: string - shared_by_user_id?: string | null - space_id: string - video_id: string - } + shared_at?: string; + shared_by_user_id?: string | null; + space_id: string; + video_id: string; + }; Update: { - shared_at?: string - shared_by_user_id?: string | null - space_id?: string - video_id?: string - } + shared_at?: string; + shared_by_user_id?: string | null; + space_id?: string; + video_id?: string; + }; Relationships: [ { - foreignKeyName: "shared_videos_shared_by_user_id_fkey" - columns: ["shared_by_user_id"] - isOneToOne: false - referencedRelation: "users" - referencedColumns: ["id"] + foreignKeyName: "shared_videos_shared_by_user_id_fkey"; + columns: ["shared_by_user_id"]; + isOneToOne: false; + referencedRelation: "users"; + referencedColumns: ["id"]; }, { - foreignKeyName: "shared_videos_space_id_fkey" - columns: ["space_id"] - isOneToOne: false - referencedRelation: "spaces" - referencedColumns: ["id"] + foreignKeyName: "shared_videos_space_id_fkey"; + columns: ["space_id"]; + isOneToOne: false; + referencedRelation: "spaces"; + referencedColumns: ["id"]; }, { - foreignKeyName: "shared_videos_video_id_fkey" - columns: ["video_id"] - isOneToOne: false - referencedRelation: "videos" - referencedColumns: ["id"] + foreignKeyName: "shared_videos_video_id_fkey"; + columns: ["video_id"]; + isOneToOne: false; + referencedRelation: "videos"; + referencedColumns: ["id"]; } - ] - } + ]; + }; space_members: { Row: { - role: Database["public"]["Enums"]["user_role"] - space_id: string - user_id: string - } + role: Database["public"]["Enums"]["user_role"]; + space_id: string; + user_id: string; + }; Insert: { - role: Database["public"]["Enums"]["user_role"] - space_id: string - user_id: string - } + role: Database["public"]["Enums"]["user_role"]; + space_id: string; + user_id: string; + }; Update: { - role?: Database["public"]["Enums"]["user_role"] - space_id?: string - user_id?: string - } + role?: Database["public"]["Enums"]["user_role"]; + space_id?: string; + user_id?: string; + }; Relationships: [ { - foreignKeyName: "space_members_space_id_fkey" - columns: ["space_id"] - isOneToOne: false - referencedRelation: "spaces" - referencedColumns: ["id"] + foreignKeyName: "space_members_space_id_fkey"; + columns: ["space_id"]; + isOneToOne: false; + referencedRelation: "spaces"; + referencedColumns: ["id"]; }, { - foreignKeyName: "space_members_user_id_fkey" - columns: ["user_id"] - isOneToOne: false - referencedRelation: "users" - referencedColumns: ["id"] + foreignKeyName: "space_members_user_id_fkey"; + columns: ["user_id"]; + isOneToOne: false; + referencedRelation: "users"; + referencedColumns: ["id"]; } - ] - } + ]; + }; spaces: { Row: { - created_at: string - id: string - metadata: Json | null - name: string - owner_id: string | null - updated_at: string - } + created_at: string; + id: string; + metadata: Json | null; + name: string; + owner_id: string | null; + updated_at: string; + }; Insert: { - created_at?: string - id?: string - metadata?: Json | null - name: string - owner_id?: string | null - updated_at?: string - } + created_at?: string; + id?: string; + metadata?: Json | null; + name: string; + owner_id?: string | null; + updated_at?: string; + }; Update: { - created_at?: string - id?: string - metadata?: Json | null - name?: string - owner_id?: string | null - updated_at?: string - } + created_at?: string; + id?: string; + metadata?: Json | null; + name?: string; + owner_id?: string | null; + updated_at?: string; + }; Relationships: [ { - foreignKeyName: "spaces_owner_id_fkey" - columns: ["owner_id"] - isOneToOne: false - referencedRelation: "users" - referencedColumns: ["id"] + foreignKeyName: "spaces_owner_id_fkey"; + columns: ["owner_id"]; + isOneToOne: false; + referencedRelation: "users"; + referencedColumns: ["id"]; } - ] - } + ]; + }; users: { Row: { - active_space_id: string | null - avatar_url: string | null - created_at: string - email: string - full_name: string | null - id: string - onboarding_questions: Json | null - stripe_customer_id: string | null - stripe_subscription_id: string | null - stripe_subscription_price_id: string | null - stripe_subscription_status: string | null - updated_at: string - video_quota: number - videos_created: number - } + active_space_id: string | null; + avatar_url: string | null; + created_at: string; + email: string; + full_name: string | null; + id: string; + onboarding_questions: Json | null; + stripe_customer_id: string | null; + stripe_subscription_id: string | null; + stripe_subscription_price_id: string | null; + stripe_subscription_status: string | null; + updated_at: string; + video_quota: number; + videos_created: number; + }; Insert: { - active_space_id?: string | null - avatar_url?: string | null - created_at?: string - email: string - full_name?: string | null - id?: string - onboarding_questions?: Json | null - stripe_customer_id?: string | null - stripe_subscription_id?: string | null - stripe_subscription_price_id?: string | null - stripe_subscription_status?: string | null - updated_at?: string - video_quota?: number - videos_created?: number - } + active_space_id?: string | null; + avatar_url?: string | null; + created_at?: string; + email: string; + full_name?: string | null; + id?: string; + onboarding_questions?: Json | null; + stripe_customer_id?: string | null; + stripe_subscription_id?: string | null; + stripe_subscription_price_id?: string | null; + stripe_subscription_status?: string | null; + updated_at?: string; + video_quota?: number; + videos_created?: number; + }; Update: { - active_space_id?: string | null - avatar_url?: string | null - created_at?: string - email?: string - full_name?: string | null - id?: string - onboarding_questions?: Json | null - stripe_customer_id?: string | null - stripe_subscription_id?: string | null - stripe_subscription_price_id?: string | null - stripe_subscription_status?: string | null - updated_at?: string - video_quota?: number - videos_created?: number - } + active_space_id?: string | null; + avatar_url?: string | null; + created_at?: string; + email?: string; + full_name?: string | null; + id?: string; + onboarding_questions?: Json | null; + stripe_customer_id?: string | null; + stripe_subscription_id?: string | null; + stripe_subscription_price_id?: string | null; + stripe_subscription_status?: string | null; + updated_at?: string; + video_quota?: number; + videos_created?: number; + }; Relationships: [ { - foreignKeyName: "users_active_space_id_fkey" - columns: ["active_space_id"] - isOneToOne: false - referencedRelation: "spaces" - referencedColumns: ["id"] + foreignKeyName: "users_active_space_id_fkey"; + columns: ["active_space_id"]; + isOneToOne: false; + referencedRelation: "spaces"; + referencedColumns: ["id"]; } - ] - } + ]; + }; videos: { Row: { - aws_bucket: string | null - aws_region: string | null - complete: boolean - created_at: string - duration: number | null - id: string - is_public: boolean - metadata: Json | null - name: string - owner_id: string | null - s3_url: string | null - thumbnail_url: string | null - updated_at: string - } + aws_bucket: string | null; + aws_region: string | null; + complete: boolean; + created_at: string; + duration: number | null; + id: string; + is_public: boolean; + metadata: Json | null; + name: string; + owner_id: string | null; + s3_url: string | null; + thumbnail_url: string | null; + updated_at: string; + }; Insert: { - aws_bucket?: string | null - aws_region?: string | null - complete?: boolean - created_at?: string - duration?: number | null - id?: string - is_public?: boolean - metadata?: Json | null - name?: string - owner_id?: string | null - s3_url?: string | null - thumbnail_url?: string | null - updated_at?: string - } + aws_bucket?: string | null; + aws_region?: string | null; + complete?: boolean; + created_at?: string; + duration?: number | null; + id?: string; + is_public?: boolean; + metadata?: Json | null; + name?: string; + owner_id?: string | null; + s3_url?: string | null; + thumbnail_url?: string | null; + updated_at?: string; + }; Update: { - aws_bucket?: string | null - aws_region?: string | null - complete?: boolean - created_at?: string - duration?: number | null - id?: string - is_public?: boolean - metadata?: Json | null - name?: string - owner_id?: string | null - s3_url?: string | null - thumbnail_url?: string | null - updated_at?: string - } + aws_bucket?: string | null; + aws_region?: string | null; + complete?: boolean; + created_at?: string; + duration?: number | null; + id?: string; + is_public?: boolean; + metadata?: Json | null; + name?: string; + owner_id?: string | null; + s3_url?: string | null; + thumbnail_url?: string | null; + updated_at?: string; + }; Relationships: [ { - foreignKeyName: "videos_owner_id_fkey" - columns: ["owner_id"] - isOneToOne: false - referencedRelation: "users" - referencedColumns: ["id"] + foreignKeyName: "videos_owner_id_fkey"; + columns: ["owner_id"]; + isOneToOne: false; + referencedRelation: "users"; + referencedColumns: ["id"]; } - ] - } - } + ]; + }; + }; Views: { - [_ in never]: never - } + [_ in never]: never; + }; Functions: { citext: | { Args: { - "": boolean - } - Returns: string + "": boolean; + }; + Returns: string; } | { Args: { - "": string - } - Returns: string + "": string; + }; + Returns: string; } | { Args: { - "": unknown - } - Returns: string - } + "": unknown; + }; + Returns: string; + }; citext_hash: { Args: { - "": string - } - Returns: number - } + "": string; + }; + Returns: number; + }; citextin: { Args: { - "": unknown - } - Returns: string - } + "": unknown; + }; + Returns: string; + }; citextout: { Args: { - "": string - } - Returns: unknown - } + "": string; + }; + Returns: unknown; + }; citextrecv: { Args: { - "": unknown - } - Returns: string - } + "": unknown; + }; + Returns: string; + }; citextsend: { Args: { - "": string - } - Returns: string - } - } + "": string; + }; + Returns: string; + }; + }; Enums: { - user_role: "owner" | "admin" | "member" - } + user_role: "owner" | "admin" | "member"; + }; CompositeTypes: { - [_ in never]: never - } - } + [_ in never]: never; + }; + }; } export type Tables< @@ -310,7 +310,7 @@ export type Tables< > = PublicTableNameOrOptions extends { schema: keyof Database } ? (Database[PublicTableNameOrOptions["schema"]]["Tables"] & Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends { - Row: infer R + Row: infer R; } ? R : never @@ -318,11 +318,11 @@ export type Tables< Database["public"]["Views"]) ? (Database["public"]["Tables"] & Database["public"]["Views"])[PublicTableNameOrOptions] extends { - Row: infer R + Row: infer R; } ? R : never - : never + : never; export type TablesInsert< PublicTableNameOrOptions extends @@ -333,17 +333,17 @@ export type TablesInsert< : never = never > = PublicTableNameOrOptions extends { schema: keyof Database } ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { - Insert: infer I + Insert: infer I; } ? I : never : PublicTableNameOrOptions extends keyof Database["public"]["Tables"] ? Database["public"]["Tables"][PublicTableNameOrOptions] extends { - Insert: infer I + Insert: infer I; } ? I : never - : never + : never; export type TablesUpdate< PublicTableNameOrOptions extends @@ -354,17 +354,17 @@ export type TablesUpdate< : never = never > = PublicTableNameOrOptions extends { schema: keyof Database } ? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends { - Update: infer U + Update: infer U; } ? U : never : PublicTableNameOrOptions extends keyof Database["public"]["Tables"] ? Database["public"]["Tables"][PublicTableNameOrOptions] extends { - Update: infer U + Update: infer U; } ? U : never - : never + : never; export type Enums< PublicEnumNameOrOptions extends @@ -377,4 +377,4 @@ export type Enums< ? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName] : PublicEnumNameOrOptions extends keyof Database["public"]["Enums"] ? Database["public"]["Enums"][PublicEnumNameOrOptions] - : never + : never; From 19b6c26b108492498c0606259e3dc03426a393cc Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Sat, 2 Nov 2024 03:38:41 +0800 Subject: [PATCH 2/5] beta.6 --- Cargo.lock | 2 +- apps/desktop/src-tauri/Cargo.toml | 2 +- apps/web/content/changelog/13.mdx | 10 ++++++++++ 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 apps/web/content/changelog/13.mdx diff --git a/Cargo.lock b/Cargo.lock index d978865e4..7ec0ef688 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1548,7 +1548,7 @@ dependencies = [ [[package]] name = "desktop" -version = "0.3.0-beta.5.8" +version = "0.3.0-beta.6" dependencies = [ "anyhow", "axum", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index fe2fc0665..2c8878c81 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "desktop" -version = "0.3.0-beta.5.8" +version = "0.3.0-beta.6" description = "Beautiful, shareable screen recordings." authors = ["you"] edition = "2021" diff --git a/apps/web/content/changelog/13.mdx b/apps/web/content/changelog/13.mdx new file mode 100644 index 000000000..6ea6616ec --- /dev/null +++ b/apps/web/content/changelog/13.mdx @@ -0,0 +1,10 @@ +--- +title: Microphone selector fix +app: Cap Desktop +publishedAt: "2024-11-02" +version: 0.3.0-beta.6 +image: +--- + +Fixes a bug where the microphone selector wouldn't ask for permission to access the microphone. +Thanks to everyone who reported this as feedback! From 52476203571f8ce0c310d68dec72d44a2840a4cf Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Sat, 2 Nov 2024 03:43:10 +0800 Subject: [PATCH 3/5] don't install ffmpeg in ci it's already handled in prebuild.js --- .github/workflows/publish.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b6d831fdf..1a8c8d759 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -125,11 +125,6 @@ jobs: - name: Install dependencies run: pnpm install - - name: Install FFmpeg - run: | - brew update - brew install ffmpeg@7 - - name: Create .env file in root run: | echo "appVersion=${{ needs.draft.outputs.version }}" >> .env @@ -144,9 +139,6 @@ jobs: - name: Output .env file run: cat apps/desktop/.env - - name: Log ffmpeg version - run: ffmpeg -version - - name: Cargo clean run: cargo clean @@ -176,4 +168,4 @@ jobs: uses: crabnebula-dev/cloud-release@v0 with: command: release upload ${{ env.CN_APPLICATION }} "${{ needs.draft.outputs.version }}" --framework tauri - api-key: ${{ secrets.CN_API_KEY }} \ No newline at end of file + api-key: ${{ secrets.CN_API_KEY }} From 0b6a0d9549748d53ceb6a64678693742cf0269bb Mon Sep 17 00:00:00 2001 From: Bojan Stefanovic <5675392+bojanstef@users.noreply.github.com> Date: Sun, 3 Nov 2024 04:18:22 -0500 Subject: [PATCH 4/5] Add shortcut icon link to RootLayout (#132) Include a link to the shortcut icon in the RootLayout component's head. --- apps/web/app/favicon.ico | Bin 25931 -> 0 bytes apps/web/app/layout.tsx | 1 + 2 files changed, 1 insertion(+) delete mode 100644 apps/web/app/favicon.ico diff --git a/apps/web/app/favicon.ico b/apps/web/app/favicon.ico deleted file mode 100644 index 718d6fea4835ec2d246af9800eddb7ffb276240c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25931 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 LrJugUA?W`A8`#=m diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index adcced572..66057db66 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -62,6 +62,7 @@ export default async function RootLayout({ /> + From e64afcd9b17820318ce7189b2341629c8f2694f5 Mon Sep 17 00:00:00 2001 From: Alejandro Saucedo Date: Sun, 3 Nov 2024 10:21:22 +0100 Subject: [PATCH 5/5] Update docker command to use default docker plugin (#133) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7712ec3b..298962481 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "type": "module", "scripts": { "build": "dotenv -e .env -- turbo run build", - "dev": "(cd apps/web && docker-compose up -d && cd ../..) && dotenv -e .env -- pnpm --dir packages/database db:generate && dotenv -e .env -- pnpm --dir packages/database db:push && dotenv -e .env -- turbo run dev --no-cache --concurrency 12", + "dev": "(cd apps/web && docker compose up -d && cd ../..) && dotenv -e .env -- pnpm --dir packages/database db:generate && dotenv -e .env -- pnpm --dir packages/database db:push && dotenv -e .env -- turbo run dev --no-cache --concurrency 12", "dev:manual": "dotenv -e .env -- turbo run dev --no-cache --concurrency 1", "lint": "turbo run lint", "format": "prettier --write \"**/*.{ts,tsx,md}\"",