Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ scripts/releases-backfill-data.txt
# SEO agent state (machine-local, contains sensitive ranking data)
/seo/
/docs/
/.growth-agent/

# OpenCode commands (local)
.opencode/
Expand Down
39 changes: 38 additions & 1 deletion apps/desktop/src-tauri/src/recording_settings.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
use cap_recording::{
RecordingMode, feeds::camera::DeviceOrModelID, sources::screen_capture::ScreenCaptureTarget,
RecordingMode,
feeds::{
camera::{CameraDeviceSettings, DeviceOrModelID},
microphone::MicrophoneDeviceSettings,
},
sources::screen_capture::ScreenCaptureTarget,
};
use std::collections::HashMap;
use tauri::{AppHandle, Wry};
use tauri_plugin_store::StoreExt;

Expand All @@ -24,6 +30,8 @@ pub struct RecordingSettingsStore {
pub mode: Option<RecordingMode>,
pub system_audio: bool,
pub organization_id: Option<String>,
pub camera_device_settings: HashMap<String, CameraDeviceSettings>,
pub microphone_device_settings: HashMap<String, MicrophoneDeviceSettings>,
}

impl RecordingSettingsStore {
Expand All @@ -48,6 +56,35 @@ impl RecordingSettingsStore {
store.set(Self::KEY, serde_json::json!(settings));
store.save().map_err(|e| e.to_string())
}

pub fn camera_settings_for(
app: &AppHandle<Wry>,
id: &DeviceOrModelID,
) -> Option<CameraDeviceSettings> {
Self::get(app).ok().flatten().and_then(|settings| {
settings
.camera_device_settings
.get(&camera_key(id))
.copied()
})
}

pub fn microphone_settings_for(
app: &AppHandle<Wry>,
label: &str,
) -> Option<MicrophoneDeviceSettings> {
Self::get(app)
.ok()
.flatten()
.and_then(|settings| settings.microphone_device_settings.get(label).copied())
}
}

pub fn camera_key(id: &DeviceOrModelID) -> String {
match id {
DeviceOrModelID::DeviceID(device_id) => format!("device:{device_id}"),
DeviceOrModelID::ModelID(model_id) => format!("model:{model_id}"),
}
}

#[tauri::command]
Expand Down
3 changes: 2 additions & 1 deletion apps/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ function Inner() {
if (match.route.info?.AUTO_SHOW_WINDOW === false) return;
}

if (location.pathname !== "/camera") currentWindow.show();
if (location.pathname !== "/" && location.pathname !== "/camera")
currentWindow.show();
});

