Add Calendar integration (read-only)
This commit is contained in:
+144
@@ -0,0 +1,144 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
"log"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
// calendarsURL is the page URL of calendarsDir ("/Calendars/").
|
||||
var calendarsURL string
|
||||
|
||||
// maintenanceHorizon is how many days after today the header widget lists.
|
||||
const maintenanceHorizon = 7
|
||||
|
||||
type maintItem struct {
|
||||
When string // "03:00" today; "Sa 27.09." or "Sa 27.09. 03:00" later
|
||||
Summary string
|
||||
}
|
||||
|
||||
type maintWidgetData struct {
|
||||
Count int // occurrences today (the badge)
|
||||
Today []maintItem
|
||||
Upcoming []maintItem
|
||||
PageURL string
|
||||
}
|
||||
|
||||
var maintWidgetTmpl = template.Must(template.New("widget.html").
|
||||
Funcs(template.FuncMap{"icon": readIcon}).
|
||||
ParseFS(assets, "assets/calendar/widget.html"))
|
||||
|
||||
// maintenanceWidget renders the header chip and its dropdown. It is a
|
||||
// template func so layout.html can call it without every page's data struct
|
||||
// carrying it. Empty when no maintenance calendar is configured.
|
||||
func maintenanceWidget() template.HTML {
|
||||
if calendarsDir == "" {
|
||||
return ""
|
||||
}
|
||||
names := readCalendarSettings(calendarsDir).Maintenance
|
||||
if len(names) == 0 {
|
||||
return ""
|
||||
}
|
||||
now := time.Now()
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
|
||||
tomorrow := today.AddDate(0, 0, 1)
|
||||
|
||||
data := maintWidgetData{PageURL: calendarsURL}
|
||||
for _, ev := range calendarOccurrences(calendarsDir, names, today, today.AddDate(0, 0, maintenanceHorizon+1)) {
|
||||
// Anything that started before tomorrow is due today, including
|
||||
// multi-day events still running.
|
||||
if ev.Start.Before(tomorrow) {
|
||||
item := maintItem{Summary: ev.Summary}
|
||||
if !ev.AllDay && !ev.Start.Before(today) {
|
||||
item.When = ev.Start.Format("15:04")
|
||||
}
|
||||
data.Today = append(data.Today, item)
|
||||
continue
|
||||
}
|
||||
when := calDayLabel(ev.Start)
|
||||
if !ev.AllDay {
|
||||
when += " " + ev.Start.Format("15:04")
|
||||
}
|
||||
data.Upcoming = append(data.Upcoming, maintItem{When: when, Summary: ev.Summary})
|
||||
}
|
||||
data.Count = len(data.Today)
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := maintWidgetTmpl.Execute(&buf, data); err != nil {
|
||||
log.Printf("maintenance widget template: %v", err)
|
||||
return ""
|
||||
}
|
||||
return template.HTML(buf.String())
|
||||
}
|
||||
|
||||
// maintSeriesRow is one task in the calendars page's maintenance table.
|
||||
type maintSeriesRow struct {
|
||||
Summary string
|
||||
Rule string
|
||||
Last string // most recent occurrence before today
|
||||
Next string // first occurrence today or later
|
||||
next time.Time
|
||||
}
|
||||
|
||||
// maintenanceWindow bounds the table's occurrence search, in days each way
|
||||
// from today. A year covers yearly tasks.
|
||||
const maintenanceWindow = 366
|
||||
|
||||
// maintenanceSeries lists every task of the named calendars in dir: one row
|
||||
// per recurring series with its last and next date, plus upcoming single
|
||||
// events. Sorted by next date; series without a next date come last.
|
||||
func maintenanceSeries(dir string, names []string, now time.Time) []maintSeriesRow {
|
||||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
|
||||
evs := calendarOccurrences(dir, names, today.AddDate(0, 0, -maintenanceWindow), today.AddDate(0, 0, maintenanceWindow))
|
||||
|
||||
type key struct{ calendar, uid string }
|
||||
rows := map[key]*maintSeriesRow{}
|
||||
var order []*maintSeriesRow
|
||||
for _, ev := range evs {
|
||||
if ev.Series == nil {
|
||||
if ev.Start.Before(today) {
|
||||
continue
|
||||
}
|
||||
order = append(order, &maintSeriesRow{
|
||||
Summary: ev.Summary,
|
||||
Rule: "once",
|
||||
Next: calDayLabel(ev.Start),
|
||||
next: ev.Start,
|
||||
})
|
||||
continue
|
||||
}
|
||||
k := key{ev.Calendar, ev.Series.UID}
|
||||
row := rows[k]
|
||||
if row == nil {
|
||||
row = &maintSeriesRow{Summary: ev.Series.Summary, Rule: ev.Series.Rule}
|
||||
rows[k] = row
|
||||
order = append(order, row)
|
||||
}
|
||||
// Occurrences arrive sorted by start: keep overwriting Last, take
|
||||
// the first one from today on as Next.
|
||||
if ev.Start.Before(today) {
|
||||
row.Last = calDayLabel(ev.Start)
|
||||
} else if row.next.IsZero() {
|
||||
row.next = ev.Start
|
||||
row.Next = calDayLabel(ev.Start)
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]maintSeriesRow, 0, len(order))
|
||||
for _, r := range order {
|
||||
out = append(out, *r)
|
||||
}
|
||||
sort.SliceStable(out, func(i, j int) bool {
|
||||
a, b := out[i].next, out[j].next
|
||||
if a.IsZero() != b.IsZero() {
|
||||
return b.IsZero()
|
||||
}
|
||||
if !a.Equal(b) {
|
||||
return a.Before(b)
|
||||
}
|
||||
return out[i].Summary < out[j].Summary
|
||||
})
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user