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(())
}

41
src-tauri/src/lib.rs Normal file
View File

@@ -0,0 +1,41 @@
mod engine;
mod tray;
use tauri::Manager;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
Some(vec!["--minimized"]),
))
.plugin(tauri_plugin_opener::init())
.invoke_handler(tauri::generate_handler![
engine::toggle_pause,
engine::get_pause_state,
engine::get_timeline
])
.setup(|app| {
app.manage(engine::AppState {
is_paused: Arc::new(AtomicBool::new(false)),
});
tray::create_tray(app.handle())?;
engine::start_engine(app.handle().clone());
Ok(())
})
.on_window_event(|window, event| match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
window.hide().unwrap();
api.prevent_close();
}
_ => {}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}

6
src-tauri/src/main.rs Normal file
View File

@@ -0,0 +1,6 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
chrono_snap_lib::run()
}

59
src-tauri/src/tray.rs Normal file
View File

@@ -0,0 +1,59 @@
use tauri::{
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
AppHandle, Manager,
};
pub fn create_tray(app: &AppHandle) -> tauri::Result<()> {
let open_i = MenuItem::with_id(app, "open", "打开主界面", true, None::<&str>)?;
let toggle_i = MenuItem::with_id(app, "toggle", "暂停记录", true, None::<&str>)?;
let quit_i = MenuItem::with_id(app, "quit", "退出", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&open_i, &toggle_i, &quit_i])?;
let toggle_i_clone = toggle_i.clone();
TrayIconBuilder::with_id("main_tray")
.tooltip("Chrono Snap")
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.show_menu_on_left_click(false)
.on_menu_event(move |app, event| match event.id().as_ref() {
"open" => {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
"toggle" => {
let state = app.state::<crate::engine::AppState>();
let current = state.is_paused.load(std::sync::atomic::Ordering::SeqCst);
let new_state = !current;
state.is_paused.store(new_state, std::sync::atomic::Ordering::SeqCst);
let text = if new_state { "恢复记录" } else { "暂停记录" };
let _ = toggle_i_clone.set_text(text);
}
"quit" => {
app.exit(0);
}
_ => {}
})
.on_tray_icon_event(|tray, event| {
if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.set_focus();
}
}
})
.build(app)?;
Ok(())
}