return <Suspense fallback={null}>{props.children}</Suspense>;
Expand Down
17 changes: 3 additions & 14 deletions apps/desktop/src/routes/(window-chrome).tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { RouteSectionProps } from "@solidjs/router";
import type { UnlistenFn } from "@tauri-apps/api/event";
import { emit } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { type as ostype } from "@tauri-apps/plugin-os";
import { cx } from "cva";
Expand All @@ -20,19 +19,9 @@ export default function (props: RouteSectionProps) {

onMount(async () => {
console.log("window chrome mounted");
unlistenResize = await initializeTitlebar();
const { __CAP__ } = window as typeof window & {
__CAP__?: { initialTargetMode?: unknown };
};
const hasInitialTargetMode = __CAP__?.initialTargetMode != null;
const currentWindow = getCurrentWindow();
if (location.pathname === "/") {
void emit("main-window-ready");
}
if (location.pathname === "/" && !hasInitialTargetMode) {
await currentWindow.show();
await currentWindow.setFocus();
}
void initializeTitlebar().then((unlisten) => {
unlistenResize = unlisten;
});
});

const handleKeyDown = (e: KeyboardEvent) => {
Expand Down
73 changes: 57 additions & 16 deletions apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createTimer } from "@solid-primitives/timer";
import { CheckMenuItem, Menu, PredefinedMenuItem } from "@tauri-apps/api/menu";
import { cx } from "cva";
import {
type Component,
type ComponentProps,
Expand All @@ -15,6 +16,13 @@ import {
type DeviceOrModelID,
type OSPermissionsCheck,
} from "~/utils/tauri";
import {
DEVICE_ROW_CLASS,
DEVICE_ROW_ICON_CLASS,
DEVICE_ROW_LABEL_CLASS,
DEVICE_ROW_TRAILING_CLASS,
DEVICE_SHORTCUT_BUTTON_CLASS,
} from "./deviceRowStyles";
import InfoPill from "./InfoPill";
import TargetSelectInfoPill from "./TargetSelectInfoPill";
import useRequestPermission from "./useRequestPermission";
Expand All @@ -25,10 +33,13 @@ export default function CameraSelect(props: {
disabled?: boolean;
options: CameraInfo[];
value: CameraInfo | null;
selectedLabel?: string | null;
isSelected?: boolean;
onChange: (camera: CameraInfo | null) => void;
permissions?: OSPermissionsCheck;
hidePreviewButton?: boolean;
onOpen?: () => void;
onOpenSettings?: () => void;
}) {
const currentRecording = createCurrentRecordingQuery();
const requestPermission = useRequestPermission();
Expand Down Expand Up @@ -74,55 +85,84 @@ export default function CameraSelect(props: {
};

const permissionGranted = () =>
props.permissions?.camera === "granted" ||
props.permissions?.camera === "notNeeded";
props.permissions === undefined ||
props.permissions.camera === "granted" ||
props.permissions.camera === "notNeeded";

const hasSelection = () => props.isSelected ?? props.value !== null;

const label = () =>
props.value?.display_name ??
props.selectedLabel ??
(hasSelection() ? "Camera" : NO_CAMERA);

const showHiddenIndicator = () =>
props.value !== null &&
permissionGranted() &&
!cameraWindowOpen() &&
!props.hidePreviewButton;

const showSettingsShortcut = () =>
props.value !== null && permissionGranted() && !!props.onOpenSettings;

const isDisabled = () => !!currentRecording.data || props.disabled;

return (
<div class="flex flex-col gap-[0.25rem] items-stretch text-[--text-primary]">
<div class="flex flex-col items-stretch text-[--text-primary]">
<button
type="button"
disabled={!!currentRecording.data || props.disabled}
disabled={isDisabled()}
onClick={() => {
if (!permissionGranted()) {
requestPermission("camera", props.permissions?.camera);
return;
}
props.onOpen?.();
}}
class="flex flex-row gap-2 items-center px-2 w-full h-[42px] rounded-lg border border-gray-5 transition-colors cursor-default disabled:opacity-70 bg-gray-3 disabled:text-gray-11 KSelect"
class={cx(DEVICE_ROW_CLASS, "KSelect")}
aria-haspopup="menu"
>
<IconCapCamera class="text-gray-10 size-4" />
<p class="flex-1 text-sm text-left truncate">
{props.value?.display_name ?? NO_CAMERA}
</p>
<div class="flex items-center gap-1">
<IconCapCamera class={DEVICE_ROW_ICON_CLASS} />
<p class={DEVICE_ROW_LABEL_CLASS}>{label()}</p>
<div class={DEVICE_ROW_TRAILING_CLASS}>
<Show when={showHiddenIndicator()}>
<button
type="button"
onClick={openCameraWindow}
onPointerDown={(e) => e.stopPropagation()}
class="flex items-center justify-center px-2 py-1 rounded-full bg-gray-6 text-gray-11 hover:bg-gray-7 transition-colors"
class={DEVICE_SHORTCUT_BUTTON_CLASS}
title="Show camera preview"
aria-label="Show camera preview"
>
<IconLucideEyeOff class="size-3.5" />
</button>
</Show>
<Show when={showSettingsShortcut()}>
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
props.onOpenSettings?.();
}}
onPointerDown={(e) => e.stopPropagation()}
class={DEVICE_SHORTCUT_BUTTON_CLASS}
title="Camera settings"
aria-label="Camera settings"
>
<IconLucideSettings class="size-3.5" />
</button>
</Show>
<TargetSelectInfoPill
PillComponent={InfoPill}
value={props.value}
value={hasSelection() ? true : null}
permissionGranted={permissionGranted()}
requestPermission={() =>
requestPermission("camera", props.permissions?.camera)
}
onClick={(e) => {
if (!props.options) return;
if (props.value !== null) {
if (hasSelection()) {
e.stopPropagation();
props.onChange(null);
}
Expand All @@ -140,7 +180,7 @@ export function CameraSelectBase(props: {
value: CameraInfo | null;
onChange: (camera: CameraInfo | null) => void;
PillComponent: Component<
ComponentProps<"button"> & { variant: "blue" | "red" }
ComponentProps<"button"> & { variant: "blue" | "red" | "gray" }
>;
class: string;
iconClass: string;
Expand Down Expand Up @@ -191,8 +231,9 @@ export function CameraSelectBase(props: {
};

const permissionGranted = () =>
props.permissions?.camera === "granted" ||
props.permissions?.camera === "notNeeded";
props.permissions === undefined ||
props.permissions.camera === "granted" ||
props.permissions.camera === "notNeeded";

const onChange = (cameraLabel: CameraInfo | null) => {
if (!cameraLabel && !permissionGranted())
Expand Down
11 changes: 8 additions & 3 deletions apps/desktop/src/routes/(window-chrome)/new-main/InfoPill.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { cx } from "cva";
import type { ComponentProps } from "solid-js";

export type InfoPillVariant = "blue" | "red" | "gray";

export default function InfoPill(
props: ComponentProps<"button"> & { variant: "blue" | "red" },
props: ComponentProps<"button"> & { variant: InfoPillVariant },
) {
return (
<button
{...props}
type="button"
class={cx(
"px-2 py-0.5 rounded-full text-white text-[11px]",
props.variant === "blue" ? "bg-blue-9" : "bg-red-9",
"inline-flex items-center justify-center min-w-[40px] h-[24px] px-2.5 rounded-full text-[11px] font-medium leading-none transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-9 focus-visible:ring-offset-1 focus-visible:ring-offset-gray-1",
props.variant === "blue" && "bg-blue-9 text-white hover:bg-blue-10",
props.variant === "red" && "bg-red-9 text-white hover:bg-red-10",
props.variant === "gray" &&
"bg-gray-5 text-gray-11 hover:bg-gray-6 hover:text-gray-12",
)}
/>
);
Expand Down
Loading
Loading