use crate::db::Db; use chrono::TimeZone; use std::collections::HashSet; use std::sync::Mutex; use std::time::Duration; use tauri::{AppHandle, Manager}; use tauri_plugin_notification::NotificationExt; const POLL_INTERVAL_SECS: u64 = 30; /// Only fire a reminder whose trigger moment is this recent — avoids a flood of /// stale notifications for reminders that elapsed while the app was closed. const FRESHNESS_WINDOW_SECS: i64 = 300; const LOOKAHEAD_SECS: i64 = 2 * 86400; pub fn spawn_poller(app: AppHandle) { tauri::async_runtime::spawn(async move { let notified: Mutex> = Mutex::new(HashSet::new()); let mut interval = tokio::time::interval(Duration::from_secs(POLL_INTERVAL_SECS)); loop { interval.tick().await; poll_once(&app, ¬ified); } }); } fn poll_once(app: &AppHandle, notified: &Mutex>) { let db = app.state::(); let Ok(events) = db.list_events_with_reminders() else { return }; let now = chrono::Utc::now().timestamp(); for base in events { let Some(reminder_min) = base.reminder_minutes else { continue }; let occurrences = if let Some(rule) = &base.rrule { let exdates: Vec = base.exdates.as_deref().unwrap_or("").split(',').filter_map(|s| s.trim().parse().ok()).collect(); crate::recurrence::expand(base.start_ts, base.end_ts, rule, now - 3600, now + LOOKAHEAD_SECS, &exdates) } else { vec![(base.start_ts, base.end_ts)] }; for (occ_start, _) in occurrences { let trigger_ts = occ_start - i64::from(reminder_min) * 60; if trigger_ts > now || now - trigger_ts >= FRESHNESS_WINDOW_SECS { continue; } let key = format!("{}#{}", base.id, occ_start); { let mut set = notified.lock().unwrap(); if !set.insert(key) { continue; } } let when = chrono::Local .timestamp_opt(occ_start, 0) .single() .map(|d| d.format("um %H:%M").to_string()) .unwrap_or_default(); let body = if base.all_day { "Heute".to_string() } else { format!("Beginnt {when}") }; let _ = app .notification() .builder() .title(format!("Erinnerung: {}", base.title)) .body(body) .show(); } } }