Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 22 additions & 48 deletions apps/desktop/src-tauri/src/deeplink_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<DeviceOrModelID>,
Expand All @@ -26,6 +26,10 @@ pub enum DeepLinkAction {
mode: RecordingMode,
},
StopRecording,
PauseRecording,
ResumeRecording,
ToggleMicrophone,
ToggleCamera,
OpenEditor {
project_path: PathBuf,
},
Expand All @@ -41,15 +45,14 @@ pub fn handle(app_handle: &AppHandle, urls: Vec<Url>) {
.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)
}
ActionParseFromUrlError::Invalid => {
eprintln!("Invalid deep link format \"{}\"", &url)
}
// Likely login action, not handled here.
ActionParseFromUrlError::NotAction => {}
})
.ok()
Expand All @@ -76,7 +79,7 @@ pub enum ActionParseFromUrlError {
NotAction,
}

impl TryFrom<&Url> for DeepLinkAction {
impl TryFrom<&Url> for Action {
type Error = ActionParseFromUrlError;

fn try_from(url: &Url) -> Result<Self, Self::Error> {
Expand Down Expand Up @@ -104,54 +107,25 @@ impl TryFrom<&Url> for DeepLinkAction {
}
}

impl DeepLinkAction {
impl Action {
pub async fn execute(self, app: &AppHandle) -> Result<(), String> {
let state = app.state::<ArcLock<App>>();
match self {
DeepLinkAction::StartRecording {
capture_mode,
camera,
mic_label,
capture_system_audio,
mode,
} => {
let state = app.state::<ArcLock<App>>();

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 } => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This drops the existing camera/mic selection before starting the recording (it used to call set_camera_input / set_mic_input). If deep links still accept camera/mic_label, that looks like a behavior regression.

Suggested change
Action::StartRecording { capture_mode, camera, mic_label, capture_system_audio, mode } => {
Action::StartRecording { capture_mode, camera, mic_label, capture_system_audio, mode } => {
crate::set_camera_input(app.clone(), state.clone(), camera).await?;
crate::set_mic_input(state.clone(), mic_label).await?;
let capture_target: ScreenCaptureTarget = match capture_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(|_| ())
}
DeepLinkAction::StopRecording => {
crate::recording::stop_recording(app.clone(), app.state()).await
}
DeepLinkAction::OpenEditor { project_path } => {
crate::open_project_from_path(Path::new(&project_path), app.clone())
}
DeepLinkAction::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()),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: ToggleMicrophone / ToggleCamera always pass None (so it’s more like “clear” than toggle). If the intent is real toggling, it probably needs to read current state and flip it.

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,
}
}
}
}
45 changes: 45 additions & 0 deletions apps/desktop/src-tauri/src/import streamlit as st.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import streamlit as st
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file looks unrelated to the desktop app (Streamlit/Python) and it’s under the Rust source tree with a spacey filename. Probably should be removed from the PR.

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")
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 deeplink on some platforms (eg macOS), see deeplink_actions.
let Some(cap_file) = args
.iter()
.find(|arg| arg.ends_with(".cap"))
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
"identifier": "so.cap.desktop.dev",
"mainBinaryName": "Cap - Development",
"build": {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blanking beforeDevCommand and dropping beforeBuildCommand will change the desktop dev/build flow. If this was accidental, restoring the previous build config keeps localdev + turbo build wired.

Suggested change
"build": {
"build": {
"beforeDevCommand": "pnpm localdev",
"devUrl": "http://localhost:3002",
"beforeBuildCommand": "pnpm turbo build --filter @cap/desktop",
"frontendDist": "../.output/public"
},

"beforeDevCommand": "pnpm localdev",

"devUrl": "http://localhost:3002",
"beforeBuildCommand": "pnpm turbo build --filter @cap/desktop",
"beforeDevCommand": "",
"frontendDist": "../.output/public"
},
"app": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,8 @@ export const useWebRecorder = ({
}, [stopRecordingInternal, cleanupStreams, clearTimer]);

const startRecording = async (options?: { reuseInstantVideo?: boolean }) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setPhase("recording") happens before the validation early-returns, so the UI can enter the recording state even when we immediately bail.

Suggested change
const startRecording = async (options?: { reuseInstantVideo?: boolean }) => {
const startRecording = async (options?: { reuseInstantVideo?: boolean }) => {
if (!organisationId) {
toast.error("Select an organization before recording.");
return;
}
setPhase("recording");

if (!organisationId) {
setPhase("recording");
if (!organisationId) {
toast.error("Select an organization before recording.");
return;
}
Expand Down
25 changes: 25 additions & 0 deletions apps/web/app/test-record/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use client";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This introduces a new /test-record route that mounts WebRecorder. If this is a dev-only page, it should be gated (or removed) so it can’t ship in production builds.


import { WebRecorder } from "../../components/dashboard/caps/components/web-recorder";

export default function QuickTestPage() {
return (
<div style={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
backgroundColor: '#000',
color: 'white'
}}>
<h1 style={{ marginBottom: '20px' }}>ໜ້າທົດສອບການອັດວິດີໂອ</h1>
<div style={{ border: '2px solid #333', padding: '40px', borderRadius: '20px', background: '#111' }}>
<WebRecorder />
</div>
<p style={{ marginTop: '20px', color: '#aaa' }}>
ຖ້າເຫັນປຸ່ມແລ້ວ ລອງກົດ Start ເບິ່ງໄດ້ເລີຍ!
</p>
</div>
);
}
134 changes: 0 additions & 134 deletions apps/web/middleware.ts

This file was deleted.

9 changes: 4 additions & 5 deletions packages/database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import { drizzle } from "drizzle-orm/mysql2";

function createDrizzle() {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hard-coding AUTH_SECRET and returning {} here breaks db() and also risks shipping a static secret.

Suggested change
function createDrizzle() {
function createDrizzle() {
const url = process.env.DATABASE_URL;
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);
}

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<typeof createDrizzle> | undefined;
Expand Down
4 changes: 3 additions & 1 deletion packages/env/build.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
process.env.SKIP_ENV_VALIDATION = "true";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting SKIP_ENV_VALIDATION (and skipValidation) disables env checks for builds. If this was for local dev, it’s safer to keep validation on and fix the env input.

Suggested change
process.env.SKIP_ENV_VALIDATION = "true";
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";

Expand All @@ -7,7 +8,8 @@ let _env: ReturnType<typeof create>;

const create = () =>
createEnv({
client: {

skipValidation: true,client: {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it accidentally inlined two properties and turns off validation.

Suggested change
skipValidation: true,client: {
client: {

NEXT_PUBLIC_IS_CAP: z.string().optional(),
NEXT_PUBLIC_POSTHOG_KEY: z.string().optional(),
NEXT_PUBLIC_POSTHOG_HOST: z.string().optional(),
Expand Down
1 change: 1 addition & 0 deletions packages/env/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const boolString = (_default = false) =>

function createServerEnv() {
return createEnv({
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skipValidation: true on server env will let missing/invalid secrets through and fail later in less obvious ways.

Suggested change
return createEnv({
return createEnv({
server: {

skipValidation: true,
server: {
/// General configuration
DATABASE_URL: z.string().describe("MySQL database URL"),
Expand Down
Loading