1+ use headless_chrome:: { Browser , LaunchOptions } ;
2+ use headless_chrome:: protocol:: cdp:: Page ;
3+ use std:: fs;
4+ use std:: time:: Duration ;
5+ use tauri:: AppHandle ;
6+
7+ /// Captures a screenshot of a URL using headless Chrome
8+ ///
9+ /// This function launches a headless Chrome browser, navigates to the specified URL,
10+ /// and captures a screenshot of either the entire page or a specific element.
11+ ///
12+ /// # Arguments
13+ /// * `app` - The Tauri application handle
14+ /// * `url` - The URL to capture
15+ /// * `selector` - Optional CSS selector for a specific element to capture
16+ /// * `full_page` - Whether to capture the entire page or just the viewport
17+ ///
18+ /// # Returns
19+ /// * `Result<String, String>` - The path to the saved screenshot file, or an error message
20+ #[ tauri:: command]
21+ pub async fn capture_url_screenshot (
22+ _app : AppHandle ,
23+ url : String ,
24+ selector : Option < String > ,
25+ full_page : bool ,
26+ ) -> Result < String , String > {
27+ log:: info!(
28+ "Capturing screenshot of URL: {}, selector: {:?}, full_page: {}" ,
29+ url,
30+ selector,
31+ full_page
32+ ) ;
33+
34+ // Run the browser operations in a blocking task since headless_chrome is not async
35+ let result = tokio:: task:: spawn_blocking ( move || {
36+ capture_screenshot_sync ( url, selector, full_page)
37+ } )
38+ . await
39+ . map_err ( |e| format ! ( "Failed to spawn blocking task: {}" , e) ) ?;
40+
41+ // Log the result of the headless Chrome capture before returning
42+ match & result {
43+ Ok ( path) => log:: info!( "capture_url_screenshot returning path: {}" , path) ,
44+ Err ( err) => log:: error!( "capture_url_screenshot encountered error: {}" , err) ,
45+ }
46+
47+ result
48+ }
49+
50+ /// Synchronous helper function to capture screenshots using headless Chrome
51+ fn capture_screenshot_sync (
52+ url : String ,
53+ selector : Option < String > ,
54+ full_page : bool ,
55+ ) -> Result < String , String > {
56+ // Configure browser launch options
57+ let launch_options = LaunchOptions {
58+ headless : true ,
59+ window_size : Some ( ( 1920 , 1080 ) ) ,
60+ ..Default :: default ( )
61+ } ;
62+
63+ // Launch the browser
64+ let browser = Browser :: new ( launch_options)
65+ . map_err ( |e| format ! ( "Failed to launch browser: {}" , e) ) ?;
66+
67+ // Create a new tab
68+ let tab = browser
69+ . new_tab ( )
70+ . map_err ( |e| format ! ( "Failed to create new tab: {}" , e) ) ?;
71+
72+ // Set a reasonable timeout for page navigation
73+ tab. set_default_timeout ( Duration :: from_secs ( 30 ) ) ;
74+
75+ // Navigate to the URL
76+ tab. navigate_to ( & url)
77+ . map_err ( |e| format ! ( "Failed to navigate to URL: {}" , e) ) ?;
78+
79+ // Wait for the page to load
80+ // Try to wait for network idle, but don't fail if it times out
81+ let _ = tab. wait_until_navigated ( ) ;
82+
83+ // Additional wait to ensure dynamic content loads
84+ std:: thread:: sleep ( Duration :: from_millis ( 500 ) ) ;
85+
86+ // Wait explicitly for the <body> element to exist – this often prevents
87+ // "Unable to capture screenshot" CDP errors on some pages
88+ if let Err ( e) = tab. wait_for_element ( "body" ) {
89+ log:: warn!( "Timed out waiting for <body> element: {} – continuing anyway" , e) ;
90+ }
91+
92+ // Capture the screenshot
93+ let screenshot_data = if let Some ( selector) = selector {
94+ // Wait for the element and capture it
95+ log:: info!( "Waiting for element with selector: {}" , selector) ;
96+
97+ let element = tab
98+ . wait_for_element ( & selector)
99+ . map_err ( |e| format ! ( "Failed to find element '{}': {}" , selector, e) ) ?;
100+
101+ element
102+ . capture_screenshot ( Page :: CaptureScreenshotFormatOption :: Png )
103+ . map_err ( |e| format ! ( "Failed to capture element screenshot: {}" , e) ) ?
104+ } else {
105+ // Capture the entire page or viewport
106+ log:: info!( "Capturing {} screenshot" , if full_page { "full page" } else { "viewport" } ) ;
107+
108+ // Get the page dimensions for full page screenshot
109+ let clip = if full_page {
110+ // Execute JavaScript to get the full page dimensions
111+ let dimensions = tab
112+ . evaluate (
113+ r#"
114+ ({
115+ width: Math.max(
116+ document.body.scrollWidth,
117+ document.documentElement.scrollWidth,
118+ document.body.offsetWidth,
119+ document.documentElement.offsetWidth,
120+ document.documentElement.clientWidth
121+ ),
122+ height: Math.max(
123+ document.body.scrollHeight,
124+ document.documentElement.scrollHeight,
125+ document.body.offsetHeight,
126+ document.documentElement.offsetHeight,
127+ document.documentElement.clientHeight
128+ )
129+ })
130+ "# ,
131+ false ,
132+ )
133+ . map_err ( |e| format ! ( "Failed to get page dimensions: {}" , e) ) ?;
134+
135+ // Extract dimensions from the result
136+ let width = dimensions
137+ . value
138+ . as_ref ( )
139+ . and_then ( |v| v. as_object ( ) )
140+ . and_then ( |obj| obj. get ( "width" ) )
141+ . and_then ( |v| v. as_f64 ( ) )
142+ . unwrap_or ( 1920.0 ) ;
143+
144+ let height = dimensions
145+ . value
146+ . as_ref ( )
147+ . and_then ( |v| v. as_object ( ) )
148+ . and_then ( |obj| obj. get ( "height" ) )
149+ . and_then ( |v| v. as_f64 ( ) )
150+ . unwrap_or ( 1080.0 ) ;
151+
152+ Some ( Page :: Viewport {
153+ x : 0.0 ,
154+ y : 0.0 ,
155+ width,
156+ height,
157+ scale : 1.0 ,
158+ } )
159+ } else {
160+ None
161+ } ;
162+
163+ let capture_result = tab. capture_screenshot (
164+ Page :: CaptureScreenshotFormatOption :: Png ,
165+ None ,
166+ clip. clone ( ) ,
167+ full_page, // capture_beyond_viewport only makes sense for full page
168+ ) ;
169+
170+ match capture_result {
171+ Ok ( data) => data,
172+ Err ( err) => {
173+ // Retry once with capture_beyond_viewport=true which works around some Chromium bugs
174+ log:: warn!(
175+ "Initial screenshot attempt failed: {}. Retrying with capture_beyond_viewport=true" ,
176+ err
177+ ) ;
178+
179+ tab. capture_screenshot (
180+ Page :: CaptureScreenshotFormatOption :: Png ,
181+ None ,
182+ clip,
183+ true ,
184+ )
185+ . map_err ( |e| format ! ( "Failed to capture screenshot after retry: {}" , e) ) ?
186+ }
187+ }
188+ } ;
189+
190+ // Save to temporary file
191+ let temp_dir = std:: env:: temp_dir ( ) ;
192+ let timestamp = chrono:: Utc :: now ( ) . timestamp_millis ( ) ;
193+ let filename = format ! ( "claudia_screenshot_{}.png" , timestamp) ;
194+ let file_path = temp_dir. join ( filename) ;
195+
196+ fs:: write ( & file_path, screenshot_data)
197+ . map_err ( |e| format ! ( "Failed to save screenshot: {}" , e) ) ?;
198+
199+ // Log the screenshot path prominently
200+ println ! ( "═══════════════════════════════════════════════════════════════" ) ;
201+ println ! ( "📸 SCREENSHOT SAVED SUCCESSFULLY!" ) ;
202+ println ! ( "📁 Location: {}" , file_path. display( ) ) ;
203+ println ! ( "═══════════════════════════════════════════════════════════════" ) ;
204+
205+ log:: info!( "Screenshot saved to: {:?}" , file_path) ;
206+
207+ Ok ( file_path. to_string_lossy ( ) . to_string ( ) )
208+ }
209+
210+ /// Cleans up old screenshot files from the temporary directory
211+ ///
212+ /// This function removes screenshot files older than the specified number of minutes
213+ /// to prevent accumulation of temporary files.
214+ ///
215+ /// # Arguments
216+ /// * `older_than_minutes` - Remove files older than this many minutes (default: 60)
217+ ///
218+ /// # Returns
219+ /// * `Result<usize, String>` - The number of files deleted, or an error message
220+ #[ tauri:: command]
221+ pub async fn cleanup_screenshot_temp_files (
222+ older_than_minutes : Option < u64 > ,
223+ ) -> Result < usize , String > {
224+ let minutes = older_than_minutes. unwrap_or ( 60 ) ;
225+ log:: info!( "Cleaning up screenshot files older than {} minutes" , minutes) ;
226+
227+ let temp_dir = std:: env:: temp_dir ( ) ;
228+ let cutoff_time = chrono:: Utc :: now ( ) - chrono:: Duration :: minutes ( minutes as i64 ) ;
229+ let mut deleted_count = 0 ;
230+
231+ // Read directory entries
232+ let entries = fs:: read_dir ( & temp_dir)
233+ . map_err ( |e| format ! ( "Failed to read temp directory: {}" , e) ) ?;
234+
235+ for entry in entries {
236+ if let Ok ( entry) = entry {
237+ let path = entry. path ( ) ;
238+
239+ // Check if it's a claudia screenshot file
240+ if let Some ( filename) = path. file_name ( ) {
241+ if let Some ( filename_str) = filename. to_str ( ) {
242+ if filename_str. starts_with ( "claudia_screenshot_" ) && filename_str. ends_with ( ".png" ) {
243+ // Check file age
244+ if let Ok ( metadata) = fs:: metadata ( & path) {
245+ if let Ok ( modified) = metadata. modified ( ) {
246+ let modified_time = chrono:: DateTime :: < chrono:: Utc > :: from ( modified) ;
247+ if modified_time < cutoff_time {
248+ // Delete the file
249+ if fs:: remove_file ( & path) . is_ok ( ) {
250+ deleted_count += 1 ;
251+ log:: debug!( "Deleted old screenshot: {:?}" , path) ;
252+ }
253+ }
254+ }
255+ }
256+ }
257+ }
258+ }
259+ }
260+ }
261+
262+ log:: info!( "Cleaned up {} old screenshot files" , deleted_count) ;
263+ Ok ( deleted_count)
264+ }
0 commit comments