Skip to content

Commit 884bbaa

Browse files
committed
feat: add desktop onboarding flow
Introduce an onboarding window that guides new users through initial setup. Adds the Onboarding window variant, route, settings flag (has_completed_onboarding), startup logic to show onboarding when needed, and a Help & Tour button in the main window header. Made-with: Cursor
1 parent 17755dc commit 884bbaa

File tree

6 files changed

+2439
-20
lines changed

6 files changed

+2439
-20
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,8 @@ pub struct GeneralSettingsStore {
149149
pub camera_window_position: Option<WindowPosition>,
150150
#[serde(default)]
151151
pub camera_window_positions_by_monitor_name: BTreeMap<String, WindowPosition>,
152+
#[serde(default = "default_true")]
153+
pub has_completed_onboarding: bool,
152154
}
153155

154156
fn default_enable_native_camera_preview() -> bool {
@@ -229,6 +231,7 @@ impl Default for GeneralSettingsStore {
229231
main_window_position: None,
230232
camera_window_position: None,
231233
camera_window_positions_by_monitor_name: BTreeMap::new(),
234+
has_completed_onboarding: false,
232235
}
233236
}
234237
}

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

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,15 @@ async fn set_camera_input(
638638
drop(app);
639639

640640
if id == current_id && camera_in_use {
641+
if id.is_some() && !skip_camera_window.unwrap_or(false) {
642+
let show_result = ShowCapWindow::Camera { centered: false }
643+
.show(&app_handle)
644+
.await;
645+
show_result
646+
.map_err(|err| error!("Failed to show camera preview window: {err}"))
647+
.ok();
648+
}
649+
641650
return Ok(());
642651
}
643652

@@ -2693,16 +2702,19 @@ async fn reset_camera_permissions(_app: AppHandle) -> Result<(), String> {
26932702

26942703
#[tauri::command]
26952704
#[specta::specta]
2696-
#[instrument(skip(app))]
2697-
async fn reset_microphone_permissions(app: AppHandle) -> Result<(), ()> {
2698-
let bundle_id = app.config().identifier.clone();
2705+
#[instrument(skip(_app))]
2706+
async fn reset_microphone_permissions(_app: AppHandle) -> Result<(), String> {
2707+
#[cfg(target_os = "macos")]
2708+
{
2709+
let bundle_id = _app.config().identifier.clone();
26992710

2700-
Command::new("tccutil")
2701-
.arg("reset")
2702-
.arg("Microphone")
2703-
.arg(bundle_id)
2704-
.output()
2705-
.expect("Failed to reset microphone permissions");
2711+
Command::new("tccutil")
2712+
.arg("reset")
2713+
.arg("Microphone")
2714+
.arg(bundle_id)
2715+
.output()
2716+
.map_err(|_| "Failed to reset microphone permissions".to_string())?;
2717+
}
27062718

27072719
Ok(())
27082720
}
@@ -3501,18 +3513,24 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
35013513
tokio::spawn({
35023514
let app = app.clone();
35033515
async move {
3504-
if !permissions.screen_recording.permitted()
3505-
|| !permissions.accessibility.permitted()
3506-
|| GeneralSettingsStore::get(&app)
3507-
.ok()
3508-
.flatten()
3509-
.map(|s| !s.has_completed_startup)
3510-
.unwrap_or(false)
3516+
let settings = GeneralSettingsStore::get(&app).ok().flatten();
3517+
let startup_completed = settings
3518+
.as_ref()
3519+
.map(|s| s.has_completed_startup)
3520+
.unwrap_or(false);
3521+
let onboarding_completed = settings
3522+
.as_ref()
3523+
.map(|s| s.has_completed_onboarding)
3524+
.unwrap_or(false);
3525+
3526+
if !startup_completed
3527+
|| !onboarding_completed
3528+
|| !permissions.necessary_granted()
35113529
{
3512-
let _ = ShowCapWindow::Setup.show(&app).await;
3530+
println!("Showing onboarding");
3531+
let _ = ShowCapWindow::Onboarding.show(&app).await;
35133532
} else {
3514-
println!("Permissions granted, showing main window");
3515-
3533+
println!("Showing main window");
35163534
let _ = ShowCapWindow::Main {
35173535
init_target_mode: None,
35183536
}
@@ -3864,6 +3882,12 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) {
38643882
.run(move |_handle, event| match event {
38653883
#[cfg(target_os = "macos")]
38663884
tauri::RunEvent::Reopen { .. } => {
3885+
if let Some(onboarding) = CapWindowId::Onboarding.get(_handle) {
3886+
onboarding.show().ok();
3887+
onboarding.set_focus().ok();
3888+
return;
3889+
}
3890+
38673891
let has_window = _handle.webview_windows().iter().any(|(label, _)| {
38683892
label.starts_with("editor-")
38693893
|| label.starts_with("screenshot-editor-")

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

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,7 @@ pub enum CapWindowId {
394394
ModeSelect,
395395
Debug,
396396
ScreenshotEditor { id: u32 },
397+
Onboarding,
397398
}
398399

399400
impl FromStr for CapWindowId {
@@ -412,6 +413,7 @@ impl FromStr for CapWindowId {
412413
"upgrade" => Self::Upgrade,
413414
"mode-select" => Self::ModeSelect,
414415
"debug" => Self::Debug,
416+
"onboarding" => Self::Onboarding,
415417
s if s.starts_with("editor-") => Self::Editor {
416418
id: s
417419
.replace("editor-", "")
@@ -462,6 +464,7 @@ impl std::fmt::Display for CapWindowId {
462464
Self::Editor { id } => write!(f, "editor-{id}"),
463465
Self::Debug => write!(f, "debug"),
464466
Self::ScreenshotEditor { id } => write!(f, "screenshot-editor-{id}"),
467+
Self::Onboarding => write!(f, "onboarding"),
465468
}
466469
}
467470
}
@@ -481,6 +484,7 @@ impl CapWindowId {
481484
Self::Editor { .. } => "Cap Editor".to_string(),
482485
Self::ScreenshotEditor { .. } => "Cap Screenshot Editor".to_string(),
483486
Self::ModeSelect => "Cap Mode Selection".to_string(),
487+
Self::Onboarding => "Welcome to Cap".to_string(),
484488
Self::Camera => "Cap Camera".to_string(),
485489
Self::RecordingsOverlay => "Cap Recordings Overlay".to_string(),
486490
Self::TargetSelectOverlay { .. } => "Cap Target Select".to_string(),
@@ -498,6 +502,7 @@ impl CapWindowId {
498502
| Self::Settings
499503
| Self::Upgrade
500504
| Self::ModeSelect
505+
| Self::Onboarding
501506
)
502507
}
503508

@@ -546,6 +551,7 @@ impl CapWindowId {
546551
Self::Camera => (200.0, 200.0),
547552
Self::Upgrade => (950.0, 850.0),
548553
Self::ModeSelect => (580.0, 340.0),
554+
Self::Onboarding => (860.0, 680.0),
549555
_ => return None,
550556
})
551557
}
@@ -585,6 +591,7 @@ pub enum ShowCapWindow {
585591
ScreenshotEditor {
586592
path: PathBuf,
587593
},
594+
Onboarding,
588595
}
589596

590597
impl ShowCapWindow {
@@ -992,11 +999,14 @@ impl ShowCapWindow {
992999
}
9931000
}
9941001

1002+
window.show().ok();
1003+
window.set_focus().ok();
1004+
9951005
window
9961006
}
9971007
Self::Main { init_target_mode } => {
9981008
if !permissions::do_permissions_check(false).necessary_granted() {
999-
return Box::pin(Self::Setup.show(app)).await;
1009+
return Box::pin(Self::Onboarding.show(app)).await;
10001010
}
10011011

10021012
let title = CapWindowId::Main.title();
@@ -1277,6 +1287,7 @@ impl ShowCapWindow {
12771287
.min_inner_size(800.0, 580.0)
12781288
.resizable(true)
12791289
.maximized(false)
1290+
.focused(true)
12801291
.build()?;
12811292

12821293
let (pos_x, pos_y) = cursor_monitor.center_position(800.0, 580.0);
@@ -1293,6 +1304,9 @@ impl ShowCapWindow {
12931304
}
12941305
}
12951306

1307+
window.show().ok();
1308+
window.set_focus().ok();
1309+
12961310
window
12971311
}
12981312
Self::Editor { .. } => {
@@ -1361,6 +1375,9 @@ impl ShowCapWindow {
13611375
}
13621376
}
13631377

1378+
window.show().ok();
1379+
window.set_focus().ok();
1380+
13641381
window
13651382
}
13661383
Self::Upgrade => {
@@ -1393,6 +1410,9 @@ impl ShowCapWindow {
13931410
}
13941411
}
13951412

1413+
window.show().ok();
1414+
window.set_focus().ok();
1415+
13961416
window
13971417
}
13981418
Self::ModeSelect => {
@@ -1425,6 +1445,47 @@ impl ShowCapWindow {
14251445
}
14261446
}
14271447

1448+
window.show().ok();
1449+
window.set_focus().ok();
1450+
1451+
window
1452+
}
1453+
Self::Onboarding => {
1454+
if let Some(main) = CapWindowId::Main.get(app) {
1455+
let _ = main.hide();
1456+
}
1457+
1458+
let width = (cursor_monitor.width * 0.58).clamp(860.0, 1080.0);
1459+
let height = (width * 0.72).clamp(680.0, 780.0);
1460+
1461+
let window = self
1462+
.window_builder(app, "/onboarding")
1463+
.inner_size(width, height)
1464+
.min_inner_size(860.0, 680.0)
1465+
.resizable(false)
1466+
.maximized(false)
1467+
.maximizable(false)
1468+
.focused(true)
1469+
.shadow(true)
1470+
.build()?;
1471+
1472+
let (pos_x, pos_y) = cursor_monitor.center_position(width, height);
1473+
let _ = window.set_position(tauri::LogicalPosition::new(pos_x, pos_y));
1474+
1475+
#[cfg(windows)]
1476+
{
1477+
use tauri::LogicalSize;
1478+
if let Err(e) = window.set_size(LogicalSize::new(width, height)) {
1479+
warn!("Failed to set Onboarding window size on Windows: {}", e);
1480+
}
1481+
if let Err(e) = window.set_position(tauri::LogicalPosition::new(pos_x, pos_y)) {
1482+
warn!("Failed to position Onboarding window on Windows: {}", e);
1483+
}
1484+
}
1485+
1486+
window.show().ok();
1487+
window.set_focus().ok();
1488+
14281489
window
14291490
}
14301491
Self::Camera { centered } => {
@@ -2119,6 +2180,7 @@ impl ShowCapWindow {
21192180
ShowCapWindow::InProgressRecording { .. } => CapWindowId::RecordingControls,
21202181
ShowCapWindow::Upgrade => CapWindowId::Upgrade,
21212182
ShowCapWindow::ModeSelect => CapWindowId::ModeSelect,
2183+
ShowCapWindow::Onboarding => CapWindowId::Onboarding,
21222184
ShowCapWindow::ScreenshotEditor { path } => {
21232185
let state = app.state::<ScreenshotEditorWindowIds>();
21242186
let s = state.ids.lock().unwrap();

apps/desktop/src/App.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ const SettingsIntegrationsPage = lazy(
5555
const SettingsS3ConfigPage = lazy(
5656
() => import("./routes/(window-chrome)/settings/integrations/s3-config"),
5757
);
58+
const OnboardingPage = lazy(
59+
() => import("./routes/(window-chrome)/onboarding"),
60+
);
5861
const UpgradePage = lazy(() => import("./routes/(window-chrome)/upgrade"));
5962
const UpdatePage = lazy(() => import("./routes/(window-chrome)/update"));
6063
const CameraPage = lazy(() => import("./routes/camera"));
@@ -172,6 +175,7 @@ function Inner() {
172175
component={SettingsS3ConfigPage}
173176
/>
174177
</Route>
178+
<Route path="/onboarding" component={OnboardingPage} />
175179
<Route path="/upgrade" component={UpgradePage} />
176180
<Route path="/update" component={UpdatePage} />
177181
</Route>

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import IconCapSettings from "~icons/cap/settings";
7272
import IconLucideAppWindowMac from "~icons/lucide/app-window-mac";
7373
import IconLucideArrowLeft from "~icons/lucide/arrow-left";
7474
import IconLucideBug from "~icons/lucide/bug";
75+
import IconLucideCircleHelp from "~icons/lucide/circle-help";
7576
import IconLucideImage from "~icons/lucide/image";
7677
import IconLucideImport from "~icons/lucide/import";
7778
import IconLucideSearch from "~icons/lucide/search";
@@ -1743,6 +1744,17 @@ function Page() {
17431744
</button>
17441745
</Tooltip>
17451746
<ChangelogButton />
1747+
<Tooltip content={<span>Help & Tour</span>}>
1748+
<button
1749+
type="button"
1750+
onClick={() => {
1751+
commands.showWindow("Onboarding");
1752+
}}
1753+
class="flex justify-center items-center size-5 focus:outline-none"
1754+
>
1755+
<IconLucideCircleHelp class="transition-colors text-gray-11 size-4 hover:text-gray-12" />
1756+
</button>
1757+
</Tooltip>
17461758
{import.meta.env.DEV && (
17471759
<button
17481760
type="button"

0 commit comments

Comments
 (0)