Fix external calendar timezone conversion and styling

- Add comprehensive Windows timezone support for global external calendars
  - Map Windows timezone names (e.g. "Mountain Standard Time") to IANA zones (e.g. "America/Denver")
  - Support 60+ timezone mappings across North America, Europe, Asia, Asia Pacific, Africa, South America
  - Add chrono-tz dependency for proper timezone handling
- Fix external calendar event colors by setting calendar_path for color lookup
- Add visual distinction for external calendar events with dashed borders and calendar emoji
- Update timezone parsing to extract TZID parameters from iCalendar DTSTART/DTEND properties
- Pass external calendar data through component hierarchy for color matching

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Connor Johnstone
2025-09-03 19:11:57 -04:00
parent 8caa1f45ae
commit f88c238b0a
8 changed files with 544 additions and 24 deletions

View File

@@ -135,8 +135,13 @@ fn convert_ical_to_vevent(ical_event: IcalEvent) -> Result<VEvent, Box<dyn std::
dtstart = Some(chrono::TimeZone::from_utc_datetime(&Utc, &date.and_hms_opt(12, 0, 0).unwrap()));
}
} else {
// Parse datetime - could be various formats
if let Some(dt) = parse_datetime(&value) {
// Extract timezone info from parameters
let tzid = property.params.as_ref()
.and_then(|params| params.iter().find(|(k, _)| k == "TZID"))
.and_then(|(_, v)| v.first().cloned());
// Parse datetime with timezone information
if let Some(dt) = parse_datetime_with_tz(&value, tzid.as_deref()) {
dtstart = Some(dt);
}
}
@@ -150,7 +155,13 @@ fn convert_ical_to_vevent(ical_event: IcalEvent) -> Result<VEvent, Box<dyn std::
dtend = Some(chrono::TimeZone::from_utc_datetime(&Utc, &date.and_hms_opt(12, 0, 0).unwrap()));
}
} else {
if let Some(dt) = parse_datetime(&value) {
// Extract timezone info from parameters
let tzid = property.params.as_ref()
.and_then(|params| params.iter().find(|(k, _)| k == "TZID"))
.and_then(|(_, v)| v.first().cloned());
// Parse datetime with timezone information
if let Some(dt) = parse_datetime_with_tz(&value, tzid.as_deref()) {
dtend = Some(dt);
}
}
@@ -206,18 +217,16 @@ fn convert_ical_to_vevent(ical_event: IcalEvent) -> Result<VEvent, Box<dyn std::
Ok(vevent)
}
fn parse_datetime(datetime_str: &str) -> Option<DateTime<Utc>> {
fn parse_datetime_with_tz(datetime_str: &str, tzid: Option<&str>) -> Option<DateTime<Utc>> {
use chrono::TimeZone;
use chrono_tz::Tz;
// Try various datetime formats commonly found in ICS files
// Format: 20231201T103000Z (UTC)
if let Ok(dt) = DateTime::parse_from_str(datetime_str, "%Y%m%dT%H%M%SZ") {
return Some(dt.with_timezone(&Utc));
}
// Format: 20231201T103000 (floating time - assume UTC)
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(datetime_str, "%Y%m%dT%H%M%S") {
return Some(chrono::TimeZone::from_utc_datetime(&Utc, &dt));
}
// Format: 20231201T103000-0500 (with timezone offset)
if let Ok(dt) = DateTime::parse_from_str(datetime_str, "%Y%m%dT%H%M%S%z") {
@@ -229,9 +238,123 @@ fn parse_datetime(datetime_str: &str) -> Option<DateTime<Utc>> {
return Some(dt.with_timezone(&Utc));
}
// Format: 2023-12-01T10:30:00 (ISO without timezone)
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%dT%H:%M:%S") {
return Some(chrono::TimeZone::from_utc_datetime(&Utc, &dt));
// Handle naive datetime with timezone parameter
let naive_dt = if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(datetime_str, "%Y%m%dT%H%M%S") {
Some(dt)
} else if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(datetime_str, "%Y-%m-%dT%H:%M:%S") {
Some(dt)
} else {
None
};
if let Some(naive_dt) = naive_dt {
// If TZID is provided, try to parse it
if let Some(tzid_str) = tzid {
// Handle common timezone formats
let tz_result = if tzid_str.starts_with("/mozilla.org/") {
// Mozilla/Thunderbird format: /mozilla.org/20070129_1/Europe/London
tzid_str.split('/').last().and_then(|tz_name| tz_name.parse::<Tz>().ok())
} else if tzid_str.contains('/') {
// Standard timezone format: America/New_York, Europe/London
tzid_str.parse::<Tz>().ok()
} else {
// Try common abbreviations and Windows timezone names
match tzid_str {
// Standard abbreviations
"EST" => Some(Tz::America__New_York),
"PST" => Some(Tz::America__Los_Angeles),
"MST" => Some(Tz::America__Denver),
"CST" => Some(Tz::America__Chicago),
// North America - Windows timezone names to IANA mapping
"Mountain Standard Time" => Some(Tz::America__Denver),
"Eastern Standard Time" => Some(Tz::America__New_York),
"Central Standard Time" => Some(Tz::America__Chicago),
"Pacific Standard Time" => Some(Tz::America__Los_Angeles),
"Mountain Daylight Time" => Some(Tz::America__Denver),
"Eastern Daylight Time" => Some(Tz::America__New_York),
"Central Daylight Time" => Some(Tz::America__Chicago),
"Pacific Daylight Time" => Some(Tz::America__Los_Angeles),
"Hawaiian Standard Time" => Some(Tz::Pacific__Honolulu),
"Alaskan Standard Time" => Some(Tz::America__Anchorage),
"Alaskan Daylight Time" => Some(Tz::America__Anchorage),
"Atlantic Standard Time" => Some(Tz::America__Halifax),
"Newfoundland Standard Time" => Some(Tz::America__St_Johns),
// Europe
"GMT Standard Time" => Some(Tz::Europe__London),
"Greenwich Standard Time" => Some(Tz::UTC),
"W. Europe Standard Time" => Some(Tz::Europe__Berlin),
"Central Europe Standard Time" => Some(Tz::Europe__Warsaw),
"Romance Standard Time" => Some(Tz::Europe__Paris),
"Central European Standard Time" => Some(Tz::Europe__Belgrade),
"E. Europe Standard Time" => Some(Tz::Europe__Bucharest),
"FLE Standard Time" => Some(Tz::Europe__Helsinki),
"GTB Standard Time" => Some(Tz::Europe__Athens),
"Russian Standard Time" => Some(Tz::Europe__Moscow),
"Turkey Standard Time" => Some(Tz::Europe__Istanbul),
// Asia
"China Standard Time" => Some(Tz::Asia__Shanghai),
"Tokyo Standard Time" => Some(Tz::Asia__Tokyo),
"Korea Standard Time" => Some(Tz::Asia__Seoul),
"Singapore Standard Time" => Some(Tz::Asia__Singapore),
"India Standard Time" => Some(Tz::Asia__Kolkata),
"Pakistan Standard Time" => Some(Tz::Asia__Karachi),
"Bangladesh Standard Time" => Some(Tz::Asia__Dhaka),
"Thailand Standard Time" => Some(Tz::Asia__Bangkok),
"SE Asia Standard Time" => Some(Tz::Asia__Bangkok),
"Myanmar Standard Time" => Some(Tz::Asia__Yangon),
"Sri Lanka Standard Time" => Some(Tz::Asia__Colombo),
"Nepal Standard Time" => Some(Tz::Asia__Kathmandu),
"Central Asia Standard Time" => Some(Tz::Asia__Almaty),
"West Asia Standard Time" => Some(Tz::Asia__Tashkent),
"Afghanistan Standard Time" => Some(Tz::Asia__Kabul),
"Iran Standard Time" => Some(Tz::Asia__Tehran),
"Arabian Standard Time" => Some(Tz::Asia__Dubai),
"Arab Standard Time" => Some(Tz::Asia__Riyadh),
"Israel Standard Time" => Some(Tz::Asia__Jerusalem),
"Jordan Standard Time" => Some(Tz::Asia__Amman),
"Syria Standard Time" => Some(Tz::Asia__Damascus),
"Middle East Standard Time" => Some(Tz::Asia__Beirut),
"Egypt Standard Time" => Some(Tz::Africa__Cairo),
"South Africa Standard Time" => Some(Tz::Africa__Johannesburg),
"E. Africa Standard Time" => Some(Tz::Africa__Nairobi),
"W. Central Africa Standard Time" => Some(Tz::Africa__Lagos),
// Asia Pacific
"AUS Eastern Standard Time" => Some(Tz::Australia__Sydney),
"AUS Central Standard Time" => Some(Tz::Australia__Darwin),
"W. Australia Standard Time" => Some(Tz::Australia__Perth),
"Tasmania Standard Time" => Some(Tz::Australia__Hobart),
"New Zealand Standard Time" => Some(Tz::Pacific__Auckland),
"Fiji Standard Time" => Some(Tz::Pacific__Fiji),
"Tonga Standard Time" => Some(Tz::Pacific__Tongatapu),
// South America
"Argentina Standard Time" => Some(Tz::America__Buenos_Aires),
"E. South America Standard Time" => Some(Tz::America__Sao_Paulo),
"SA Eastern Standard Time" => Some(Tz::America__Cayenne),
"SA Pacific Standard Time" => Some(Tz::America__Bogota),
"SA Western Standard Time" => Some(Tz::America__La_Paz),
"Pacific SA Standard Time" => Some(Tz::America__Santiago),
"Venezuela Standard Time" => Some(Tz::America__Caracas),
"Montevideo Standard Time" => Some(Tz::America__Montevideo),
// Try parsing as IANA name
_ => tzid_str.parse::<Tz>().ok()
}
};
if let Some(tz) = tz_result {
if let Some(dt_with_tz) = tz.from_local_datetime(&naive_dt).single() {
return Some(dt_with_tz.with_timezone(&Utc));
}
}
}
// If no timezone info or parsing failed, treat as UTC (safer than local time assumptions)
return Some(chrono::TimeZone::from_utc_datetime(&Utc, &naive_dt));
}
None