AkurAI Build
Menu

cosmic-mobile

public

Latest change bd10038fdf127d3b0f9f4ddc6c5afd463c4d2e34 - Calendar: iOS month grid, today/selection, event dots, day agenda, timed events via chrono by AkurAI Build

use chrono::{NaiveDate, NaiveTime};
use std::{io, path::PathBuf};
/// Calendar "when" field: date with optional 24h time. Lexical order equals chronological order.
pub fn parse_when(text: &str) -> Option<(NaiveDate, Option<NaiveTime>)> {
    let (date, time) = match text.split_once(' ') {
        Some((d, t)) => (d, Some(t)),
        None => (text, None),
    };
    let date = NaiveDate::parse_from_str(date, "%Y-%m-%d").ok()?;
    if date.format("%Y-%m-%d").to_string() != text.split(' ').next()? {
        return None;
    }
    match time {
        None => Some((date, None)),
        Some(t) => Some((date, Some(NaiveTime::parse_from_str(t, "%H:%M").ok()?))),
    }
}
pub struct Store {
    pub rows: Vec<(String, String, String, String)>,
    path: PathBuf,
}
impl Store {
    pub fn for_app(&self, app: &str) -> Vec<&(String, String, String, String)> {
        let mut rows: Vec<_> = self.rows.iter().filter(|r| r.0 == app).collect();
        if app == "Calendar" {
            rows.sort_by(|a, b| a.1.cmp(&b.1));
        }
        rows
    }
    pub fn load(path: PathBuf) -> io::Result<Self> {
        let text = match std::fs::read_to_string(&path) {
            Ok(text) => text,
            Err(e) if e.kind() == io::ErrorKind::NotFound => String::new(),
            Err(e) => return Err(e),
        };
        let mut rows = Vec::new();
        for line in text.lines() {
            let fields: Vec<_> = line.split('\t').collect();
            if fields.len() != 4 {
                return Err(io::Error::other("Invalid records; file preserved"));
            }
            rows.push((
                fields[0].into(),
                fields[1].into(),
                fields[2].into(),
                fields[3].into(),
            ));
        }
        Ok(Self { rows, path })
    }
    pub fn add(&mut self, app: &str, first: &str, second: &str) -> io::Result<()> {
        let first = first.trim();
        let second = second.trim();
        if !["Phone", "Contacts", "Calendar", "Messages"].contains(&app)
            || [first, second]
                .iter()
                .any(|v| v.is_empty() || v.len() > 4096 || v.chars().any(char::is_control))
        {
            return Err(io::Error::other(
                "Enter both fields; control characters are not allowed",
            ));
        }
        let number = match app {
            "Contacts" => Some(second),
            "Phone" | "Messages" => Some(first),
            _ => None,
        };
        if let Some(number) = number {
            if number.len() > 64
                || !number.bytes().any(|b| b.is_ascii_digit())
                || !number
                    .bytes()
                    .all(|b| b.is_ascii_digit() || b"+ -()*#".contains(&b))
            {
                return Err(io::Error::other("Enter a valid phone number"));
            }
        }
        if app == "Calendar" && parse_when(first).is_none() {
            return Err(io::Error::other("Use YYYY-MM-DD or YYYY-MM-DD HH:MM"));
        }
        let status = match app {
            "Phone" => "Simulated — no call placed",
            "Messages" => "Simulated — not sent",
            _ => "Saved locally",
        };
        let mut rows = self.rows.clone();
        rows.push((app.into(), first.into(), second.into(), status.into()));
        let data: String = rows
            .iter()
            .map(|r| format!("{}\t{}\t{}\t{}\n", r.0, r.1, r.2, r.3))
            .collect();
        if let Some(parent) = self.path.parent() {
            std::fs::create_dir_all(parent)?;
        }
        let tmp = self.path.with_extension("tmp");
        std::fs::write(&tmp, data)?;
        std::fs::rename(tmp, &self.path)?;
        self.rows = rows;
        Ok(())
    }
}