168 lines
6.1 KiB
TypeScript
168 lines
6.1 KiB
TypeScript
import { notify } from "@/utils/system";
|
|
import { execAsync, GLib, GObject, property, register, timeout, type AstalIO } 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: number = 1;
|
|
#reminders: AstalIO.Time[] = [];
|
|
#loading: boolean = 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 = {};
|
|
|
|
const today = ical.Time.now();
|
|
const upcoming = ical.Time.now().adjust(config.upcomingDays.get(), 0, 0, 0);
|
|
for (const [name, cal] of Object.entries(this.#calendars)) {
|
|
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] = [];
|
|
|
|
const rEvent = new ical.Event(e);
|
|
rEvent.startDate = next;
|
|
this.#upcoming[date].push({ calendar: name, event: rEvent });
|
|
}
|
|
} 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");
|
|
|
|
this.setReminders();
|
|
}
|
|
|
|
#notifyEvent(event: ical.Event, calendar: string) {
|
|
const start = GLib.DateTime.new_from_unix_local(event.startDate.toUnixTime());
|
|
const end = GLib.DateTime.new_from_unix_local(event.endDate.toUnixTime());
|
|
const time = `${start.format(`%A, %-d %B`)} • Now — ${end.format("%-I:%M%P")}`;
|
|
const locIfExists = event.location ? ` ${event.location}\n` : "";
|
|
const descIfExists = event.description ? ` ${event.description}\n` : "";
|
|
|
|
notify({
|
|
summary: ` ${event.summary} `,
|
|
body: `${time}\n${locIfExists}${descIfExists} ${calendar}`,
|
|
}).catch(console.error);
|
|
}
|
|
|
|
#createReminder(event: ical.Event, calendar: string, next: ical.Time) {
|
|
const diff = next.toUnixTime() - ical.Time.now().toUnixTime();
|
|
if (diff > 0) this.#reminders.push(timeout(diff * 1000, () => this.#notifyEvent(event, calendar)));
|
|
}
|
|
|
|
setReminders() {
|
|
this.#reminders.forEach(r => r.cancel());
|
|
this.#reminders = [];
|
|
|
|
if (!config.notify.get()) return;
|
|
|
|
const today = ical.Time.now();
|
|
const upcoming = ical.Time.now().adjust(config.upcomingDays.get(), 0, 0, 0);
|
|
for (const [name, cal] of Object.entries(this.#calendars)) {
|
|
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) this.#createReminder(event, name, next);
|
|
} else if (event.startDate.compare(today) >= 0 && event.startDate.compare(upcoming) <= 0)
|
|
// Create reminder if in upcoming range
|
|
this.#createReminder(event, name, event.startDate);
|
|
}
|
|
}
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.updateCalendars().catch(console.error);
|
|
config.webcals.subscribe(() => this.updateCalendars().catch(console.error));
|
|
config.upcomingDays.subscribe(() => this.updateUpcoming());
|
|
config.notify.subscribe(() => this.setReminders());
|
|
}
|
|
}
|