Skip to content

Commit 72edbb3

Browse files
Merge pull request #1479 from CapSoftware/permissions-bits
Improve permission request flow and recording events
2 parents 6d6c67d + d04678b commit 72edbb3

File tree

15 files changed

+159
-100
lines changed

15 files changed

+159
-100
lines changed

apps/desktop/src-tauri/src/hotkeys.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,9 @@ async fn handle_hotkey(app: AppHandle, action: HotkeyAction) -> Result<(), Strin
152152
HotkeyAction::RestartRecording => recording::restart_recording(app.clone(), app.state())
153153
.await
154154
.map(|_| ()),
155-
HotkeyAction::TogglePauseRecording => recording::toggle_pause_recording(app.state()).await,
155+
HotkeyAction::TogglePauseRecording => {
156+
recording::toggle_pause_recording(app.clone(), app.state()).await
157+
}
156158
HotkeyAction::CycleRecordingMode => {
157159
let current = RecordingSettingsStore::get(&app)
158160
.ok()

apps/desktop/src-tauri/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3095,6 +3095,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
30953095
|| label.starts_with("screenshot-editor-")
30963096
|| label.as_str() == "settings"
30973097
|| label.as_str() == "signin"
3098+
|| label.as_str() == "setup"
30983099
});
30993100

