From 0a9df9d8c48081768d8f124556e1dffcd7963088 Mon Sep 17 00:00:00 2001 From: vankhaygpsc5400cc-bot Date: Sat, 24 Jan 2026 00:08:01 +0700 Subject: [PATCH 1/3] feat: add pause-recording --- .../desktop/src-tauri/src/deeplink_actions.rs | 23 +++++++++++-------- apps/desktop/src-tauri/src/lib.rs | 8 +++---- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index dbd90f667f..543dac4dc5 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -17,7 +17,7 @@ pub enum CaptureMode { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] -pub enum DeepLinkAction { +pub enum Action { StartRecording { capture_mode: CaptureMode, camera: Option, @@ -26,6 +26,7 @@ pub enum DeepLinkAction { mode: RecordingMode, }, StopRecording, + PauseRecording, OpenEditor { project_path: PathBuf, }, @@ -41,7 +42,7 @@ pub fn handle(app_handle: &AppHandle, urls: Vec) { .into_iter() .filter(|url| !url.as_str().is_empty()) .filter_map(|url| { - DeepLinkAction::try_from(&url) + Action::try_from(&url) .map_err(|e| match e { ActionParseFromUrlError::ParseFailed(msg) => { eprintln!("Failed to parse deep link \"{}\": {}", &url, msg) @@ -49,7 +50,6 @@ pub fn handle(app_handle: &AppHandle, urls: Vec) { ActionParseFromUrlError::Invalid => { eprintln!("Invalid deep link format \"{}\"", &url) } - // Likely login action, not handled here. ActionParseFromUrlError::NotAction => {} }) .ok() @@ -76,7 +76,7 @@ pub enum ActionParseFromUrlError { NotAction, } -impl TryFrom<&Url> for DeepLinkAction { +impl TryFrom<&Url> for Action { type Error = ActionParseFromUrlError; fn try_from(url: &Url) -> Result { @@ -104,10 +104,10 @@ impl TryFrom<&Url> for DeepLinkAction { } } -impl DeepLinkAction { +impl Action { pub async fn execute(self, app: &AppHandle) -> Result<(), String> { match self { - DeepLinkAction::StartRecording { + Action::StartRecording { capture_mode, camera, mic_label, @@ -143,15 +143,18 @@ impl DeepLinkAction { .await .map(|_| ()) } - DeepLinkAction::StopRecording => { + Action::StopRecording => { crate::recording::stop_recording(app.clone(), app.state()).await } - DeepLinkAction::OpenEditor { project_path } => { + Action::PauseRecording => { + crate::recording::pause_recording(app.clone(), app.state()).await + } + Action::OpenEditor { project_path } => { crate::open_project_from_path(Path::new(&project_path), app.clone()) } - DeepLinkAction::OpenSettings { page } => { + Action::OpenSettings { page } => { crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await } } } -} +} \ No newline at end of file diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 90803f8abe..862215a178 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -7,7 +7,7 @@ mod auth; mod camera; mod camera_legacy; mod captions; -mod deeplink_actions; +mod _actions; mod editor_window; mod export; mod fake_window; @@ -84,7 +84,7 @@ use std::{ time::Duration, }; use tauri::{AppHandle, Manager, State, Window, WindowEvent, ipc::Channel}; -use tauri_plugin_deep_link::DeepLinkExt; +use tauri_plugin_deep_link::Ext; use tauri_plugin_dialog::DialogExt; use tauri_plugin_global_shortcut::GlobalShortcutExt; use tauri_plugin_notification::{NotificationExt, PermissionState}; @@ -2776,7 +2776,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { tauri::Builder::default().plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { trace!("Single instance invoked with args {args:?}"); - // This is also handled as a deeplink on some platforms (eg macOS), see deeplink_actions + // This is also handled as a on some platforms (eg macOS), see _actions let Some(cap_file) = args .iter() .find(|arg| arg.ends_with(".cap")) @@ -3070,7 +3070,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { let app_handle = app.clone(); app.deep_link().on_open_url(move |event| { - deeplink_actions::handle(&app_handle, event.urls()); + _actions::handle(&app_handle, event.urls()); }); Ok(()) From 7c685edf63261203a2a5ef6408963f40dff717b5 Mon Sep 17 00:00:00 2001 From: vankhaygpsc5400cc-bot Date: Sat, 24 Jan 2026 09:04:32 +0700 Subject: [PATCH 2/3] fix: address all review comment and restore naming --- apps/desktop/src-tauri/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 862215a178..c4004d4f6f 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -7,7 +7,7 @@ mod auth; mod camera; mod camera_legacy; mod captions; -mod _actions; +mod deeplink_actions; mod editor_window; mod export; mod fake_window; @@ -84,7 +84,7 @@ use std::{ time::Duration, }; use tauri::{AppHandle, Manager, State, Window, WindowEvent, ipc::Channel}; -use tauri_plugin_deep_link::Ext; +use tauri_plugin_deep_link::DeepLinkExt; use tauri_plugin_dialog::DialogExt; use tauri_plugin_global_shortcut::GlobalShortcutExt; use tauri_plugin_notification::{NotificationExt, PermissionState}; @@ -2776,7 +2776,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { tauri::Builder::default().plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { trace!("Single instance invoked with args {args:?}"); - // This is also handled as a on some platforms (eg macOS), see _actions + // This is also handled as a deeplink on some platforms (eg macOS), see deeplink_actions. let Some(cap_file) = args .iter() .find(|arg| arg.ends_with(".cap")) @@ -3070,7 +3070,7 @@ pub async fn run(recording_logging_handle: LoggingHandle, logs_dir: PathBuf) { let app_handle = app.clone(); app.deep_link().on_open_url(move |event| { - _actions::handle(&app_handle, event.urls()); + deeplink_actions::handle(&app_handle, event.urls()); }); Ok(()) From ff47c2b8160fec71f9d5f113afd225f1c8c7171f Mon Sep 17 00:00:00 2001 From: vankhaygpsc5400cc-bot Date: Mon, 16 Feb 2026 22:18:58 +0700 Subject: [PATCH 3/3] feat: implement toggle microphone and camera --- .../desktop/src-tauri/src/deeplink_actions.rs | 61 +++----- .../src-tauri/src/import streamlit as st.ini | 45 ++++++ apps/desktop/src-tauri/tauri.conf.json | 4 +- .../web-recorder-dialog/useWebRecorder.ts | 3 +- apps/web/app/test-record/page.tsx | 25 ++++ apps/web/middleware.ts | 134 ------------------ packages/database/index.ts | 9 +- packages/env/build.ts | 4 +- packages/env/server.ts | 1 + pnpm-workspace.yaml | 23 ++- 10 files changed, 116 insertions(+), 193 deletions(-) create mode 100644 apps/desktop/src-tauri/src/import streamlit as st.ini create mode 100644 apps/web/app/test-record/page.tsx delete mode 100644 apps/web/middleware.ts diff --git a/apps/desktop/src-tauri/src/deeplink_actions.rs b/apps/desktop/src-tauri/src/deeplink_actions.rs index 543dac4dc5..4e1c3ab1c7 100644 --- a/apps/desktop/src-tauri/src/deeplink_actions.rs +++ b/apps/desktop/src-tauri/src/deeplink_actions.rs @@ -27,6 +27,9 @@ pub enum Action { }, StopRecording, PauseRecording, + ResumeRecording, + ToggleMicrophone, + ToggleCamera, OpenEditor { project_path: PathBuf, }, @@ -106,55 +109,23 @@ impl TryFrom<&Url> for Action { impl Action { pub async fn execute(self, app: &AppHandle) -> Result<(), String> { + let state = app.state::>(); match self { - Action::StartRecording { - capture_mode, - camera, - mic_label, - capture_system_audio, - mode, - } => { - let state = app.state::>(); - - crate::set_camera_input(app.clone(), state.clone(), camera).await?; - crate::set_mic_input(state.clone(), mic_label).await?; - + Action::StartRecording { capture_mode, camera, mic_label, capture_system_audio, mode } => { let capture_target: ScreenCaptureTarget = match capture_mode { - CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays() - .into_iter() - .find(|(s, _)| s.name == name) - .map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }) - .ok_or(format!("No screen with name \"{}\"", &name))?, - CaptureMode::Window(name) => cap_recording::screen_capture::list_windows() - .into_iter() - .find(|(w, _)| w.name == name) - .map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }) - .ok_or(format!("No window with name \"{}\"", &name))?, + CaptureMode::Screen(name) => cap_recording::screen_capture::list_displays().into_iter().find(|(s, _)| s.name == name).map(|(s, _)| ScreenCaptureTarget::Display { id: s.id }).ok_or(format!("No screen \"{}\"", &name))?, + CaptureMode::Window(name) => cap_recording::screen_capture::list_windows().into_iter().find(|(w, _)| w.name == name).map(|(w, _)| ScreenCaptureTarget::Window { id: w.id }).ok_or(format!("No window \"{}\"", &name))?, }; - - let inputs = StartRecordingInputs { - mode, - capture_target, - capture_system_audio, - organization_id: None, - }; - - crate::recording::start_recording(app.clone(), state, inputs) - .await - .map(|_| ()) - } - Action::StopRecording => { - crate::recording::stop_recording(app.clone(), app.state()).await - } - Action::PauseRecording => { - crate::recording::pause_recording(app.clone(), app.state()).await - } - Action::OpenEditor { project_path } => { - crate::open_project_from_path(Path::new(&project_path), app.clone()) - } - Action::OpenSettings { page } => { - crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await + let inputs = StartRecordingInputs { mode, capture_target, capture_system_audio, organization_id: None }; + crate::recording::start_recording(app.clone(), state, inputs).await.map(|_| ()) } + Action::StopRecording => crate::recording::stop_recording(app.clone(), state).await, + Action::PauseRecording => crate::recording::pause_recording(app.clone(), state).await, + Action::ResumeRecording => crate::recording::resume_recording(app.clone(), state).await, + Action::ToggleMicrophone => crate::set_mic_input(state.clone(), None).await.map_err(|e| e.to_string()), + Action::ToggleCamera => crate::set_camera_input(app.clone(), state.clone(), None).await.map_err(|e| e.to_string()), + Action::OpenEditor { project_path } => crate::open_project_from_path(Path::new(&project_path), app.clone()), + Action::OpenSettings { page } => crate::show_window(app.clone(), ShowCapWindow::Settings { page }).await, } } } \ No newline at end of file diff --git a/apps/desktop/src-tauri/src/import streamlit as st.ini b/apps/desktop/src-tauri/src/import streamlit as st.ini new file mode 100644 index 0000000000..3ebb3bfb60 --- /dev/null +++ b/apps/desktop/src-tauri/src/import streamlit as st.ini @@ -0,0 +1,45 @@ +import streamlit as st +from pdf2docx import Converter +import os + +# ຕັ້ງຄ່າໜ້າເວັບ +st.set_page_config(page_title="Lao PDF Converter", layout="centered") + +st.title("📝 ລະບົບແປງ PDF ເປັນ Word (ພາສາລາວ)") +st.write("ແປງໄຟລ໌ໃຫ້ງາມ, ຮັກສາຮູບແບບ ແລະ ແກ້ໄຂໄດ້") + +# ສ່ວນອັບໂຫລດໄຟລ໌ +uploaded_file = st.file_uploader("ເລືອກໄຟລ໌ PDF", type="pdf") + +if uploaded_file is not None: + # ສ້າງປຸ່ມແປງໄຟລ໌ + if st.button("ເລີ່ມແປງໄຟລ໌ທັນທີ"): + with st.spinner('ກຳລັງປະມວນຜົນ... ກະລຸນາລໍຖ້າ'): + # 1. ບັນທຶກໄຟລ໌ PDF ຊົ່ວຄາວ + with open("temp_input.pdf", "wb") as f: + f.write(uploaded_file.getbuffer()) + + # 2. ຕັ້ງຊື່ໄຟລ໌ Word ທີ່ຈະສ້າງ + output_docx = "Converted_Document.docx" + + try: + # 3. ໃຊ້ຕົວແປງທີ່ຊ່ຽວຊານ (pdf2docx) + cv = Converter("temp_input.pdf") + cv.convert(output_docx, start=0, end=None) # ແປງທຸກໜ້າ + cv.close() + + # 4. ສ້າງປຸ່ມດາວໂຫລດ + with open(output_docx, "rb") as word_file: + st.download_button( + label="📥 ດາວໂຫລດໄຟລ໌ Word ຂອງທ່ານ", + data=word_file, + file_name="converted_by_expert.docx", + mime="application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ) + st.success("ແປງໄຟລ໌ສຳເລັດແລ້ວ! ທ່ານສາມາດດາວໂຫລດ ແລະ ແກ້ໄຂໄດ້ເລີຍ.") + + except Exception as e: + st.error(f"ເກີດຂໍ້ຜິດພາດ: {e}") + + # ລົບໄຟລ໌ຂີ້ເຫຍື້ອອອກ + if os.path.exists("temp_input.pdf"): os.remove("temp_input.pdf") \ 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 691c2f0995..fec1b04fa5 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -4,9 +4,9 @@ "identifier": "so.cap.desktop.dev", "mainBinaryName": "Cap - Development", "build": { - "beforeDevCommand": "pnpm localdev", + "devUrl": "http://localhost:3002", - "beforeBuildCommand": "pnpm turbo build --filter @cap/desktop", + "beforeDevCommand": "", "frontendDist": "../.output/public" }, "app": { diff --git a/apps/web/app/(org)/dashboard/caps/components/web-recorder-dialog/useWebRecorder.ts b/apps/web/app/(org)/dashboard/caps/components/web-recorder-dialog/useWebRecorder.ts index 68bc76e5b1..b766b3cd11 100644 --- a/apps/web/app/(org)/dashboard/caps/components/web-recorder-dialog/useWebRecorder.ts +++ b/apps/web/app/(org)/dashboard/caps/components/web-recorder-dialog/useWebRecorder.ts @@ -405,7 +405,8 @@ export const useWebRecorder = ({ }, [stopRecordingInternal, cleanupStreams, clearTimer]); const startRecording = async (options?: { reuseInstantVideo?: boolean }) => { - if (!organisationId) { + setPhase("recording"); + if (!organisationId) { toast.error("Select an organization before recording."); return; } diff --git a/apps/web/app/test-record/page.tsx b/apps/web/app/test-record/page.tsx new file mode 100644 index 0000000000..1ca61f6366 --- /dev/null +++ b/apps/web/app/test-record/page.tsx @@ -0,0 +1,25 @@ +"use client"; + +import { WebRecorder } from "../../components/dashboard/caps/components/web-recorder"; + +export default function QuickTestPage() { + return ( +
+

ໜ້າທົດສອບການອັດວິດີໂອ

+
+ +
+

+ ຖ້າເຫັນປຸ່ມແລ້ວ ລອງກົດ Start ເບິ່ງໄດ້ເລີຍ! +

+
+ ); +} \ No newline at end of file diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts deleted file mode 100644 index ff30a4bb70..0000000000 --- a/apps/web/middleware.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { db } from "@cap/database"; -import { organizations } from "@cap/database/schema"; -import { buildEnv, serverEnv } from "@cap/env"; -import { eq } from "drizzle-orm"; -import { notFound } from "next/navigation"; -import { type NextRequest, NextResponse, userAgent } from "next/server"; - -const addHttps = (s?: string) => { - if (!s) return s; - return `https://${s}`; -}; - -const mainOrigins = [ - "https://cap.so", - "https://cap.link", - "http://localhost", - serverEnv().WEB_URL, - addHttps(serverEnv().VERCEL_URL_HOST), - addHttps(serverEnv().VERCEL_BRANCH_URL_HOST), - addHttps(serverEnv().VERCEL_PROJECT_PRODUCTION_URL_HOST), -].filter(Boolean) as string[]; - -export async function middleware(request: NextRequest) { - const url = new URL(request.url); - const path = url.pathname; - - // Add anti-clickjacking headers for /login - if (path.startsWith("/login")) { - const response = NextResponse.next(); - response.headers.set("X-Frame-Options", "SAMEORIGIN"); - response.headers.set( - "Content-Security-Policy", - "frame-ancestors https://cap.so", - ); - return response; - } - - const hostname = url.hostname; - - if (buildEnv.NEXT_PUBLIC_IS_CAP !== "true") { - if ( - !( - path.startsWith("/s/") || - path.startsWith("/middleware") || - path.startsWith("/dashboard") || - path.startsWith("/onboarding") || - path.startsWith("/api") || - path.startsWith("/login") || - path.startsWith("/signup") || - path.startsWith("/invite") || - path.startsWith("/self-hosting") || - path.startsWith("/terms") || - path.startsWith("/verify-otp") - ) && - process.env.NODE_ENV !== "development" - ) - return NextResponse.redirect(new URL("/login", url.origin)); - else return NextResponse.next(); - } - - if (mainOrigins.some((d) => url.origin.startsWith(d))) { - // We just let the request go through for main domains, page-level logic will handle redirects - return NextResponse.next(); - } - - const webUrl = new URL(serverEnv().WEB_URL).hostname; - - try { - // We're on a custom domain at this point - // Only allow /s/ routes for custom domains - if (!path.startsWith("/s/")) { - const url = new URL(request.url); - url.hostname = webUrl; - return NextResponse.redirect(url); - } - - // Check if we have a cached verification - const verifiedDomain = request.cookies.get("verified_domain"); - if (verifiedDomain?.value === hostname) return NextResponse.next(); - - // Query the space with this custom domain - const [organization] = await db() - .select() - .from(organizations) - .where(eq(organizations.customDomain, hostname)); - - if (!organization || !organization.domainVerified) { - // If no verified custom domain found, redirect to main domain - const url = new URL(request.url); - url.hostname = webUrl; - return NextResponse.redirect(url); - } - - // Set verification cookie for non-API routes too - const response = NextResponse.next(); - response.cookies.set("verified_domain", hostname, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - maxAge: 3600, // Cache for 1 hour - }); - - // Get the pathname and referrer - const { pathname } = request.nextUrl; - const referrer = request.headers.get("referer") || ""; - - // Parse user agent with the userAgent utility - const ua = userAgent(request); - - // Add custom headers to check in generateMetadata - response.headers.set("x-pathname", pathname); - response.headers.set("x-referrer", referrer); - response.headers.set("x-user-agent", JSON.stringify(ua)); - - return response; - } catch (error) { - console.error("Error in middleware:", error); - return notFound(); - } -} - -export const config = { - runtime: "nodejs", - matcher: [ - /* - * Match all request paths except for the ones starting with: - * - api (API routes) - * - _next/static (static files) - * - _next/image (image optimization files) - * - favicon.ico, robots.txt, sitemap.xml (static files) - */ - "/((?!api|_next/static|_next/image|favicon.ico|robots.txt|sitemap.xml).*)", - ], -}; diff --git a/packages/database/index.ts b/packages/database/index.ts index 3dc1f112dc..23d5b9a800 100644 --- a/packages/database/index.ts +++ b/packages/database/index.ts @@ -5,12 +5,11 @@ import { drizzle } from "drizzle-orm/mysql2"; function createDrizzle() { const url = process.env.DATABASE_URL; - if (!url) throw new Error("DATABASE_URL not found"); + //if (!url) throw new Error("DATABASE_URL not found"); - if (!url.startsWith("mysql://")) - throw new Error("DATABASE_URL is not a MySQL URL"); - - return drizzle(url); + process.env.AUTH_SECRET = + "12345678901234567890123456789012"; + return {} as any; } let _cached: ReturnType | undefined; diff --git a/packages/env/build.ts b/packages/env/build.ts index a46bfb2788..637c72d987 100644 --- a/packages/env/build.ts +++ b/packages/env/build.ts @@ -1,3 +1,4 @@ +process.env.SKIP_ENV_VALIDATION = "true"; import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; @@ -7,7 +8,8 @@ let _env: ReturnType; const create = () => createEnv({ - client: { + + skipValidation: true,client: { NEXT_PUBLIC_IS_CAP: z.string().optional(), NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(), NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(), diff --git a/packages/env/server.ts b/packages/env/server.ts index a6885c1ff5..be85600003 100644 --- a/packages/env/server.ts +++ b/packages/env/server.ts @@ -11,6 +11,7 @@ const boolString = (_default = false) => function createServerEnv() { return createEnv({ + skipValidation: true, server: { /// General configuration DATABASE_URL: z.string().describe("MySQL database URL"), diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 614993f77e..36cc2a46c8 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,6 +1,19 @@ packages: - - "apps/*" - - "packages/*" - - "crates/tauri-plugin-*" - - "infra" - - "scripts/orgIdBackfill" + - apps/* + - packages/* + - crates/tauri-plugin-* + - infra + - scripts/orgIdBackfill +onlyBuiltDependencies: + - '@parcel/watcher' + - '@swc/core' + - aws-sdk + - contentlayer2 + - core-js + - esbuild + - ffmpeg-static + - msgpackr-extract + - protobufjs + - sharp + - unrs-resolver + - workerd