init
This commit is contained in:
216
src-tauri/src/engine.rs
Normal file
216
src-tauri/src/engine.rs
Normal 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
41
src-tauri/src/lib.rs
Normal 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
6
src-tauri/src/main.rs
Normal 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
59
src-tauri/src/tray.rs
Normal 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(())
|
||||
}
|
||||
Reference in New Issue
Block a user