31003101
if has_window {
@@ -3106,6 +3107,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
31063107
|| label.starts_with("screenshot-editor-")
31073108
|| label.as_str() == "settings"
31083109
|| label.as_str() == "signin"
3110+
|| label.as_str() == "setup"
31093111
})
31103112
.map(|(_, window)| window.clone())
31113113
{

apps/desktop/src-tauri/src/permissions.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -66,33 +66,33 @@ pub fn open_permission_settings(_permission: OSPermission) {
6666
pub async fn request_permission(_permission: OSPermission) {
6767
#[cfg(target_os = "macos")]
6868
{
69-
use futures::executor::block_on;
70-
use std::thread;
71-
7269
match _permission {
7370
OSPermission::ScreenRecording => {
74-
#[cfg(target_os = "macos")]
7571
scap_screencapturekit::request_permission();
7672
}
7773
OSPermission::Camera => {
78-
thread::spawn(|| {
79-
block_on(av::CaptureDevice::request_access_for_media_type(
74+
tauri::async_runtime::spawn_blocking(|| {
75+
futures::executor::block_on(av::CaptureDevice::request_access_for_media_type(
8076
av::MediaType::video(),
8177
))
8278
.ok();
83-
});
79+
})
80+
.await
81+
.ok();
8482
}
8583
OSPermission::Microphone => {
86-
thread::spawn(|| {
87-
block_on(av::CaptureDevice::request_access_for_media_type(
84+
tauri::async_runtime::spawn_blocking(|| {
85+
futures::executor::block_on(av::CaptureDevice::request_access_for_media_type(
8886
av::MediaType::audio(),
8987
))
9088
.ok();
91-
});
89+
})
90+
.await
91+
.ok();
9292
}
9393
OSPermission::Accessibility => {
9494
use core_foundation::base::TCFType;
95-
use core_foundation::dictionary::CFDictionary; // Import CFDictionaryRef
95+
use core_foundation::dictionary::CFDictionary;
9696
use core_foundation::string::CFString;
9797

9898
let prompt_key = CFString::new("AXTrustedCheckOptionPrompt");

apps/desktop/src-tauri/src/recording.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,8 @@ pub enum RecordingEvent {
354354
Countdown { value: u32 },
355355
Started,
356356
Stopped,
357+
Paused,
358+
Resumed,
357359
Failed { error: String },
358360
InputLost { input: RecordingInputKind },
359361
InputRestored { input: RecordingInputKind },
@@ -943,41 +945,48 @@ pub async fn start_recording(
943945

944946
#[tauri::command]
945947
#[specta::specta]
946-
#[instrument(skip(state))]
947-
pub async fn pause_recording(state: MutableState<'_, App>) -> Result<(), String> {
948+
#[instrument(skip(app, state))]
949+
pub async fn pause_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> {
948950
let mut state = state.write().await;
949951

950952
if let Some(recording) = state.current_recording_mut() {
951953
recording.pause().await.map_err(|e| e.to_string())?;
954+
RecordingEvent::Paused.emit(&app).ok();
952955
}
953956

954957
Ok(())
955958
}
956959

957960
#[tauri::command]
958961
#[specta::specta]
959-
#[instrument(skip(state))]
960-
pub async fn resume_recording(state: MutableState<'_, App>) -> Result<(), String> {
962+
#[instrument(skip(app, state))]
963+
pub async fn resume_recording(app: AppHandle, state: MutableState<'_, App>) -> Result<(), String> {
961964
let mut state = state.write().await;
962965

963966
if let Some(recording) = state.current_recording_mut() {
964967
recording.resume().await.map_err(|e| e.to_string())?;
968+
RecordingEvent::Resumed.emit(&app).ok();
965969
}
966970

967971
Ok(())
968972
}
969973

970974
#[tauri::command]
971975
#[specta::specta]
972-
#[instrument(skip(state))]
973-
pub async fn toggle_pause_recording(state: MutableState<'_, App>) -> Result<(), String> {
976+
#[instrument(skip(app, state))]
977+
pub async fn toggle_pause_recording(
978+
app: AppHandle,
979+
state: MutableState<'_, App>,
980+
) -> Result<(), String> {
974981
let state = state.read().await;
975982

976983
if let Some(recording) = state.current_recording() {
977984
if recording.is_paused().await.map_err(|e| e.to_string())? {
978985
recording.resume().await.map_err(|e| e.to_string())?;
986+
RecordingEvent::Resumed.emit(&app).ok();
979987
} else {
980988
recording.pause().await.map_err(|e| e.to_string())?;
989+
RecordingEvent::Paused.emit(&app).ok();
981990
}
982991
}
983992

apps/desktop/src-tauri/src/tray.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pub enum TrayItem {
4444
ModeStudio,
4545
ModeInstant,
4646
ModeScreenshot,
47+
RequestPermissions,
4748
}
4849

4950
impl From<TrayItem> for MenuId {
@@ -64,6 +65,7 @@ impl From<TrayItem> for MenuId {
6465
TrayItem::ModeStudio => "mode_studio",
6566
TrayItem::ModeInstant => "mode_instant",
6667
TrayItem::ModeScreenshot => "mode_screenshot",
68+
TrayItem::RequestPermissions => "request_permissions",
6769
}
6870
.into()
6971
}
@@ -92,6 +94,7 @@ impl TryFrom<MenuId> for TrayItem {
9294
"mode_studio" => Ok(TrayItem::ModeStudio),
9395
"mode_instant" => Ok(TrayItem::ModeInstant),
9496
"mode_screenshot" => Ok(TrayItem::ModeScreenshot),
97+
"request_permissions" => Ok(TrayItem::RequestPermissions),
9598
value => Err(format!("Invalid tray item id {value}")),
9699
}
97100
}
@@ -321,6 +324,10 @@ fn get_current_mode(app: &AppHandle) -> RecordingMode {
321324
.unwrap_or_default()
322325
}
323326

327+
fn is_setup_window_open(app: &AppHandle) -> bool {
328+
app.webview_windows().contains_key("setup")
329+
}
330+
324331
fn create_mode_submenu(app: &AppHandle) -> tauri::Result<Submenu<tauri::Wry>> {
325332
let current_mode = get_current_mode(app);
326333

@@ -352,6 +359,30 @@ fn create_mode_submenu(app: &AppHandle) -> tauri::Result<Submenu<tauri::Wry>> {
352359
}
353360

354361
fn build_tray_menu(app: &AppHandle, cache: &PreviousItemsCache) -> tauri::Result<Menu<tauri::Wry>> {
362+
if is_setup_window_open(app) {
363+
return Menu::with_items(
364+
app,
365+
&[
366+
&MenuItem::with_id(
367+
app,
368+
TrayItem::RequestPermissions,
369+
"Request Permissions",
370+
true,
371+
None::<&str>,
372+
)?,
373+
&PredefinedMenuItem::separator(app)?,
374+
&MenuItem::with_id(
375+
app,
376+
"version",
377+
format!("Cap v{}", env!("CARGO_PKG_VERSION")),
378+
false,
379+
None::<&str>,
380+
)?,
381+
&MenuItem::with_id(app, TrayItem::Quit, "Quit Cap", true, None::<&str>)?,
382+
],
383+
);
384+
}
385+
355386
let previous_submenu = create_previous_submenu(app, cache)?;
356387
let mode_submenu = create_mode_submenu(app)?;
357388

@@ -628,6 +659,12 @@ pub fn create_tray(app: &AppHandle) -> tauri::Result<()> {
628659
Ok(TrayItem::ModeScreenshot) => {
629660
handle_mode_selection(app, RecordingMode::Screenshot, &cache);
630661
}
662+
Ok(TrayItem::RequestPermissions) => {
663+
let app = app.clone();
664+
tokio::spawn(async move {
665+
let _ = ShowCapWindow::Setup.show(&app).await;
666+
});
667+
}
631668
_ => {}
632669
}
633670
})

apps/desktop/src-tauri/src/windows.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -666,8 +666,10 @@ impl ShowCapWindow {
666666
.init_window(window.clone(), camera_feed)
667667
.await
668668
{
669-
error!("Error initializing camera preview: {err}");
670-
window.close().ok();
669+
error!(
670+
"Error initializing camera preview, falling back to WebSocket preview: {err}"
671+
);
672+
window.show().ok();
671673
}
672674
} else {
673675
window.show().ok();

apps/desktop/src/components/ModeSelect.tsx

Lines changed: 4 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,18 @@
11
import { cx } from "cva";
22
import { type JSX, Show } from "solid-js";
3-
import instantModeDark from "~/assets/illustrations/instant-mode-dark.png";
4-
import instantModeLight from "~/assets/illustrations/instant-mode-light.png";
5-
import studioModeDark from "~/assets/illustrations/studio-mode-dark.png";
6-
import studioModeLight from "~/assets/illustrations/studio-mode-light.png";
73
import { createOptionsQuery } from "~/utils/queries";
84
import { commands, type RecordingMode } from "~/utils/tauri";
95

106
interface ModeOptionProps {
117
mode: RecordingMode;
128
title: string;
139
description: string;
14-
imageDark?: string;
15-
imageLight?: string;
16-
icon?: (props: { class: string; style?: JSX.CSSProperties }) => JSX.Element;
10+
icon: (props: { class: string; style?: JSX.CSSProperties }) => JSX.Element;
1711
isSelected: boolean;
1812
onSelect: (mode: RecordingMode) => void;
1913
}
2014

2115
const ModeOption = (props: ModeOptionProps) => {
22-
const hasImage = () => props.imageDark && props.imageLight;
23-
2416
return (
2517
<div
2618
data-tauri-drag-region="none"
@@ -41,34 +33,7 @@ const ModeOption = (props: ModeOptionProps) => {
4133
</Show>
4234

4335
<div class="flex items-center justify-center w-full pt-5 pb-3">
44-
<Show
45-
when={hasImage()}
46-
fallback={
47-
<div
48-
class={cx(
49-
"flex items-center justify-center size-20 rounded-full",
50-
props.isSelected
51-
? "bg-blue-4 dark:bg-blue-4/50"
52-
: "bg-gray-3 dark:bg-gray-4",
53-
)}
54-
>
55-
{props.icon?.({
56-
class: "size-10 invert dark:invert-0",
57-
})}
58-
</div>
59-
}
60-
>
61-
<img
62-
src={props.imageDark}
63-
alt={props.title}
64-
class="hidden dark:block size-20 object-contain"
65-
/>
66-
<img
67-
src={props.imageLight}
68-
alt={props.title}
69-
class="block dark:hidden size-20 object-contain"
70-
/>
71-
</Show>
36+
<props.icon class="size-6 invert dark:invert-0" />
7237
</div>
7338

7439
<div class="flex flex-col items-center px-4 pb-4 text-center">
@@ -101,15 +66,13 @@ const ModeSelect = (props: { onClose?: () => void; standalone?: boolean }) => {
10166
mode: "instant" as const,
10267
title: "Instant",
10368
description: "Share instantly with a link. Uploads as you record.",
104-
imageDark: instantModeDark,
105-
imageLight: instantModeLight,
69+
icon: IconCapInstant,
10670
},
10771
{
10872
mode: "studio" as const,
10973
title: "Studio",
11074
description: "Highest quality local recording for editing later.",
111-
imageDark: studioModeDark,
112-
imageLight: studioModeLight,
75+
icon: IconCapFilmCut,
11376
},
11477
{
11578
mode: "screenshot" as const,
@@ -145,8 +108,6 @@ const ModeSelect = (props: { onClose?: () => void; standalone?: boolean }) => {
145108
mode={option.mode}
146109
title={option.title}
147110
description={option.description}
148-
imageDark={option.imageDark}
149-
imageLight={option.imageLight}
150111
icon={option.icon}
151112
isSelected={rawOptions.mode === option.mode}
152113
onSelect={handleModeChange}

apps/desktop/src/routes/(window-chrome)/new-main/CameraSelect.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ export function CameraSelectBase(props: {
9090

9191
const onChange = (cameraLabel: CameraInfo | null) => {
9292
if (!cameraLabel && !permissionGranted())
93-
return requestPermission("camera");
93+
return requestPermission("camera", props.permissions?.camera);
9494

9595
props.onChange(cameraLabel);
9696

@@ -110,7 +110,7 @@ export function CameraSelectBase(props: {
110110
disabled={!!currentRecording.data || props.disabled}
111111
onClick={() => {
112112
if (!permissionGranted()) {
113-
requestPermission("camera");
113+
requestPermission("camera", props.permissions?.camera);
114114
return;
115115
}
116116

@@ -156,7 +156,9 @@ export function CameraSelectBase(props: {
156156
PillComponent={props.PillComponent}
157157
value={props.value}
158158
permissionGranted={permissionGranted()}
159-
requestPermission={() => requestPermission("camera")}
159+
requestPermission={() =>
160+
requestPermission("camera", props.permissions?.camera)
161+
}
160162
onClick={(e) => {
161163
if (!props.options) return;
162164
if (props.value !== null) {

apps/desktop/src/routes/(window-chrome)/new-main/MicrophoneSelect.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ export function MicrophoneSelectBase(props: {
7979
else setDbs(dbs);
8080
});
8181

82-
// visual audio level from 0 -> 1
8382
const audioLevel = () =>
8483
(1 - Math.max((dbs() ?? 0) + DB_SCALE, 0) / DB_SCALE) ** 0.5;
8584

@@ -98,7 +97,7 @@ export function MicrophoneSelectBase(props: {
9897
class={props.class}
9998
onClick={() => {
10099
if (!permissionGranted()) {
101-
requestPermission("microphone");
100+
requestPermission("microphone", props.permissions?.microphone);
102101
return;
103102
}
104103

@@ -142,7 +141,9 @@ export function MicrophoneSelectBase(props: {
142141
PillComponent={props.PillComponent}
143142
value={props.value}
144143
permissionGranted={permissionGranted()}
145-
requestPermission={() => requestPermission("microphone")}
144+
requestPermission={() =>
145+
requestPermission("microphone", props.permissions?.microphone)
146+
}
146147
onClick={(e) => {
147148
if (props.value !== null) {
148149
e.stopPropagation();

0 commit comments

Comments
 (0)