server: cleanup calendar summary on mobile
This commit is contained in:
parent
00ce9267c1
commit
d16c221995
@ -1,7 +1,7 @@
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use askama::Template;
|
||||
use chrono::{TimeZone, Utc, Datelike, NaiveDate, Local, LocalResult};
|
||||
use chrono::{Datelike, Local, LocalResult, NaiveDate, TimeZone, Utc};
|
||||
use chrono_tz::Tz;
|
||||
use mailparse::{parse_content_type, parse_mail, MailHeader, MailHeaderMap, ParsedMail};
|
||||
use quick_xml::de::from_str as xml_from_str;
|
||||
@ -1319,7 +1319,7 @@ pub fn render_ical_summary(ical_data: &str) -> Result<String, ServerError> {
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
"DTEND" => dtend = prop.value.as_deref(),
|
||||
"ORGANIZER" => {
|
||||
organizer = prop.value.as_deref();
|
||||
@ -1336,11 +1336,20 @@ pub fn render_ical_summary(ical_data: &str) -> Result<String, ServerError> {
|
||||
}
|
||||
|
||||
// Parse start/end as chrono DateTime
|
||||
let (start_dt, end_dt, local_fmt_start, local_fmt_end, event_days) = if let Some(dtstart) = dtstart {
|
||||
let tz: Tz = tzid.as_deref().unwrap_or("UTC").parse().unwrap_or(chrono_tz::UTC);
|
||||
let fallback = tz.from_utc_datetime(&chrono::NaiveDateTime::from_timestamp_opt(0, 0).unwrap());
|
||||
let (start_dt, end_dt, local_fmt_start, local_fmt_end, event_days) =
|
||||
if let Some(dtstart) = dtstart {
|
||||
let tz: Tz = tzid
|
||||
.as_deref()
|
||||
.unwrap_or("UTC")
|
||||
.parse()
|
||||
.unwrap_or(chrono_tz::UTC);
|
||||
let fallback = tz.from_utc_datetime(
|
||||
&chrono::NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
|
||||
);
|
||||
let start = parse_ical_datetime_tz(dtstart, tz).unwrap_or(fallback);
|
||||
let end = dtend.and_then(|d| parse_ical_datetime_tz(d, tz)).unwrap_or(start);
|
||||
let end = dtend
|
||||
.and_then(|d| parse_ical_datetime_tz(d, tz))
|
||||
.unwrap_or(start);
|
||||
let local_start = start.with_timezone(&Local);
|
||||
let local_end = end.with_timezone(&Local);
|
||||
let fmt_start = local_start.format("%c").to_string();
|
||||
@ -1349,7 +1358,8 @@ pub fn render_ical_summary(ical_data: &str) -> Result<String, ServerError> {
|
||||
let mut d = start.date_naive();
|
||||
let mut end_d = end.date_naive();
|
||||
// Check for all-day event (DATE, not DATE-TIME)
|
||||
let allday = dtstart.len() == 8 && (dtend.map(|s| s.len() == 8).unwrap_or(false));
|
||||
let allday =
|
||||
dtstart.len() == 8 && (dtend.map(|s| s.len() == 8).unwrap_or(false));
|
||||
if allday {
|
||||
// DTEND is exclusive for all-day events
|
||||
if end_d > d {
|
||||
@ -1367,7 +1377,9 @@ pub fn render_ical_summary(ical_data: &str) -> Result<String, ServerError> {
|
||||
(start, end, fmt_start, fmt_end, days)
|
||||
} else {
|
||||
let tz = chrono_tz::UTC;
|
||||
let fallback = tz.from_utc_datetime(&chrono::NaiveDateTime::from_timestamp_opt(0, 0).unwrap());
|
||||
let fallback = tz.from_utc_datetime(
|
||||
&chrono::NaiveDateTime::from_timestamp_opt(0, 0).unwrap(),
|
||||
);
|
||||
(fallback, fallback, String::new(), String::new(), vec![])
|
||||
};
|
||||
|
||||
@ -1378,13 +1390,15 @@ pub fn render_ical_summary(ical_data: &str) -> Result<String, ServerError> {
|
||||
let first_event = event_days.first().unwrap();
|
||||
let last_event = event_days.last().unwrap();
|
||||
// First day of the first event's month
|
||||
let first_of_month = NaiveDate::from_ymd_opt(first_event.year(), first_event.month(), 1).unwrap();
|
||||
let first_of_month =
|
||||
NaiveDate::from_ymd_opt(first_event.year(), first_event.month(), 1).unwrap();
|
||||
// Last day of the last event's month
|
||||
let last_of_month = {
|
||||
let next_month = if last_event.month() == 12 {
|
||||
NaiveDate::from_ymd_opt(last_event.year() + 1, 1, 1).unwrap()
|
||||
} else {
|
||||
NaiveDate::from_ymd_opt(last_event.year(), last_event.month() + 1, 1).unwrap()
|
||||
NaiveDate::from_ymd_opt(last_event.year(), last_event.month() + 1, 1)
|
||||
.unwrap()
|
||||
};
|
||||
next_month.pred_opt().unwrap()
|
||||
};
|
||||
@ -1433,42 +1447,96 @@ pub fn render_ical_summary(ical_data: &str) -> Result<String, ServerError> {
|
||||
} else {
|
||||
"border:1px solid #eee; text-align:center;background:#f7f7f7;color:#bbb;"
|
||||
};
|
||||
calendar_html.push_str(&format!("<td style='{}'>{}</td>", style, day.day()));
|
||||
calendar_html.push_str(&format!(
|
||||
"<td style='{}'>{}</td>",
|
||||
style,
|
||||
day.day()
|
||||
));
|
||||
}
|
||||
calendar_html.push_str("</tr>");
|
||||
}
|
||||
calendar_html.push_str("</tbody></table>");
|
||||
}
|
||||
let mut summary_html = String::from("<div style='background:#f7f7f7; border-radius:8px; box-shadow:0 2px 8px #bbb; padding:16px 18px; margin:0 0 8px 0; min-width:220px; max-width:400px; font-size:15px; color:#222;'>");
|
||||
// Remove block-level summary wrapper, move its styles to the flex child
|
||||
let summary_inner = {
|
||||
let mut s = String::new();
|
||||
s.push_str("<div style='font-size:17px; font-weight:bold; margin-bottom:8px; color:#333;'><b>Summary:</b> ");
|
||||
if let Some(summary) = summary {
|
||||
summary_html.push_str(&format!("<div style='font-size:17px; font-weight:bold; margin-bottom:8px; color:#333;'><b>Summary:</b> {}</div>", summary));
|
||||
s.push_str(summary);
|
||||
}
|
||||
if let Some(desc) = description {
|
||||
summary_html.push_str(&format!("<div style='margin-bottom:8px; color:#444;'>{}</div>", desc));
|
||||
}
|
||||
summary_html.push_str(&format!("<div style='margin-bottom:4px;'><b>Start:</b> {}</div>", local_fmt_start));
|
||||
summary_html.push_str(&format!("<div style='margin-bottom:4px;'><b>End:</b> {}</div>", local_fmt_end));
|
||||
s.push_str("</div>");
|
||||
s.push_str(&format!(
|
||||
"<div style='margin-bottom:4px;'><b>Start:</b> {}</div>",
|
||||
local_fmt_start
|
||||
));
|
||||
s.push_str(&format!(
|
||||
"<div style='margin-bottom:4px;'><b>End:</b> {}</div>",
|
||||
local_fmt_end
|
||||
));
|
||||
if let Some(cn) = organizer_cn {
|
||||
summary_html.push_str(&format!("<div style='margin-bottom:4px;'><b>Organizer:</b> {}</div>", cn));
|
||||
s.push_str(&format!(
|
||||
"<div style='margin-bottom:4px;'><b>Organizer:</b> {}</div>",
|
||||
cn
|
||||
));
|
||||
} else if let Some(organizer) = organizer {
|
||||
summary_html.push_str(&format!("<div style='margin-bottom:4px;'><b>Organizer:</b> {}</div>", organizer));
|
||||
s.push_str(&format!(
|
||||
"<div style='margin-bottom:4px;'><b>Organizer:</b> {}</div>",
|
||||
organizer
|
||||
));
|
||||
}
|
||||
summary_html.push_str("</div>");
|
||||
// Wrap in a flexbox for layout
|
||||
s
|
||||
};
|
||||
|
||||
// Responsive layout: row with wrap by default, summary expands, calendar right-justified
|
||||
let event_block = format!(
|
||||
"<div style=\"display: flex; align-items: flex-start; gap: 1.5em;\">\
|
||||
<div style=\"flex: 1;\">{}</div>\
|
||||
<div style=\"flex: none;\">{}</div>\
|
||||
"<style>.ical-flex {{ display: flex; flex-direction: row; flex-wrap: wrap; align-items: stretch; gap: 0.5em; max-width: 700px; width: 100%; }}\n.ical-flex .summary-block {{ flex: 1 1 0%; }}\n.ical-flex .calendar-block {{ flex: none; margin-left: auto; }}\n@media (max-width: 599px) {{ .ical-flex {{ flex-direction: column; }} .ical-flex > div.summary-block {{ margin-bottom: 0.5em; margin-left: 0; }} .ical-flex > div.calendar-block {{ margin-left: 0; }} }}</style>\
|
||||
<div class='ical-flex'>\
|
||||
<div class='summary-block' style='background:#f7f7f7; border-radius:8px; box-shadow:0 2px 8px #bbb; padding:16px 18px; margin:0 0 8px 0; min-width:220px; max-width:400px; font-size:15px; color:#222;'>{}</div>\
|
||||
{}\
|
||||
</div>",
|
||||
summary_html, calendar_html
|
||||
summary_inner,
|
||||
if !calendar_html.is_empty() {
|
||||
format!("<div class=\"calendar-block\" style=\"margin-top: 8px;\">{}</div>", calendar_html)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
);
|
||||
summary_parts.push(event_block);
|
||||
|
||||
// Description: only if present and non-whitespace, below summary+calendar
|
||||
let mut desc_block = String::new();
|
||||
if let Some(desc) = description {
|
||||
// Replace escaped newlines with real newlines, then split
|
||||
let desc = desc.replace("\\n", "\n");
|
||||
if desc.trim().len() > 0 {
|
||||
let paragraphs: Vec<String> = desc
|
||||
.lines()
|
||||
.map(|line| line.trim())
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(|line| {
|
||||
format!("<p style='margin: 0 0 8px 0; color:#444;'>{}</p>", line)
|
||||
})
|
||||
.collect();
|
||||
if !paragraphs.is_empty() {
|
||||
desc_block = format!(
|
||||
"<div style='max-width:700px; width:100%;'>{}</div>",
|
||||
paragraphs.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
summary_parts.push(format!("{}{}", event_block, desc_block));
|
||||
}
|
||||
}
|
||||
|
||||
fn render_month_calendar_widget(date: NaiveDate, event_days: &[NaiveDate]) -> String {
|
||||
let first = date.with_day(1).unwrap();
|
||||
let last = first.with_day(1).unwrap().succ_opt().unwrap_or(first).pred_opt().unwrap_or(first);
|
||||
let last = first
|
||||
.with_day(1)
|
||||
.unwrap()
|
||||
.succ_opt()
|
||||
.unwrap_or(first)
|
||||
.pred_opt()
|
||||
.unwrap_or(first);
|
||||
let month_name = first.format("%B").to_string();
|
||||
let year = first.year();
|
||||
let mut html = String::from(
|
||||
@ -1493,9 +1561,15 @@ pub fn render_ical_summary(ical_data: &str) -> Result<String, ServerError> {
|
||||
if event_days.contains(&cur) {
|
||||
html.push_str(&format!("<td style='background:#ffd700; color:#222; font-weight:bold; border:1px solid #aaa; border-radius:4px; text-align:center;'>{}</td>", cur.day()));
|
||||
} else {
|
||||
html.push_str(&format!("<td style='border:1px solid #eee; text-align:center;'>{}</td>", cur.day()));
|
||||
html.push_str(&format!(
|
||||
"<td style='border:1px solid #eee; text-align:center;'>{}</td>",
|
||||
cur.day()
|
||||
));
|
||||
}
|
||||
cur = match cur.succ_opt() { Some(d) => d, None => break };
|
||||
cur = match cur.succ_opt() {
|
||||
Some(d) => d,
|
||||
None => break,
|
||||
};
|
||||
}
|
||||
let last_wd = last.weekday().num_days_from_sunday();
|
||||
for _ in last_wd + 1..7 {
|
||||
@ -1582,7 +1656,6 @@ mod tests {
|
||||
assert!(html.contains("<b>Start:</b> "));
|
||||
assert!(html.contains("<b>End:</b> "));
|
||||
assert!(html.contains("<b>Organizer:</b> Family"));
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -1593,7 +1666,10 @@ mod tests {
|
||||
// Print the calendar row containing 28, 29, 30 for debug
|
||||
if let Some(row_start) = html.find("<tr>") {
|
||||
if let Some(row_end) = html[row_start..].find("</tr>") {
|
||||
println!("CALENDAR ROW: {}", &html[row_start..row_start+row_end+5]);
|
||||
println!(
|
||||
"CALENDAR ROW: {}",
|
||||
&html[row_start..row_start + row_end + 5]
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(html.contains("<b>Summary:</b> Multi-day Event"));
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user