145 lines
4.1 KiB
Go
145 lines
4.1 KiB
Go
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
|
|
}
|