From 2be4c40c07b1e31075ff216622870ff12b08be81 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Fri, 5 Apr 2024 13:34:13 +0100 Subject: [PATCH 01/16] feat: Add status API endpoint --- apps/web/app/api/status/route.ts | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 apps/web/app/api/status/route.ts diff --git a/apps/web/app/api/status/route.ts b/apps/web/app/api/status/route.ts new file mode 100644 index 00000000..ae5ff2d4 --- /dev/null +++ b/apps/web/app/api/status/route.ts @@ -0,0 +1,7 @@ +export const revalidate = 0; + +export async function GET() { + return new Response("OK", { + status: 200, + }); +} From 87980faacf53b46bfb8eed8a9bd670c088573574 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:11:18 +0100 Subject: [PATCH 02/16] feat: Replace default S3 url with CloudFront url --- apps/web/app/api/playlist/route.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/web/app/api/playlist/route.ts b/apps/web/app/api/playlist/route.ts index 014e1e9e..ff4b8c4d 100644 --- a/apps/web/app/api/playlist/route.ts +++ b/apps/web/app/api/playlist/route.ts @@ -15,6 +15,8 @@ import { generateMasterPlaylist, } from "@/utils/video/ffmpeg/helpers"; +export const revalidate = 3599; + const allowedOrigins = [ process.env.NEXT_PUBLIC_URL, "https://cap.link", @@ -198,7 +200,13 @@ export async function GET(request: NextRequest) { }) ); - return { url: url, duration: metadata?.Metadata?.duration ?? "" }; + return { + url: url.replace( + "https://capso.s3.us-east-1.amazonaws.com", + "https://v.cap.so" + ), + duration: metadata?.Metadata?.duration ?? "", + }; }) ); From 5e92a64df36703c61fb174bf4423197ea9cba085 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:16:32 +0100 Subject: [PATCH 03/16] fix: Update next config for image patterns --- apps/web/next.config.mjs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs index bb268686..23f2f54b 100644 --- a/apps/web/next.config.mjs +++ b/apps/web/next.config.mjs @@ -38,6 +38,18 @@ const nextConfig = { port: "", pathname: "**", }, + { + protocol: "https", + hostname: "*.cloudfront.net", + port: "", + pathname: "**", + }, + { + protocol: "https", + hostname: "*v.cap.so", + port: "", + pathname: "**", + }, ], }, async rewrites() { From 5827a5ced39440689b663872a313e42476824d27 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:20:09 +0100 Subject: [PATCH 04/16] fix: Revert CloudFront change --- apps/web/app/api/playlist/route.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/web/app/api/playlist/route.ts b/apps/web/app/api/playlist/route.ts index ff4b8c4d..8275d42d 100644 --- a/apps/web/app/api/playlist/route.ts +++ b/apps/web/app/api/playlist/route.ts @@ -200,13 +200,7 @@ export async function GET(request: NextRequest) { }) ); - return { - url: url.replace( - "https://capso.s3.us-east-1.amazonaws.com", - "https://v.cap.so" - ), - duration: metadata?.Metadata?.duration ?? "", - }; + return { url: url, duration: metadata?.Metadata?.duration ?? "" }; }) ); From 427aae5149cdc4bd96e2ef7157f0e58881242306 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:32:16 +0100 Subject: [PATCH 05/16] feat: re-add CloudFront url --- apps/web/app/api/playlist/route.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/web/app/api/playlist/route.ts b/apps/web/app/api/playlist/route.ts index 8275d42d..ff4b8c4d 100644 --- a/apps/web/app/api/playlist/route.ts +++ b/apps/web/app/api/playlist/route.ts @@ -200,7 +200,13 @@ export async function GET(request: NextRequest) { }) ); - return { url: url, duration: metadata?.Metadata?.duration ?? "" }; + return { + url: url.replace( + "https://capso.s3.us-east-1.amazonaws.com", + "https://v.cap.so" + ), + duration: metadata?.Metadata?.duration ?? "", + }; }) ); From 6f3da6d17dabc5bc4595b5fde1ede175b2dca606 Mon Sep 17 00:00:00 2001 From: Richie McIlroy <33632126+richiemcilroy@users.noreply.github.com> Date: Fri, 5 Apr 2024 18:38:01 +0100 Subject: [PATCH 06/16] fix: Revert CloudFront change again --- apps/web/app/api/playlist/route.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/apps/web/app/api/playlist/route.ts b/apps/web/app/api/playlist/route.ts index ff4b8c4d..8275d42d 100644 --- a/apps/web/app/api/playlist/route.ts +++ b/apps/web/app/api/playlist/route.ts @@ -200,13 +200,7 @@ export async function GET(request: NextRequest) { }) ); - return { - url: url.replace( - "https://capso.s3.us-east-1.amazonaws.com", - "https://v.cap.so" - ), - duration: metadata?.Metadata?.duration ?? "", - }; + return { url: url, duration: metadata?.Metadata?.duration ?? "" }; }) ); From 70ff7b14b7e90c073c6b34eb26cd5691e3ae2041 Mon Sep 17 00:00:00 2001 From: Ilya <47112191+ItsEeleeya@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:17:24 +0330 Subject: [PATCH 07/16] Add tray menu Added tray menu icon Icon changes when recording starts or stops When icon is clicked the window gets focus --- apps/desktop/src-tauri/Cargo.lock | 13 ++++- apps/desktop/src-tauri/Cargo.toml | 2 +- .../src-tauri/icons/tray-default-icon.png | Bin 0 -> 519 bytes .../src-tauri/icons/tray-stop-icon.png | Bin 0 -> 555 bytes apps/desktop/src-tauri/src/main.rs | 47 +++++++++++++++++- apps/desktop/src-tauri/tauri.conf.json | 6 ++- .../src/components/windows/inner/Recorder.tsx | 30 ++++++++++- 7 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 apps/desktop/src-tauri/icons/tray-default-icon.png create mode 100644 apps/desktop/src-tauri/icons/tray-stop-icon.png diff --git a/apps/desktop/src-tauri/Cargo.lock b/apps/desktop/src-tauri/Cargo.lock index cfef6e0f..759d8f04 100644 --- a/apps/desktop/src-tauri/Cargo.lock +++ b/apps/desktop/src-tauri/Cargo.lock @@ -2320,6 +2320,15 @@ dependencies = [ "serde", ] +[[package]] +name = "infer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f178e61cdbfe084aa75a2f4f7a25a5bb09701a47ae1753608f194b15783c937a" +dependencies = [ + "cfb", +] + [[package]] name = "infer" version = "0.13.0" @@ -4738,6 +4747,7 @@ dependencies = [ "http", "ignore", "indexmap 1.9.3", + "infer 0.9.0", "minisign-verify", "nix 0.26.4", "notify-rust", @@ -4747,6 +4757,7 @@ dependencies = [ "os_info", "os_pipe", "percent-encoding", + "png", "rand 0.8.5", "raw-window-handle 0.5.2", "regex", @@ -4950,7 +4961,7 @@ dependencies = [ "glob", "heck 0.4.1", "html5ever", - "infer", + "infer 0.13.0", "json-patch", "kuchikiki", "log", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 523243ad..a495a64a 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -14,7 +14,7 @@ tauri-build = { version = "1.5.1", features = [] } ffmpeg-sidecar = "0.5.1" [dependencies] -tauri = { version = "1.6.1", features = [ "updater", "macos-private-api", "window-set-position", "fs-write-file", "fs-remove-file", "fs-read-file", "fs-rename-file", "fs-exists", "fs-remove-dir", "fs-read-dir", "fs-copy-file", "fs-create-dir", "window-set-ignore-cursor-events", "window-unminimize", "window-minimize", "window-close", "window-show", "window-start-dragging", "window-hide", "window-unmaximize", "window-maximize", "window-set-always-on-top", "shell-open", "devtools", "os-all", "http-all"] } +tauri = { version = "1.6.1", features = [ "system-tray", "updater", "macos-private-api", "window-set-position", "fs-write-file", "fs-remove-file", "fs-read-file", "fs-rename-file", "fs-exists", "fs-remove-dir", "fs-read-dir", "fs-copy-file", "fs-create-dir", "window-set-ignore-cursor-events", "window-unminimize", "window-minimize", "window-close", "window-show", "window-start-dragging", "window-hide", "window-unmaximize", "window-maximize", "window-set-always-on-top", "shell-open", "devtools", "os-all", "http-all", "icon-png"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tauri-plugin-context-menu = "0.7.0" diff --git a/apps/desktop/src-tauri/icons/tray-default-icon.png b/apps/desktop/src-tauri/icons/tray-default-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..826d9ea52bc9af62ae4491a99c38dc27b50f542f GIT binary patch literal 519 zcmV+i0{H!jP)Px$!AV3xR9HvNmpd;7K@`V-*C!C%UgCzr7m#=~9--wHh>n6ptwpPrP|yhqw8W$F zNPGbWA<=upClL3a>?Gr4cCWiu#wOeB%sJ<`bN=VdOo@C?2ha{QC*~i(Gw{@?yHW!N z6M)`fpe@Dj6*vP9leez>p8(7POF(B`Z{H_80BgW@mR%NrmU$f*Z7{H%egZ6}r6&zw z5ZKRH>UZD{cuAs_fo`BBBQ=x2g&hw8DEKMR6mYf)?9_B{lM<*Gm;vSkXyklGiD z_;t_6hN+G{VA=twp8fXPmr-hYxm2Rpp0O&TEC9PaYBH^Pqtvl`GtH_Z05zU-&+rNu z_#=SJS|jTBd=Cl03b07^+yAiDQ2iGG(NhpzgvwLYG891cMijjjYZanyMgoYw9Sb7r zfkXh&k7R;~dMI_hJL<9AK}5Zn_`6uNtC`(Oqg_rzXOqIN=U?offs;cW>Pi3r002ov JPDHLkV1kWG=NA9~ literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/icons/tray-stop-icon.png b/apps/desktop/src-tauri/icons/tray-stop-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..acc363e98d8e46cf35682ef37471755edf0663c4 GIT binary patch literal 555 zcmV+`0@VG9P)Px$8A9#qt!~mLsEnqG-QDA%zSOh*p{yX*oEut1CT7PmEj>UlM>Zq za{x@M{|DVlxB#-|_p0ohz;d0zt=k5$WPZqU^lDAG0J}q>|)%ho^x$X1i= z<&!n>W;Ahv2mM>)zR6n-p2n8K=vibF2d~F zPy`ZdL`C{QNXWrPBmgO`SnTR;$I(`t{V;N?@aM;|0A#VmCUxwtd~*?k@Z?` tA+kP9^iiDcY393uWqF(?UImMr{QwuPp&=J=7ZLyf002ovPDHLkV1k|r^}hfB literal 0 HcmV?d00001 diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index cd455978..9feb3a2f 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -1,11 +1,17 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use std::any::Any; +use std::borrow::Borrow; +use std::os::macos::raw::stat; use std::sync::{Arc}; use std::path::PathBuf; +use ffmpeg_sidecar::event; +use serde::Deserialize; +use serde_json::Value; use tokio::sync::Mutex; use std::sync::atomic::{AtomicBool}; use std::env; -use tauri::{command, Manager, Window}; +use tauri::{command, CustomMenuItem, Manager, State, SystemTray, SystemTrayEvent, SystemTrayMenu, Window}; use window_vibrancy::{apply_blur, apply_vibrancy, NSVisualEffectMaterial}; use window_shadows::set_shadow; use tauri_plugin_positioner::{WindowExt, Position}; @@ -156,6 +162,17 @@ fn main() { (0, 0) }; + let tray_menu = SystemTrayMenu::new() + .add_item(CustomMenuItem::new("toggle-window", "Show Cap")) + .add_native_item(tauri::SystemTrayMenuItem::Separator) + .add_item( + CustomMenuItem::new("quit".to_string(), "Quit") + .native_image(tauri::NativeImage::StopProgress) + ); + + let tray = SystemTray::new().with_menu(tray_menu).with_menu_on_left_click(false); + + tauri::Builder::default() .plugin(tauri_plugin_oauth::init()) .plugin(tauri_plugin_positioner::init()) @@ -188,6 +205,18 @@ fn main() { app.manage(Arc::new(Mutex::new(recording_state))); + let handle = app.tray_handle().clone(); + app.listen_global("toggle-recording", move|event| { + let payload_error_msg = format!("Error while deserializing recording state from event payload: {:?}", event.payload()); + let recording_state: Value = serde_json::from_str(event.payload().expect(payload_error_msg.as_str())).unwrap(); + + if recording_state.as_bool().expect(payload_error_msg.as_str()) { + handle.set_icon(tauri::Icon::Raw(include_bytes!("../icons/tray-stop-icon.png").to_vec())).unwrap(); + } else { + handle.set_icon(tauri::Icon::Raw(include_bytes!("../icons/tray-default-icon.png").to_vec())).unwrap(); + } + }); + Ok(()) }) .invoke_handler(tauri::generate_handler![ @@ -205,6 +234,22 @@ fn main() { reset_camera_permissions, ]) .plugin(tauri_plugin_context_menu::init()) + .system_tray(tray) + .on_system_tray_event(move |app, event| match event { + SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { + "toggle-window" => { + + } + "quit" => { + app.exit(0); + } + _ => {} + }, + SystemTrayEvent::LeftClick { position: _, size: _, .. } => { + app.emit_all("tray-on-left-click", {}).unwrap(); + }, + _ => {} + }) .run(tauri::generate_context!()) .expect("Error while running tauri application"); } \ No newline at end of file diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index ccf9d002..0518fc47 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -99,6 +99,10 @@ "alwaysOnTop": false, "center": true } - ] + ], + "systemTray": { + "iconPath": "icons/tray-default-icon.png", + "iconAsTemplate": true + } } } diff --git a/apps/desktop/src/components/windows/inner/Recorder.tsx b/apps/desktop/src/components/windows/inner/Recorder.tsx index bf0c494e..c8a96871 100644 --- a/apps/desktop/src/components/windows/inner/Recorder.tsx +++ b/apps/desktop/src/components/windows/inner/Recorder.tsx @@ -9,7 +9,7 @@ import { Window } from "@/components/icons/Window"; import { ActionButton } from "@/components/recording/ActionButton"; import { Button } from "@cap/ui"; import { Logo } from "@/components/icons/Logo"; -import { emit } from "@tauri-apps/api/event"; +import { emit, listen, UnlistenFn } from "@tauri-apps/api/event"; import { invoke } from "@tauri-apps/api/tauri"; import { getLatestVideoId, saveLatestVideoId } from "@/utils/database/utils"; import { openLinkInBrowser } from "@/utils/helpers"; @@ -17,6 +17,7 @@ import toast, { Toaster } from "react-hot-toast"; import { authFetch } from "@/utils/auth/helpers"; import { appDataDir, join } from "@tauri-apps/api/path"; import { open } from "@tauri-apps/api/shell"; +import { window } from "@tauri-apps/api"; declare global { interface Window { @@ -137,6 +138,31 @@ export const Recorder = () => { return data; }; + useEffect(() => { + let unlistenFn: UnlistenFn | null = null; + const registerListener = async () => { + unlistenFn = await listen("tray-on-left-click", (_) => { + if (isRecording) { + handleStopAllRecordings(); + } + + const currentWindow = window.getCurrent(); + if (!currentWindow.isVisible) { + currentWindow.show(); + } + currentWindow.setFocus(); + }); + }; + + registerListener(); + + return () => { + if (unlistenFn) { + unlistenFn(); + } + }; + }, [isRecording, canStopRecording]); + const startDualRecording = async (videoData: { id: string; user_id: string; @@ -167,6 +193,7 @@ export const Recorder = () => { }).catch((error) => { console.error("Error invoking start_screen_recording:", error); }); + emit("toggle-recording", true); }; const handleStartAllRecordings = async () => { @@ -229,6 +256,7 @@ export const Recorder = () => { setIsRecording(false); setHasStartedRecording(false); setStoppingRecording(false); + emit("toggle-recording", false); } catch (error) { console.error("Error stopping recording:", error); } From 840cae6878f493b54a0cc03a682ea9cd74eb1e71 Mon Sep 17 00:00:00 2001 From: Ilya <47112191+ItsEeleeya@users.noreply.github.com> Date: Thu, 11 Apr 2024 12:40:39 +0330 Subject: [PATCH 08/16] Hide window when recording starts Make all windows hide when recording stops. Implement the tray "Show Cap" button --- apps/desktop/src-tauri/src/main.rs | 8 +++++--- apps/desktop/src/components/windows/inner/Recorder.tsx | 7 +++++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 9feb3a2f..caa0009e 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -163,7 +163,7 @@ fn main() { }; let tray_menu = SystemTrayMenu::new() - .add_item(CustomMenuItem::new("toggle-window", "Show Cap")) + .add_item(CustomMenuItem::new("show-window", "Show Cap")) .add_native_item(tauri::SystemTrayMenuItem::Separator) .add_item( CustomMenuItem::new("quit".to_string(), "Quit") @@ -237,8 +237,10 @@ fn main() { .system_tray(tray) .on_system_tray_event(move |app, event| match event { SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { - "toggle-window" => { - + "show-window" => { + let window = app.get_window("main").expect("Error while trying to get the main window."); + window.show().unwrap(); + window.set_focus().unwrap(); } "quit" => { app.exit(0); diff --git a/apps/desktop/src/components/windows/inner/Recorder.tsx b/apps/desktop/src/components/windows/inner/Recorder.tsx index c8a96871..7b3eb986 100644 --- a/apps/desktop/src/components/windows/inner/Recorder.tsx +++ b/apps/desktop/src/components/windows/inner/Recorder.tsx @@ -17,7 +17,7 @@ import toast, { Toaster } from "react-hot-toast"; import { authFetch } from "@/utils/auth/helpers"; import { appDataDir, join } from "@tauri-apps/api/path"; import { open } from "@tauri-apps/api/shell"; -import { window } from "@tauri-apps/api"; +import * as Tauri from "@tauri-apps/api"; declare global { interface Window { @@ -146,7 +146,7 @@ export const Recorder = () => { handleStopAllRecordings(); } - const currentWindow = window.getCurrent(); + const currentWindow = Tauri.window.getCurrent(); if (!currentWindow.isVisible) { currentWindow.show(); } @@ -180,6 +180,9 @@ export const Recorder = () => { if (window.fathom !== undefined) { window.fathom.trackEvent("start_recording"); } + Tauri.window.getAll().forEach((window) => { + window.hide(); + }); await invoke("start_dual_recording", { options: { user_id: videoData.user_id, From d9ed0b5db9ad98d5083ae321e2bf9e54128b4b48 Mon Sep 17 00:00:00 2001 From: Ilya <47112191+ItsEeleeya@users.noreply.github.com> Date: Thu, 11 Apr 2024 13:22:31 +0330 Subject: [PATCH 09/16] Don't hide camera view when recording starts. --- apps/desktop/src/components/windows/inner/Recorder.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/windows/inner/Recorder.tsx b/apps/desktop/src/components/windows/inner/Recorder.tsx index 7b3eb986..6ef8563a 100644 --- a/apps/desktop/src/components/windows/inner/Recorder.tsx +++ b/apps/desktop/src/components/windows/inner/Recorder.tsx @@ -181,7 +181,9 @@ export const Recorder = () => { window.fathom.trackEvent("start_recording"); } Tauri.window.getAll().forEach((window) => { - window.hide(); + if (window.label !== "camera") { + window.hide(); + } }); await invoke("start_dual_recording", { options: { From 1492360f8c8a6beb010dc67d523f37aaa320a749 Mon Sep 17 00:00:00 2001 From: Ilya <47112191+ItsEeleeya@users.noreply.github.com> Date: Fri, 12 Apr 2024 02:20:16 +0330 Subject: [PATCH 10/16] Change where the toggle-recording event emits and fix a small issue. The minimum 5 second length wasn't seem to be respected after your first recording in the latest version. The tray icon should now change more consistently --- apps/desktop/src-tauri/src/main.rs | 6 +++--- apps/desktop/src/components/windows/inner/Recorder.tsx | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index caa0009e..9ec5eabf 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -205,15 +205,15 @@ fn main() { app.manage(Arc::new(Mutex::new(recording_state))); - let handle = app.tray_handle().clone(); + let tray_handle = app.tray_handle().clone(); app.listen_global("toggle-recording", move|event| { let payload_error_msg = format!("Error while deserializing recording state from event payload: {:?}", event.payload()); let recording_state: Value = serde_json::from_str(event.payload().expect(payload_error_msg.as_str())).unwrap(); if recording_state.as_bool().expect(payload_error_msg.as_str()) { - handle.set_icon(tauri::Icon::Raw(include_bytes!("../icons/tray-stop-icon.png").to_vec())).unwrap(); + tray_handle.set_icon(tauri::Icon::Raw(include_bytes!("../icons/tray-stop-icon.png").to_vec())).unwrap(); } else { - handle.set_icon(tauri::Icon::Raw(include_bytes!("../icons/tray-default-icon.png").to_vec())).unwrap(); + tray_handle.set_icon(tauri::Icon::Raw(include_bytes!("../icons/tray-default-icon.png").to_vec())).unwrap(); } }); diff --git a/apps/desktop/src/components/windows/inner/Recorder.tsx b/apps/desktop/src/components/windows/inner/Recorder.tsx index 6ef8563a..1c90e4cc 100644 --- a/apps/desktop/src/components/windows/inner/Recorder.tsx +++ b/apps/desktop/src/components/windows/inner/Recorder.tsx @@ -197,8 +197,7 @@ export const Recorder = () => { }, }).catch((error) => { console.error("Error invoking start_screen_recording:", error); - }); - emit("toggle-recording", true); + }).then(() => emit("toggle-recording", true)); }; const handleStartAllRecordings = async () => { @@ -261,6 +260,7 @@ export const Recorder = () => { setIsRecording(false); setHasStartedRecording(false); setStoppingRecording(false); + setCanStopRecording(false); emit("toggle-recording", false); } catch (error) { console.error("Error stopping recording:", error); From 6bf2c1d5e24e95821f8b7c7b7ed3692a2e5fa725 Mon Sep 17 00:00:00 2001 From: Ilya <47112191+ItsEeleeya@users.noreply.github.com> Date: Sun, 14 Apr 2024 14:15:01 +0330 Subject: [PATCH 11/16] Show media device list options in tray menu and small cleanup. Not yet functional --- apps/desktop/src-tauri/src/main.rs | 71 ++++++++++++++++------- apps/desktop/src/utils/recording/utils.ts | 3 + 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 9ec5eabf..fd0bdc3c 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -1,17 +1,13 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -use std::any::Any; -use std::borrow::Borrow; -use std::os::macos::raw::stat; use std::sync::{Arc}; use std::path::PathBuf; -use ffmpeg_sidecar::event; use serde::Deserialize; use serde_json::Value; use tokio::sync::Mutex; use std::sync::atomic::{AtomicBool}; -use std::env; -use tauri::{command, CustomMenuItem, Manager, State, SystemTray, SystemTrayEvent, SystemTrayMenu, Window}; +use std::{vec}; +use tauri::{command, CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu, Window}; use window_vibrancy::{apply_blur, apply_vibrancy, NSVisualEffectMaterial}; use window_shadows::set_shadow; use tauri_plugin_positioner::{WindowExt, Position}; @@ -162,16 +158,19 @@ fn main() { (0, 0) }; - let tray_menu = SystemTrayMenu::new() - .add_item(CustomMenuItem::new("show-window", "Show Cap")) - .add_native_item(tauri::SystemTrayMenuItem::Separator) - .add_item( - CustomMenuItem::new("quit".to_string(), "Quit") - .native_image(tauri::NativeImage::StopProgress) - ); + fn create_tray_menu(submenus: Option>) -> Option { + let mut tray_menu = SystemTrayMenu::new().add_item(CustomMenuItem::new("show-window", "Show Cap")); - let tray = SystemTray::new().with_menu(tray_menu).with_menu_on_left_click(false); + if let Some(items) = submenus { + for submenu in items { + tray_menu = tray_menu.add_submenu(submenu); + } + } + + Option::Some(tray_menu.add_native_item(tauri::SystemTrayMenuItem::Separator).add_item(CustomMenuItem::new("quit".to_string(), "Quit"))) + } + let tray = SystemTray::new().with_menu(create_tray_menu(None).expect("Failed to create tray menu")).with_menu_on_left_click(false).with_title("Cap"); tauri::Builder::default() .plugin(tauri_plugin_oauth::init()) @@ -207,15 +206,45 @@ fn main() { let tray_handle = app.tray_handle().clone(); app.listen_global("toggle-recording", move|event| { - let payload_error_msg = format!("Error while deserializing recording state from event payload: {:?}", event.payload()); - let recording_state: Value = serde_json::from_str(event.payload().expect(payload_error_msg.as_str())).unwrap(); + let is_recording: bool = serde_json::from_str(event.payload().expect("Error while openning event payload")).expect("Error while deserializing recording state from event payload"); - if recording_state.as_bool().expect(payload_error_msg.as_str()) { + if is_recording { tray_handle.set_icon(tauri::Icon::Raw(include_bytes!("../icons/tray-stop-icon.png").to_vec())).unwrap(); } else { tray_handle.set_icon(tauri::Icon::Raw(include_bytes!("../icons/tray-default-icon.png").to_vec())).unwrap(); } }); + + let tray_handle = app.tray_handle().clone(); + app.listen_global("input-devices-set", move|event| { + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + struct Device { + device_id: String, + group_id: String, + kind: String, + label: String + } + + let devices: Vec = serde_json::from_str(event.payload().expect("Error wile openning event payload")).expect("Error while deserializing media devices from event payload"); + + let mut tray_vid_items = SystemTrayMenu::new(); + let mut tray_mic_items = SystemTrayMenu::new(); + + for device in devices { + if device.kind == "videoinput" { + tray_vid_items = tray_vid_items.add_item(CustomMenuItem::new(format!("in_vid_{}", device.device_id), device.label)); + } else { + tray_mic_items = tray_mic_items.add_item(CustomMenuItem::new(format!("in_mic_{}", device.device_id), device.label)); + } + } + + let new_menu = create_tray_menu(Some( + vec![SystemTraySubmenu::new("Camera", tray_vid_items), SystemTraySubmenu::new("Microphone", tray_mic_items)]) + ).unwrap(); + + tray_handle.set_menu(new_menu).expect("Error while updating the tray menu items"); + }); Ok(()) }) @@ -245,13 +274,15 @@ fn main() { "quit" => { app.exit(0); } - _ => {} + other => { + println!("Menu Item Clicked: {other}"); + } }, SystemTrayEvent::LeftClick { position: _, size: _, .. } => { - app.emit_all("tray-on-left-click", {}).unwrap(); + app.emit_all("tray-on-left-click", Some(())).unwrap(); }, _ => {} }) .run(tauri::generate_context!()) .expect("Error while running tauri application"); -} \ No newline at end of file +} diff --git a/apps/desktop/src/utils/recording/utils.ts b/apps/desktop/src/utils/recording/utils.ts index 8146290c..3fb29ba3 100644 --- a/apps/desktop/src/utils/recording/utils.ts +++ b/apps/desktop/src/utils/recording/utils.ts @@ -1,5 +1,6 @@ "use client"; +import { emit } from "@tauri-apps/api/event"; import { invoke } from "@tauri-apps/api/tauri"; export const enumerateAndStoreDevices = async () => { @@ -21,6 +22,8 @@ export const enumerateAndStoreDevices = async () => { window.localStorage.setItem("audioDevices", JSON.stringify(audioDevices)); window.localStorage.setItem("videoDevices", JSON.stringify(videoDevices)); + + emit("input-devices-set", [...audioDevices, ...videoDevices]); } }; From 3a5eb8ab4cd810f13676265a325b6dc8eecd7151 Mon Sep 17 00:00:00 2001 From: Ilya <47112191+ItsEeleeya@users.noreply.github.com> Date: Mon, 15 Apr 2024 23:15:10 +0330 Subject: [PATCH 12/16] Make media device selection in tray functional Known issue: Selecting None for video input can result in de-sync and instead select the first real device. --- apps/desktop/src-tauri/src/main.rs | 128 ++++++++++++------ .../src/components/windows/inner/Recorder.tsx | 7 +- .../utils/recording/MediaDeviceContext.tsx | 105 ++++++++++---- apps/desktop/src/utils/recording/utils.ts | 2 - 4 files changed, 168 insertions(+), 74 deletions(-) diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index fd0bdc3c..b5e399ab 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -1,13 +1,14 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use std::collections::LinkedList; use std::sync::{Arc}; use std::path::PathBuf; -use serde::Deserialize; -use serde_json::Value; +use cpal::Devices; +use regex::Regex; use tokio::sync::Mutex; use std::sync::atomic::{AtomicBool}; use std::{vec}; -use tauri::{command, CustomMenuItem, Manager, Menu, MenuItem, Submenu, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem, SystemTraySubmenu, Window}; +use tauri::{command, CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTraySubmenu, Window}; use window_vibrancy::{apply_blur, apply_vibrancy, NSVisualEffectMaterial}; use window_shadows::set_shadow; use tauri_plugin_positioner::{WindowExt, Position}; @@ -158,19 +159,42 @@ fn main() { (0, 0) }; - fn create_tray_menu(submenus: Option>) -> Option { - let mut tray_menu = SystemTrayMenu::new().add_item(CustomMenuItem::new("show-window", "Show Cap")); + #[derive(serde::Deserialize, PartialEq)] + enum DeviceKind { + #[serde(alias="videoinput")] + Video, + #[serde(alias="audioinput")] + Audio + } + + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct MediaDevice { + // index: i8, + #[serde(rename(deserialize="deviceId"))] + id: String, + kind: DeviceKind, + label: String + } + + // let mut media_devices: LinkedList = LinkedList::new(); + + fn create_tray_menu(submenus: Option>) -> SystemTrayMenu { + let mut tray_menu = SystemTrayMenu::new(); if let Some(items) = submenus { for submenu in items { tray_menu = tray_menu.add_submenu(submenu); } + tray_menu = tray_menu.add_native_item(tauri::SystemTrayMenuItem::Separator); } - Option::Some(tray_menu.add_native_item(tauri::SystemTrayMenuItem::Separator).add_item(CustomMenuItem::new("quit".to_string(), "Quit"))) + tray_menu + .add_item(CustomMenuItem::new("show-window".to_string(), "Show Cap")) + .add_item(CustomMenuItem::new("quit".to_string(), "Quit").accelerator("CmdOrControl+Q")) } - let tray = SystemTray::new().with_menu(create_tray_menu(None).expect("Failed to create tray menu")).with_menu_on_left_click(false).with_title("Cap"); + let tray = SystemTray::new().with_menu(create_tray_menu(None)).with_menu_on_left_click(false).with_title("Cap"); tauri::Builder::default() .plugin(tauri_plugin_oauth::init()) @@ -204,44 +228,53 @@ fn main() { app.manage(Arc::new(Mutex::new(recording_state))); - let tray_handle = app.tray_handle().clone(); + let tray_handle = app.tray_handle(); app.listen_global("toggle-recording", move|event| { let is_recording: bool = serde_json::from_str(event.payload().expect("Error while openning event payload")).expect("Error while deserializing recording state from event payload"); - if is_recording { - tray_handle.set_icon(tauri::Icon::Raw(include_bytes!("../icons/tray-stop-icon.png").to_vec())).unwrap(); + let icon_bytes = if is_recording { + include_bytes!("../icons/tray-stop-icon.png").to_vec() } else { - tray_handle.set_icon(tauri::Icon::Raw(include_bytes!("../icons/tray-default-icon.png").to_vec())).unwrap(); - } + include_bytes!("../icons/tray-default-icon.png").to_vec() + }; + + tray_handle.set_icon(tauri::Icon::Raw(icon_bytes)).expect("Error while setting tray icon"); }); - let tray_handle = app.tray_handle().clone(); - app.listen_global("input-devices-set", move|event| { - #[derive(Debug, Deserialize)] + let tray_handle = app.tray_handle(); + app.listen_global("media-devices-set", move|event| { + #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] - struct Device { - device_id: String, - group_id: String, - kind: String, - label: String + struct Payload { + media_devices: Vec, + selected_video: MediaDevice, + selected_audio: MediaDevice } - - let devices: Vec = serde_json::from_str(event.payload().expect("Error wile openning event payload")).expect("Error while deserializing media devices from event payload"); - - let mut tray_vid_items = SystemTrayMenu::new(); - let mut tray_mic_items = SystemTrayMenu::new(); - - for device in devices { - if device.kind == "videoinput" { - tray_vid_items = tray_vid_items.add_item(CustomMenuItem::new(format!("in_vid_{}", device.device_id), device.label)); - } else { - tray_mic_items = tray_mic_items.add_item(CustomMenuItem::new(format!("in_mic_{}", device.device_id), device.label)); - } + let payload: Payload = serde_json::from_str(event.payload().expect("Error wile openning event payload")).expect("Error while deserializing media devices from event payload"); + + fn create_submenu_items(devices: &Vec, selected_device: &MediaDevice, kind: DeviceKind) -> SystemTrayMenu { + devices + .iter() + .filter(|device| device.kind == kind) + .fold(SystemTrayMenu::new(), |mut tray_items, device| { + let item_id_prefix = if kind == DeviceKind::Video { "video" } else { "audio" }; + let mut menu_item = CustomMenuItem::new(format!("in_{}_{}", item_id_prefix, device.id), &device.label); + + if selected_device.label == device.label { + menu_item = menu_item.selected(); + } + + tray_items = tray_items.add_item(menu_item); + tray_items + }) } let new_menu = create_tray_menu(Some( - vec![SystemTraySubmenu::new("Camera", tray_vid_items), SystemTraySubmenu::new("Microphone", tray_mic_items)]) - ).unwrap(); + vec![ + SystemTraySubmenu::new("Camera", create_submenu_items(&payload.media_devices, &payload.selected_video, DeviceKind::Video)), + SystemTraySubmenu::new("Microphone", create_submenu_items(&payload.media_devices, &payload.selected_audio, DeviceKind::Audio)) + ] + )); tray_handle.set_menu(new_menu).expect("Error while updating the tray menu items"); }); @@ -268,18 +301,35 @@ fn main() { SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() { "show-window" => { let window = app.get_window("main").expect("Error while trying to get the main window."); - window.show().unwrap(); - window.set_focus().unwrap(); + window.show().expect("Error while trying to show main window"); + window.set_focus().expect("Error while trying to set focus on main window"); } "quit" => { app.exit(0); } - other => { - println!("Menu Item Clicked: {other}"); + item_id => { + let pattern = Regex::new(r"^in_(video|audio)_").unwrap(); + + if pattern.is_match(item_id) { + let device_id = pattern.replace_all(item_id, "").into_owned(); + let kind = if item_id.contains("video") { + "videoinput" + } else { + "audioinput" + }; + #[derive(Clone, serde::Serialize)] + struct Payload { + #[serde(rename(serialize="type"))] + device_type: String, + id: String + } + + app.emit_all("tray_set_device", Payload { device_type: kind.to_string(), id: device_id }).expect("Failed to emit tray set media device event to windows"); + } } }, SystemTrayEvent::LeftClick { position: _, size: _, .. } => { - app.emit_all("tray-on-left-click", Some(())).unwrap(); + app.emit_all("tray-on-left-click", Some(())).expect("Failed to emit tray left click event to windows"); }, _ => {} }) diff --git a/apps/desktop/src/components/windows/inner/Recorder.tsx b/apps/desktop/src/components/windows/inner/Recorder.tsx index 1c90e4cc..b3f4b5b8 100644 --- a/apps/desktop/src/components/windows/inner/Recorder.tsx +++ b/apps/desktop/src/components/windows/inner/Recorder.tsx @@ -75,8 +75,8 @@ export const Recorder = () => { await emit("change-device", { type: option, device: { - label: "None", index: -1, + label: "None", kind: option === "video" ? "videoinput" : "audioinput", }, }); @@ -140,7 +140,8 @@ export const Recorder = () => { useEffect(() => { let unlistenFn: UnlistenFn | null = null; - const registerListener = async () => { + + const setupListener = async () => { unlistenFn = await listen("tray-on-left-click", (_) => { if (isRecording) { handleStopAllRecordings(); @@ -154,7 +155,7 @@ export const Recorder = () => { }); }; - registerListener(); + setupListener(); return () => { if (unlistenFn) { diff --git a/apps/desktop/src/utils/recording/MediaDeviceContext.tsx b/apps/desktop/src/utils/recording/MediaDeviceContext.tsx index 8c721899..d6785119 100644 --- a/apps/desktop/src/utils/recording/MediaDeviceContext.tsx +++ b/apps/desktop/src/utils/recording/MediaDeviceContext.tsx @@ -8,7 +8,7 @@ import { useCallback, useRef, } from "react"; -import { listen } from "@tauri-apps/api/event"; +import { emit, listen } from "@tauri-apps/api/event"; import { getLocalDevices, enumerateAndStoreDevices, @@ -106,41 +106,48 @@ export const MediaDeviceProvider: React.FC> = ({ } }, []); + const changeDevice = (type: "video" | "audio", device: Devices) => { + console.log(`Change-device recieved, previous selected: audio: ${selectedAudioDevice?.index} vid: ${selectedVideoDevice?.index}`); + + if (type && device) { + if (window.fathom !== undefined) { + window.fathom.trackEvent(`${type}_device_change`); + } + if (type === "video") { + import("@tauri-apps/api/window").then(({ WebviewWindow }) => { + if (WebviewWindow.getByLabel("camera")) { + WebviewWindow.getByLabel("camera").close(); + } else { + initializeCameraWindow(); + } + }); + if (selectedVideoDevice?.index !== device.index) { + setSelectedVideoDevice(device); + } + } + + if (type === "audio") { + if (selectedAudioDevice?.index !== device.index) { + setSelectedAudioDevice(device); + } + } + } + } + useEffect(() => { - let unlistenFn: any; + let unlistenChangeDevice: any; + let unlistenTraySetDevice: any; - const setupListener = async () => { + const setupChangeDeviceListener = async () => { try { - unlistenFn = await listen( + unlistenChangeDevice = await listen( "change-device", ({ payload, }: { payload: { type: "video" | "audio"; device: Devices }; }) => { - if (payload && payload.device) { - if (window.fathom !== undefined) { - window.fathom.trackEvent(`${payload.type}_device_change`); - } - if (payload.type === "video") { - import("@tauri-apps/api/window").then(({ WebviewWindow }) => { - if (WebviewWindow.getByLabel("camera")) { - WebviewWindow.getByLabel("camera").close(); - } else { - initializeCameraWindow(); - } - }); - if (selectedVideoDevice?.index !== payload.device.index) { - setSelectedVideoDevice(payload.device); - } - } - - if (payload.type === "audio") { - if (selectedAudioDevice?.index !== payload.device.index) { - setSelectedAudioDevice(payload.device); - } - } - } + changeDevice(payload.type, payload.device); } ); } catch (error) { @@ -148,11 +155,49 @@ export const MediaDeviceProvider: React.FC> = ({ } }; - setupListener(); + const createNonDevice = (kind: "videoinput" | "audioinput") => { + return { + index: -1, + label: "None", + kind: kind, + deviceId: "none", + } as Devices; + } + + const setupTraySetDeviceListener = async () => { + unlistenTraySetDevice = await listen<{type: string, id: string}>("tray_set_device", (event) => { + const id = event.payload.id; + const option = event.payload.type as "videoinput" | "audioinput"; + const newDevice = id === "none" ? createNonDevice(option) : + devices.find((device) => option === device.kind && event.payload.id === device.deviceId); + + console.log(`Trying to set ${newDevice?.label} from ${newDevice?.kind === "videoinput" ? selectedVideoDevice?.label : selectedAudioDevice?.label}`) + + changeDevice(event.payload.type === "videoinput" ? "video" : "audio", newDevice); + }); + }; + + setupChangeDeviceListener(); + setupTraySetDeviceListener(); + + if (devices.length !== 0) { + emit("media-devices-set", { + mediaDevices: [ + createNonDevice("videoinput"), + createNonDevice("audioinput"), + ...(devices as Omit[]) + ], + selectedVideo: selectedVideoDevice?.label === "None" ? createNonDevice("videoinput") : selectedVideoDevice, + selectedAudio: selectedAudioDevice?.label === "None" ? createNonDevice("audioinput") : selectedAudioDevice, + }); + } return () => { - if (unlistenFn) { - unlistenFn(); + if (unlistenChangeDevice) { + unlistenChangeDevice(); + } + if (unlistenTraySetDevice) { + unlistenTraySetDevice(); } }; }, [selectedVideoDevice, selectedAudioDevice]); diff --git a/apps/desktop/src/utils/recording/utils.ts b/apps/desktop/src/utils/recording/utils.ts index 3fb29ba3..cea295f0 100644 --- a/apps/desktop/src/utils/recording/utils.ts +++ b/apps/desktop/src/utils/recording/utils.ts @@ -22,8 +22,6 @@ export const enumerateAndStoreDevices = async () => { window.localStorage.setItem("audioDevices", JSON.stringify(audioDevices)); window.localStorage.setItem("videoDevices", JSON.stringify(videoDevices)); - - emit("input-devices-set", [...audioDevices, ...videoDevices]); } }; From b5f3975c4a4afc72c5e4e84d5a5a54698cc8800f Mon Sep 17 00:00:00 2001 From: Ilya <47112191+ItsEeleeya@users.noreply.github.com> Date: Tue, 16 Apr 2024 14:52:05 +0330 Subject: [PATCH 13/16] Polish, sync frontend picker with tray picker, refactors of relevant areas Frontend media device pickers now sync with the tray device picker. Rename Devices to Device. Rename Device.deviceId to Device.id Refactor Recorder.handleContextClick --- apps/desktop/src-tauri/src/main.rs | 61 ++++---- .../desktop/src/components/windows/Camera.tsx | 2 +- .../src/components/windows/inner/Recorder.tsx | 87 +++++------ .../utils/recording/MediaDeviceContext.tsx | 144 ++++++++---------- apps/desktop/src/utils/recording/utils.ts | 5 +- 5 files changed, 142 insertions(+), 157 deletions(-) diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index b5e399ab..91bb5c6d 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -170,15 +170,11 @@ fn main() { #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] struct MediaDevice { - // index: i8, - #[serde(rename(deserialize="deviceId"))] id: String, kind: DeviceKind, label: String } - // let mut media_devices: LinkedList = LinkedList::new(); - fn create_tray_menu(submenus: Option>) -> SystemTrayMenu { let mut tray_menu = SystemTrayMenu::new(); @@ -247,25 +243,35 @@ fn main() { #[serde(rename_all = "camelCase")] struct Payload { media_devices: Vec, - selected_video: MediaDevice, - selected_audio: MediaDevice + selected_video: Option, + selected_audio: Option } let payload: Payload = serde_json::from_str(event.payload().expect("Error wile openning event payload")).expect("Error while deserializing media devices from event payload"); - fn create_submenu_items(devices: &Vec, selected_device: &MediaDevice, kind: DeviceKind) -> SystemTrayMenu { + fn create_submenu_items(devices: &Vec, selected_device: &Option, kind: DeviceKind) -> SystemTrayMenu { + let id_prefix = if kind == DeviceKind::Video { + "video" + } else { + "audio" + }; + let mut none_item = CustomMenuItem::new(format!("in_{}_none", id_prefix), "None"); + if selected_device.is_none() { + none_item = none_item.selected(); + } + let initial = SystemTrayMenu::new().add_item(none_item); devices .iter() .filter(|device| device.kind == kind) - .fold(SystemTrayMenu::new(), |mut tray_items, device| { - let item_id_prefix = if kind == DeviceKind::Video { "video" } else { "audio" }; - let mut menu_item = CustomMenuItem::new(format!("in_{}_{}", item_id_prefix, device.id), &device.label); - - if selected_device.label == device.label { - menu_item = menu_item.selected(); + .fold(initial, |tray_items, device| { + let mut menu_item = CustomMenuItem::new(format!("in_{}_{}", id_prefix, device.id), &device.label); + + if let Some(selected) = selected_device { + if selected.label == device.label { + menu_item = menu_item.selected(); + } } - - tray_items = tray_items.add_item(menu_item); - tray_items + + tray_items.add_item(menu_item) }) } @@ -308,23 +314,26 @@ fn main() { app.exit(0); } item_id => { - let pattern = Regex::new(r"^in_(video|audio)_").unwrap(); + if !item_id.starts_with("in") { + return; + } + let pattern = Regex::new(r"^in_(video|audio)_").expect("Failed to create regex for checking tray item events"); if pattern.is_match(item_id) { - let device_id = pattern.replace_all(item_id, "").into_owned(); - let kind = if item_id.contains("video") { - "videoinput" - } else { - "audioinput" - }; #[derive(Clone, serde::Serialize)] - struct Payload { + struct SetDevicePayload { #[serde(rename(serialize="type"))] device_type: String, - id: String + id: Option } - app.emit_all("tray_set_device", Payload { device_type: kind.to_string(), id: device_id }).expect("Failed to emit tray set media device event to windows"); + let device_id = pattern.replace_all(item_id, "").into_owned(); + let kind = if item_id.contains("video") { "videoinput" } else { "audioinput" }; + + app.emit_all("tray-set-device-id", SetDevicePayload { + device_type: kind.to_string(), + id: if device_id == "none" { None } else { Some(device_id) } + }).expect("Failed to emit tray set media device event to windows"); } } }, diff --git a/apps/desktop/src/components/windows/Camera.tsx b/apps/desktop/src/components/windows/Camera.tsx index 833d1d2b..a0de0e25 100644 --- a/apps/desktop/src/components/windows/Camera.tsx +++ b/apps/desktop/src/components/windows/Camera.tsx @@ -15,7 +15,7 @@ export const Camera = () => { const video = videoRef.current; const constraints = { video: { - deviceId: selectedVideoDevice.deviceId, + deviceId: selectedVideoDevice.id, }, }; diff --git a/apps/desktop/src/components/windows/inner/Recorder.tsx b/apps/desktop/src/components/windows/inner/Recorder.tsx index b3f4b5b8..33a7dce0 100644 --- a/apps/desktop/src/components/windows/inner/Recorder.tsx +++ b/apps/desktop/src/components/windows/inner/Recorder.tsx @@ -1,7 +1,7 @@ "use client"; import { useState, useEffect } from "react"; -import { useMediaDevices } from "@/utils/recording/MediaDeviceContext"; +import { Device, useMediaDevices } from "@/utils/recording/MediaDeviceContext"; import { Video } from "@/components/icons/Video"; import { Microphone } from "@/components/icons/Microphone"; import { Screen } from "@/components/icons/Screen"; @@ -43,52 +43,49 @@ export const Recorder = () => { const [canStopRecording, setCanStopRecording] = useState(false); const [hasStartedRecording, setHasStartedRecording] = useState(false); - const handleContextClick = async (option: string) => { + const handleContextClick = async (option: "video" | "audio") => { const { showMenu } = await import("tauri-plugin-context-menu"); + const deviceKind = option === "video" ? "videoinput" : "audioinput"; + const isSelected = (device: Device | null) => { + if (device === null) { + return deviceKind === "videoinput" + ? selectedVideoDevice === null + : selectedAudioDevice === null; + } + + return deviceKind === "videoinput" + ? device.index === selectedVideoDevice?.index + : device.index === selectedAudioDevice?.index; + } + const select = async (device: Device | null) => { + // if (isSelected(device)) { + // return + // } + emit("change-device", { type: deviceKind, device: device }).catch((error) => { + console.log("Failed to emit change-device event:", error); + }); + } - const filteredDevices = devices - .filter((device) => - option === "video" - ? device.kind === "videoinput" - : device.kind === "audioinput" - ) - .map((device) => ({ - label: device.label, - disabled: - option === "video" - ? device.index === selectedVideoDevice?.index - : device.index === selectedAudioDevice?.index, - event: async () => { - try { - await emit("change-device", { type: option, device }); - } catch (error) { - console.error("Failed to emit change-device event:", error); - } - }, - })); - - filteredDevices.push({ - label: "None", - disabled: false, - event: async () => { - try { - await emit("change-device", { - type: option, - device: { - index: -1, - label: "None", - kind: option === "video" ? "videoinput" : "audioinput", - }, - }); - } catch (error) { - console.error("Failed to emit change-device event:", error); - } - }, - }); + const devicesOfKind = devices.filter((device) => device.kind === deviceKind); + const menuItems = [ + { + label: "None", + checked: isSelected(null), + event: async() => select(null) + }, + ...devicesOfKind.map((device) => ( + { + label: device.label, + checked: isSelected(device), + event: async() => select(device) + } + )) + ] + await showMenu({ - items: [...filteredDevices], - ...(filteredDevices.length === 0 && { + items: [...menuItems], + ...(devicesOfKind.length === 0 && { items: [ { label: "Nothing found.", @@ -377,7 +374,7 @@ export const Recorder = () => { width="full" handler={() => handleContextClick("video")} icon={