Feature 3: Read-only Google Calendar panel
- CalendarEvent model with summary, start, end, location - New scope calendar.readonly in SCOPES - fetch_upcoming_events() in ApiClient (15 events, next 7 days) - Focus::Calendar variant, Tab cycle includes Calendar - Body layout: left column split into Tasks (flex) + Calendar (8) - render_calendar_panel with day headers and scroll - refresh_calendar() called on initial sync and Ctrl+R - Up/Down scroll Calendar panel when focused
This commit is contained in:
@@ -33,7 +33,10 @@ pub struct ApiClient {
|
||||
token_path: PathBuf,
|
||||
}
|
||||
|
||||
const SCOPES: &[&str] = &["https://www.googleapis.com/auth/tasks"];
|
||||
const SCOPES: &[&str] = &[
|
||||
"https://www.googleapis.com/auth/tasks",
|
||||
"https://www.googleapis.com/auth/calendar.readonly",
|
||||
];
|
||||
|
||||
impl ApiClient {
|
||||
pub async fn new(secret_path: impl AsRef<Path>) -> Result<Self, ApiError> {
|
||||
@@ -455,4 +458,60 @@ impl ApiClient {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn fetch_upcoming_events(&self, max_results: u32) -> Result<Vec<CalendarEvent>, ApiError> {
|
||||
let token = self.get_token().await?;
|
||||
|
||||
let now = chrono::Utc::now();
|
||||
let week_later = now + chrono::Duration::days(7);
|
||||
|
||||
let resp = self
|
||||
.client
|
||||
.get("https://www.googleapis.com/calendar/v3/calendars/primary/events")
|
||||
.bearer_auth(&token)
|
||||
.query(&[
|
||||
("orderBy", "startTime"),
|
||||
("singleEvents", "true"),
|
||||
("timeMin", &now.format("%Y-%m-%dT%H:%M:%SZ").to_string()),
|
||||
("timeMax", &week_later.format("%Y-%m-%dT%H:%M:%SZ").to_string()),
|
||||
("maxResults", &max_results.to_string()),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| ApiError::Network(e.to_string()))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
let status = resp.status();
|
||||
let body = resp.text().await.unwrap_or_default();
|
||||
return Err(ApiError::Api(format!("Calendar fetch failed: {} - {}", status, body)));
|
||||
}
|
||||
|
||||
let body: serde_json::Value = resp.json().await
|
||||
.map_err(|e| ApiError::Api(format!("Calendar JSON parse error: {}", e)))?;
|
||||
|
||||
let items = body["items"].as_array()
|
||||
.map(|arr| {
|
||||
arr.iter().filter_map(|item| {
|
||||
let summary = item["summary"].as_str()?.to_string();
|
||||
let start = parse_calendar_time(&item["start"]);
|
||||
let end = parse_calendar_time(&item["end"]);
|
||||
let location = item["location"].as_str().map(|s| s.to_string());
|
||||
Some(CalendarEvent { summary, start, end, location })
|
||||
}).collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(items)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_calendar_time(obj: &serde_json::Value) -> Option<chrono::NaiveDateTime> {
|
||||
if let Some(dt) = obj["dateTime"].as_str() {
|
||||
chrono::DateTime::parse_from_rfc3339(dt).ok().map(|d| d.naive_local())
|
||||
} else if let Some(d) = obj["date"].as_str() {
|
||||
chrono::NaiveDate::parse_from_str(d, "%Y-%m-%d").ok()
|
||||
.map(|date| date.and_hms_opt(0, 0, 0).unwrap())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user