summaryrefslogtreecommitdiff
path: root/src/services/calendar.ts
blob: bc7c075006f0010a9099176c159ff188c2d10c33 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import { execAsync, GObject, property, register } from "astal";
import { calendar as config } from "config";
import ical from "ical.js";

export interface IEvent {
    calendar: string;
    event: ical.Event;
}

@register({ GTypeName: "Calendar" })
export default class Calendar extends GObject.Object {
    static instance: Calendar;
    static get_default() {
        if (!this.instance) this.instance = new Calendar();

        return this.instance;
    }

    #calCount = 1;
    #loading = false;
    #calendars: { [name: string]: ical.Component } = {};
    #upcoming: { [date: string]: IEvent[] } = {};

    @property(Boolean)
    get loading() {
        return this.#loading;
    }

    @property(Object)
    get calendars() {
        return this.#calendars;
    }

    @property(Object)
    get upcoming() {
        return this.#upcoming;
    }

    @property(Number)
    get numUpcoming() {
        return Object.values(this.#upcoming).reduce((acc, e) => acc + e.length, 0);
    }

    getCalendarIndex(name: string) {
        return Object.keys(this.#calendars).indexOf(name) + 1;
    }

    async updateCalendars() {
        this.#loading = true;
        this.notify("loading");

        this.#calendars = {};
        this.#calCount = 1;

        const cals = await Promise.allSettled(config.webcals.get().map(c => execAsync(["curl", c])));
        for (const cal of cals) {
            if (cal.status === "fulfilled") {
                const comp = new ical.Component(ical.parse(cal.value));
                const name = (comp.getFirstPropertyValue("x-wr-calname") ?? `Calendar ${this.#calCount++}`) as string;
                this.#calendars[name] = comp;
            } else console.error(`Failed to get calendar: ${cal.reason}`);
        }
        this.notify("calendars");

        this.updateUpcoming();

        this.#loading = false;
        this.notify("loading");
    }

    updateUpcoming() {
        this.#upcoming = {};

        for (const [name, cal] of Object.entries(this.#calendars)) {
            const today = ical.Time.now();
            const upcoming = ical.Time.now().adjust(config.upcomingDays.get(), 0, 0, 0);

            for (const e of cal.getAllSubcomponents()) {
                const event = new ical.Event(e);

                // Skip invalid events
                if (!event.startDate) continue;

                if (event.isRecurring()) {
                    // Recurring events
                    const iter = event.iterator();
                    for (let next = iter.next(); next && next.compare(upcoming) <= 0; next = iter.next())
                        if (next.compare(today) >= 0) {
                            const date = next.toJSDate().toDateString();
                            if (!this.#upcoming.hasOwnProperty(date)) this.#upcoming[date] = [];
                            this.#upcoming[date].push({ calendar: name, event });
                        }
                } else if (event.startDate.compare(today) >= 0 && event.startDate.compare(upcoming) <= 0) {
                    // Add to upcoming if in upcoming range
                    const date = event.startDate.toJSDate().toDateString();
                    if (!this.#upcoming.hasOwnProperty(date)) this.#upcoming[date] = [];
                    this.#upcoming[date].push({ calendar: name, event });
                }
            }
        }

        for (const events of Object.values(this.#upcoming))
            events.sort((a, b) => a.event.startDate.compare(b.event.startDate));

        this.notify("upcoming");
        this.notify("num-upcoming");
    }

    constructor() {
        super();

        this.updateCalendars().catch(console.error);
        config.webcals.subscribe(() => this.updateCalendars().catch(console.error));
        config.upcomingDays.subscribe(() => this.updateUpcoming());
    }
}