This commit is contained in:
Julian Freeman
2026-03-22 15:28:30 -04:00
commit e3cba2a6ba
43 changed files with 9604 additions and 0 deletions

216
src-tauri/src/engine.rs Normal file
View File

@@ -0,0 +1,216 @@
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::time::{interval, Duration};
use tauri::{AppHandle, Manager};
use xcap::Monitor;
use chrono::Local;
use std::fs;
use std::path::PathBuf;
use tauri_plugin_store::StoreExt;
pub struct AppState {
pub is_paused: Arc<AtomicBool>,
}
#[tauri::command]
pub fn toggle_pause(state: tauri::State<'_, AppState>) -> bool {
let current = state.is_paused.load(Ordering::SeqCst);
state.is_paused.store(!current, Ordering::SeqCst);
!current
}
#[tauri::command]
pub fn get_pause_state(state: tauri::State<'_, AppState>) -> bool {
state.is_paused.load(Ordering::SeqCst)
}
#[tauri::command]
pub fn get_timeline(date: String, base_dir: String) -> Vec<serde_json::Value> {
let mut results = Vec::new();
let dir_path = PathBuf::from(base_dir).join(date);
if !dir_path.exists() || !dir_path.is_dir() {
return results;
}
if let Ok(entries) = fs::read_dir(dir_path) {
let mut paths: Vec<_> = entries
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.is_file() && p.extension().and_then(|s| s.to_str()) == Some("jpg"))
.collect();
paths.sort();
for path in paths {
if let Some(file_name) = path.file_stem().and_then(|s| s.to_str()) {
// file_name format: "14-30-00" or "14-30-00_0"
let time_str = file_name.replace("-", ":").replace("_", " ");
results.push(serde_json::json!({
"time": time_str,
"path": path.to_string_lossy().to_string()
}));
}
}
}
results
}
pub fn start_engine(app: AppHandle) {
let state = app.state::<AppState>();
let is_paused = state.is_paused.clone();
// Start Cleanup routine
let app_clone = app.clone();
tauri::async_runtime::spawn(async move {
// Run once on startup, then every 24 hours
let mut ticker = interval(Duration::from_secs(60 * 60 * 24));
loop {
ticker.tick().await;
if let Err(e) = run_cleanup(&app_clone).await {
eprintln!("Cleanup error: {}", e);
}
}
});
tauri::async_runtime::spawn(async move {
// Every 60 seconds
let mut ticker = interval(Duration::from_secs(60));
loop {
ticker.tick().await;
if is_paused.load(Ordering::SeqCst) {
continue;
}
println!("Tick: capturing screen...");
if let Err(e) = capture_screens(&app).await {
eprintln!("Failed to capture screens: {}", e);
}
}
});
}
async fn run_cleanup(app: &AppHandle) -> anyhow::Result<()> {
let store = match app.store("config.json") {
Ok(s) => s,
Err(_) => return Ok(()),
};
let save_path_value = store.get("savePath");
let save_path_str = match save_path_value {
Some(serde_json::Value::String(s)) => s,
_ => return Ok(()),
};
let base_dir = PathBuf::from(save_path_str);
if !base_dir.exists() {
return Ok(());
}
let retain_days = store
.get("retainDays")
.and_then(|v| v.as_u64())
.unwrap_or(30) as i64;
let now = chrono::Local::now().naive_local().date();
if let Ok(entries) = fs::read_dir(&base_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(folder_name) = path.file_name().and_then(|n| n.to_str()) {
if let Ok(folder_date) = chrono::NaiveDate::parse_from_str(folder_name, "%Y-%m-%d") {
let duration = now.signed_duration_since(folder_date);
if duration.num_days() > retain_days {
let _ = fs::remove_dir_all(&path);
}
}
}
}
}
}
Ok(())
}
async fn capture_screens(app: &AppHandle) -> anyhow::Result<()> {
let store = match app.store("config.json") {
Ok(s) => s,
Err(_) => return Ok(()),
};
let save_path_value = store.get("savePath");
let save_path_str = match save_path_value {
Some(serde_json::Value::String(s)) => s,
_ => return Ok(()),
};
let base_dir = PathBuf::from(save_path_str);
if !base_dir.exists() {
fs::create_dir_all(&base_dir)?;
}
let merge_screens = store
.get("mergeScreens")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let monitors = Monitor::all()?;
let now = Local::now();
let timestamp = now.format("%H-%M-%S").to_string();
let date_folder = now.format("%Y-%m-%d").to_string();
let save_dir = base_dir.join(date_folder);
if !save_dir.exists() {
fs::create_dir_all(&save_dir)?;
}
if merge_screens {
let mut min_x = i32::MAX;
let mut min_y = i32::MAX;
let mut max_x = i32::MIN;
let mut max_y = i32::MIN;
for m in &monitors {
let x = m.x()?;
let y = m.y()?;
let width = m.width()? as i32;
let height = m.height()? as i32;
min_x = min_x.min(x);
min_y = min_y.min(y);
max_x = max_x.max(x + width);
max_y = max_y.max(y + height);
}
let total_width = (max_x - min_x) as u32;
let total_height = (max_y - min_y) as u32;
let mut combined_image = image::RgbaImage::new(total_width, total_height);
for m in &monitors {
let capture = m.capture_image()?;
let offset_x = (m.x()? - min_x) as u32;
let offset_y = (m.y()? - min_y) as u32;
image::imageops::overlay(&mut combined_image, &capture, offset_x as i64, offset_y as i64);
}
let file_path = save_dir.join(format!("{}.jpg", timestamp));
let dynamic_image = image::DynamicImage::ImageRgba8(combined_image);
let rgb_image = dynamic_image.into_rgb8();
rgb_image.save_with_format(file_path, image::ImageFormat::Jpeg)?;
} else {
for (i, m) in monitors.iter().enumerate() {
let capture = m.capture_image()?;
let file_path = save_dir.join(format!("{}_{}.jpg", timestamp, i));
let dynamic_image = image::DynamicImage::ImageRgba8(capture);
let rgb_image = dynamic_image.into_rgb8();
rgb_image.save_with_format(file_path, image::ImageFormat::Jpeg)?;
}
}
Ok(())
}