use std::fs;
use std::io;
use std::path::Path;
use serde::{Deserialize, Serialize};
use log::{warn, debug};
use chrono::{Utc, TimeZone};
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct AppState {
pub stats_only_usage_count: u64,
pub last_prompt_timestamp: Option<u64>, pub donation_prompts_disabled: bool,
}
impl AppState {
pub fn new() -> Self {
AppState {
stats_only_usage_count: 0,
last_prompt_timestamp: None,
donation_prompts_disabled: false,
}
}
pub fn load(path: &Path) -> io::Result<Self> {
match fs::read_to_string(path) {
Ok(json) => {
match serde_json::from_str(&json) {
Ok(app_state) => Ok(app_state),
Err(e) => {
warn!("Failed to deserialize AppState from {}: {}. Starting with default state.", path.display(), e);
Ok(AppState::new())
}
}
},
Err(ref e) if e.kind() == io::ErrorKind::NotFound => {
debug!("App state file not found at {}. Starting with default state.", path.display());
Ok(AppState::new())
},
Err(e) => {
warn!("Failed to read app state file from {}: {}. Starting with default state.", path.display(), e);
Ok(AppState::new())
}
}
}
pub fn load_from_path(path: &Path) -> anyhow::Result<Self> {
let app_state = match fs::read_to_string(path) {
Ok(json) => serde_json::from_str(&json)?,
Err(ref e) if e.kind() == io::ErrorKind::NotFound => AppState::new(),
Err(e) => return Err(e.into()), };
Ok(app_state)
}
pub fn save(&self, path: &Path) -> io::Result<()> {
let json = serde_json::to_string_pretty(self)?;
fs::write(path, json)?;
Ok(())
}
pub fn save_to_path(&self, path: &Path) -> anyhow::Result<()> {
let json = serde_json::to_string_pretty(self)?;
fs::write(path, json)?;
Ok(())
}
#[cfg(feature = "test-exposed")]
pub fn reset_for_testing(&mut self) {
self.stats_only_usage_count = 0;
self.last_prompt_timestamp = None;
self.donation_prompts_disabled = false;
}
pub fn increment_stats_only_usage(&mut self) {
self.stats_only_usage_count += 1;
}
pub fn should_display_donation_prompt(&mut self) -> bool {
if self.donation_prompts_disabled {
return false;
}
const STATS_PROMPT_THRESHOLD: u64 = 5; const PROMPT_COOLDOWN_DAYS: i64 = 30;
let now = Utc::now().timestamp() as u64;
if self.stats_only_usage_count >= STATS_PROMPT_THRESHOLD {
if let Some(last_prompt) = self.last_prompt_timestamp {
let last_prompt_date = Utc.timestamp_opt(last_prompt as i64, 0).single();
let now_date = Utc.timestamp_opt(now as i64, 0).single();
if let (Some(last_p_date), Some(n_date)) = (last_prompt_date, now_date) {
if (n_date - last_p_date).num_days() < PROMPT_COOLDOWN_DAYS {
debug!("Donation prompt cooldown active. Last prompt: {} days ago.", (n_date - last_p_date).num_days());
return false;
}
} else {
warn!("Failed to convert timestamps for donation prompt cooldown. Displaying prompt.");
}
}
debug!("Donation prompt conditions met. Displaying prompt.");
self.last_prompt_timestamp = Some(now);
true
} else {
debug!("Donation prompt threshold not met. Current count: {}", self.stats_only_usage_count);
false
}
}
}