Skip to content

Commit 6c63bdf

Browse files
JunaYaclaude
andcommitted
feat: implement all 7 specs — capture engine, editor, history, OCR, sharing, recording, settings
Spec 01 - Capture Engine: add delay, current screen, last region modes; return rich CaptureResult metadata from all capture commands. Spec 02 - Editor: Fabric.js-based annotation editor with 11 tools (arrow, rect, ellipse, line, freehand, highlight, text, step numbers, blur, pixelate), style system, undo/redo, export. Spec 03 - History & Workflow: history metadata API with search, bulk delete; workflow rule engine with conditions and action chains. Spec 04 - OCR & Privacy: macOS Vision framework OCR (en/zh/ja/ko); regex-based sensitive info detection (email, phone, API key, JWT, IP). Spec 05 - Sharing & Export: image format conversion (PNG/JPG/WebP), image info metadata, clipboard improvements. Spec 06 - Recording: start/stop screen recording via macOS screencapture, ffmpeg-based GIF conversion, thread-safe recording state. Spec 07 - Settings & Platform: capture settings, shortcut editor with record-and-set UI, history retention, platform capability detection, diagnostics bundle. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3289f17 commit 6c63bdf

25 files changed

Lines changed: 3389 additions & 250 deletions

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@tauri-apps/plugin-store": "^2.4.3",
2525
"@tauri-apps/plugin-window-state": "^2.4.1",
2626
"@vueuse/core": "^14.3.0",
27+
"fabric": "^6.9.1",
2728
"vue": "^3.5.34"
2829
},
2930
"devDependencies": {

pnpm-lock.yaml

Lines changed: 1042 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.lock

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src-tauri/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ core-foundation = "0.10"
4949
libc = "0.2"
5050
base64 = "0.22"
5151
image_compressor = "1.5"
52+
regex-lite = "0.1"
53+
os_info = "3"
5254

5355
[features]
5456
default = [ "custom-protocol" ]

src-tauri/src/cmd/diagnostics.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
use serde::{Deserialize, Serialize};
2+
use tracing::info;
3+
4+
#[derive(Debug, Clone, Serialize, Deserialize)]
5+
pub struct PlatformCapability {
6+
pub key: String,
7+
pub status: String,
8+
pub reason: Option<String>,
9+
}
10+
11+
#[derive(Debug, Clone, Serialize, Deserialize)]
12+
pub struct DiagnosticsBundle {
13+
pub app_version: String,
14+
pub os: String,
15+
pub os_version: String,
16+
pub arch: String,
17+
pub capabilities: Vec<PlatformCapability>,
18+
pub permissions: Vec<PlatformCapability>,
19+
}
20+
21+
#[tauri::command]
22+
pub fn get_platform_capabilities() -> Vec<PlatformCapability> {
23+
let mut caps = Vec::new();
24+
25+
#[cfg(target_os = "macos")]
26+
{
27+
caps.push(PlatformCapability {
28+
key: "regionCapture".to_string(),
29+
status: "available".to_string(),
30+
reason: None,
31+
});
32+
caps.push(PlatformCapability {
33+
key: "windowCapture".to_string(),
34+
status: "available".to_string(),
35+
reason: None,
36+
});
37+
caps.push(PlatformCapability {
38+
key: "fullScreenCapture".to_string(),
39+
status: "available".to_string(),
40+
reason: None,
41+
});
42+
caps.push(PlatformCapability {
43+
key: "screenRecording".to_string(),
44+
status: "available".to_string(),
45+
reason: None,
46+
});
47+
caps.push(PlatformCapability {
48+
key: "globalShortcuts".to_string(),
49+
status: "available".to_string(),
50+
reason: None,
51+
});
52+
caps.push(PlatformCapability {
53+
key: "ocr".to_string(),
54+
status: "available".to_string(),
55+
reason: Some("macOS Vision framework".to_string()),
56+
});
57+
caps.push(PlatformCapability {
58+
key: "systemAudio".to_string(),
59+
status: "limited".to_string(),
60+
reason: Some("Requires additional setup on macOS".to_string()),
61+
});
62+
caps.push(PlatformCapability {
63+
key: "scrollingCapture".to_string(),
64+
status: "unavailable".to_string(),
65+
reason: Some("Not yet implemented".to_string()),
66+
});
67+
}
68+
69+
#[cfg(target_os = "linux")]
70+
{
71+
let is_wayland = std::env::var("WAYLAND_DISPLAY").is_ok();
72+
let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
73+
74+
caps.push(PlatformCapability {
75+
key: "regionCapture".to_string(),
76+
status: if is_wayland { "limited" } else { "available" }.to_string(),
77+
reason: if is_wayland { Some("Uses xdg-desktop-portal on Wayland".to_string()) } else { None },
78+
});
79+
caps.push(PlatformCapability {
80+
key: "globalShortcuts".to_string(),
81+
status: if is_wayland { "unavailable" } else { "available" }.to_string(),
82+
reason: if is_wayland { Some("Wayland restricts global shortcuts".to_string()) } else { None },
83+
});
84+
caps.push(PlatformCapability {
85+
key: "desktopEnvironment".to_string(),
86+
status: "available".to_string(),
87+
reason: Some(desktop),
88+
});
89+
}
90+
91+
#[cfg(target_os = "windows")]
92+
{
93+
caps.push(PlatformCapability {
94+
key: "regionCapture".to_string(),
95+
status: "available".to_string(),
96+
reason: None,
97+
});
98+
caps.push(PlatformCapability {
99+
key: "globalShortcuts".to_string(),
100+
status: "available".to_string(),
101+
reason: None,
102+
});
103+
}
104+
105+
caps
106+
}
107+
108+
#[tauri::command]
109+
pub fn get_diagnostics_bundle(app_handle: tauri::AppHandle) -> DiagnosticsBundle {
110+
let version = app_handle.config().version.clone().unwrap_or_else(|| "unknown".to_string());
111+
112+
DiagnosticsBundle {
113+
app_version: version,
114+
os: std::env::consts::OS.to_string(),
115+
os_version: os_info::get().version().to_string(),
116+
arch: std::env::consts::ARCH.to_string(),
117+
capabilities: get_platform_capabilities(),
118+
permissions: get_permission_status(),
119+
}
120+
}
121+
122+
fn get_permission_status() -> Vec<PlatformCapability> {
123+
let mut perms = Vec::new();
124+
125+
#[cfg(target_os = "macos")]
126+
{
127+
let has_accessibility = crate::platform::check_accessibility_permissions();
128+
perms.push(PlatformCapability {
129+
key: "accessibility".to_string(),
130+
status: if has_accessibility { "available" } else { "unavailable" }.to_string(),
131+
reason: if !has_accessibility {
132+
Some("Grant in System Settings > Privacy > Accessibility".to_string())
133+
} else {
134+
None
135+
},
136+
});
137+
perms.push(PlatformCapability {
138+
key: "screenRecording".to_string(),
139+
status: "unknown".to_string(),
140+
reason: Some("Check System Settings > Privacy > Screen Recording".to_string()),
141+
});
142+
}
143+
144+
perms
145+
}

src-tauri/src/cmd/export.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
use std::path::Path;
2+
use image::ImageReader;
3+
use serde::{Deserialize, Serialize};
4+
use tracing::info;
5+
6+
#[derive(Debug, Clone, Serialize, Deserialize)]
7+
pub struct ExportOptions {
8+
pub format: String,
9+
pub quality: u8,
10+
}
11+
12+
#[tauri::command]
13+
pub async fn export_image(
14+
source_path: String,
15+
target_path: String,
16+
format: String,
17+
quality: u8,
18+
) -> Result<String, String> {
19+
let source = Path::new(&source_path);
20+
if !source.exists() {
21+
return Err("Source file not found".to_string());
22+
}
23+
24+
let img = ImageReader::open(source)
25+
.map_err(|e| e.to_string())?
26+
.decode()
27+
.map_err(|e| e.to_string())?;
28+
29+
let target = Path::new(&target_path);
30+
if let Some(parent) = target.parent() {
31+
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
32+
}
33+
34+
match format.as_str() {
35+
"png" => {
36+
img.save_with_format(target, image::ImageFormat::Png)
37+
.map_err(|e| e.to_string())?;
38+
}
39+
"jpg" | "jpeg" => {
40+
let mut buf = std::io::BufWriter::new(
41+
std::fs::File::create(target).map_err(|e| e.to_string())?,
42+
);
43+
let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, quality);
44+
img.write_with_encoder(encoder).map_err(|e| e.to_string())?;
45+
}
46+
"webp" => {
47+
img.save_with_format(target, image::ImageFormat::WebP)
48+
.map_err(|e| e.to_string())?;
49+
}
50+
_ => {
51+
return Err(format!("Unsupported format: {}", format));
52+
}
53+
}
54+
55+
info!("Exported image to {} as {}", target_path, format);
56+
Ok(target_path)
57+
}
58+
59+
#[tauri::command]
60+
pub fn get_image_info(path: String) -> Result<ImageInfo, String> {
61+
let p = Path::new(&path);
62+
if !p.exists() {
63+
return Err("File not found".to_string());
64+
}
65+
let metadata = std::fs::metadata(p).map_err(|e| e.to_string())?;
66+
let (width, height) = image::image_dimensions(p).unwrap_or((0, 0));
67+
let ext = p.extension().and_then(|e| e.to_str()).unwrap_or("").to_string();
68+
69+
Ok(ImageInfo {
70+
width,
71+
height,
72+
file_size: metadata.len(),
73+
format: ext,
74+
})
75+
}
76+
77+
#[derive(Debug, Clone, Serialize, Deserialize)]
78+
pub struct ImageInfo {
79+
pub width: u32,
80+
pub height: u32,
81+
pub file_size: u64,
82+
pub format: String,
83+
}

0 commit comments

Comments
 (0)