diff options
Diffstat (limited to 'src/services')
| -rw-r--r-- | src/services/calendar.ts | 168 | ||||
| -rw-r--r-- | src/services/monitors.ts | 2 | ||||
| -rw-r--r-- | src/services/schemes.ts | 3 | ||||
| -rw-r--r-- | src/services/wallpapers.ts | 3 |
4 files changed, 172 insertions, 4 deletions
diff --git a/src/services/calendar.ts b/src/services/calendar.ts new file mode 100644 index 0000000..9743aad --- /dev/null +++ b/src/services/calendar.ts @@ -0,0 +1,168 @@ +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()); + } +} diff --git a/src/services/monitors.ts b/src/services/monitors.ts index 4cef256..6ae7ecb 100644 --- a/src/services/monitors.ts +++ b/src/services/monitors.ts @@ -55,7 +55,7 @@ export class Monitor extends GObject.Object { .then(out => { this.isDdc = out.split("\n\n").some(display => { if (!/^Display \d+/.test(display)) return false; - const lines = display.split("\n"); + const lines = display.split("\n").map(l => l.trimStart()); if (lines.find(l => l.startsWith("Monitor:"))?.split(":")[3] !== monitor.serial) return false; this.busNum = lines.find(l => l.startsWith("I2C bus:"))?.split("/dev/i2c-")[1]; return this.busNum !== undefined; diff --git a/src/services/schemes.ts b/src/services/schemes.ts index 548975c..2808b55 100644 --- a/src/services/schemes.ts +++ b/src/services/schemes.ts @@ -32,7 +32,6 @@ export default class Schemes extends GObject.Object { } readonly #schemeDir: string = `${DATA}/scripts/data/schemes`; - readonly #monitor; #map: { [k: string]: Scheme } = {}; @@ -106,7 +105,7 @@ export default class Schemes extends GObject.Object { super(); this.update().catch(console.error); - this.#monitor = monitorDirectory(this.#schemeDir, (_m, file, _f, type) => { + monitorDirectory(this.#schemeDir, (_m, file, _f, type) => { if (type !== Gio.FileMonitorEvent.DELETED) this.updateFile(file).catch(console.error); }); } diff --git a/src/services/wallpapers.ts b/src/services/wallpapers.ts index 0e0e1de..4c7c49b 100644 --- a/src/services/wallpapers.ts +++ b/src/services/wallpapers.ts @@ -40,7 +40,8 @@ export default class Wallpapers extends GObject.Object { async #thumbnail(path: string) { const dir = path.slice(1, path.lastIndexOf("/")).replaceAll("/", "-"); const thumbPath = `${this.#thumbnailDir}/${dir}-${basename(path)}.jpg`; - await execAsync(`magick -define jpeg:size=1000x500 ${path} -thumbnail 500x250 -unsharp 0x.5 ${thumbPath}`); + if (!GLib.file_test(thumbPath, GLib.FileTest.EXISTS)) + await execAsync(`magick -define jpeg:size=1000x500 ${path} -thumbnail 500x250 -unsharp 0x.5 ${thumbPath}`); return thumbPath; } |