diff options
| author | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-04-02 13:20:26 +1100 |
|---|---|---|
| committer | 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> | 2025-04-02 13:20:26 +1100 |
| commit | 0216fd3dd2831e19bc4312e1315283c53676af27 (patch) | |
| tree | b5b838adfbda7496b2c9b9bf2252a4741f304373 /src | |
| parent | app: catch errors in main (diff) | |
| download | caelestia-shell-0216fd3dd2831e19bc4312e1315283c53676af27.tar.gz caelestia-shell-0216fd3dd2831e19bc4312e1315283c53676af27.tar.bz2 caelestia-shell-0216fd3dd2831e19bc4312e1315283c53676af27.zip | |
sidebar: time pane
Also some fixes for calendar recurring events
Also fix reminders time
Diffstat (limited to 'src')
| -rw-r--r-- | src/modules/sidebar/index.tsx | 3 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/calendar.tsx | 186 | ||||
| -rw-r--r-- | src/modules/sidebar/modules/upcoming.tsx | 10 | ||||
| -rw-r--r-- | src/modules/sidebar/time.tsx | 14 | ||||
| -rw-r--r-- | src/services/calendar.ts | 131 |
5 files changed, 280 insertions, 64 deletions
diff --git a/src/modules/sidebar/index.tsx b/src/modules/sidebar/index.tsx index d4c1855..9d61bae 100644 --- a/src/modules/sidebar/index.tsx +++ b/src/modules/sidebar/index.tsx @@ -7,6 +7,7 @@ import Connectivity from "./connectivity"; import Dashboard from "./dashboard"; import NotifPane from "./notifpane"; import Packages from "./packages"; +import Time from "./time"; @register() export default class SideBar extends Widget.Window { @@ -23,7 +24,7 @@ export default class SideBar extends Widget.Window { visible: false, }); - const panes = [<Dashboard />, <Audio />, <Connectivity />, <Packages />, <NotifPane />]; + const panes = [<Dashboard />, <Audio />, <Connectivity />, <Packages />, <NotifPane />, <Time />]; this.shown = Variable(panes[0].name); this.add( diff --git a/src/modules/sidebar/modules/calendar.tsx b/src/modules/sidebar/modules/calendar.tsx new file mode 100644 index 0000000..5cf56dd --- /dev/null +++ b/src/modules/sidebar/modules/calendar.tsx @@ -0,0 +1,186 @@ +import Calendar from "@/services/calendar"; +import { setupCustomTooltip } from "@/utils/widgets"; +import { bind, GLib, Variable } from "astal"; +import { Gtk } from "astal/gtk3"; +import ical from "ical.js"; + +const isLeapYear = (year: number) => year % 400 === 0 || (year % 4 === 0 && year % 100 !== 0); + +const getMonthDays = (month: number, year: number) => { + const leapYear = isLeapYear(year); + if (month === 2 && leapYear) return leapYear ? 29 : 28; + if ((month <= 7 && month % 2 === 1) || (month >= 8 && month % 2 === 0)) return 31; + return 30; +}; + +const getNextMonthDays = (month: number, year: number) => { + if (month === 12) return 31; + return getMonthDays(month + 1, year); +}; + +const getPrevMonthDays = (month: number, year: number) => { + if (month === 1) return 31; + return getMonthDays(month - 1, year); +}; + +export function getCalendarLayout(date: ical.Time) { + const weekdayOfMonthFirst = date.startOfMonth().dayOfWeek(ical.Time.MONDAY); + const daysInMonth = getMonthDays(date.month, date.year); + const daysInPrevMonth = getPrevMonthDays(date.month, date.year); + + const calendar: ical.Time[][] = []; + let idx = -weekdayOfMonthFirst + 2; + + for (let i = 0; i < 6; i++) { + calendar.push([]); + + for (let j = 0; j < 7; j++) { + let cDay = idx++; + let cMonth = date.month; + let cYear = date.year; + + if (idx < 0) { + cDay = daysInPrevMonth + cDay; + cMonth--; + + if (cMonth < 0) { + cMonth += 12; + cYear--; + } + } else if (idx > daysInMonth) { + cDay -= daysInMonth; + cMonth++; + + if (cMonth > 12) { + cMonth -= 12; + cYear++; + } + } + + calendar[i].push(ical.Time.fromData({ day: cDay, month: cMonth, year: cYear })); + } + } + + return calendar; +} + +const dateToMonthYear = (date: ical.Time) => { + const months = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", + ]; + return `${months[date.month - 1]} ${date.year}`; +}; + +const addMonths = (date: ical.Time, num: number) => { + date = date.clone(); + if (num > 0) for (let i = 0; i < num; i++) date.adjust(getNextMonthDays(date.month, date.year), 0, 0, 0); + else for (let i = 0; i > num; i--) date.adjust(-getPrevMonthDays(date.month, date.year), 0, 0, 0); + return date; +}; + +const getDayClassName = (day: ical.Time, current: Variable<ical.Time>) => { + const isToday = day.toJSDate().toDateString() === new Date().toDateString() ? "today" : ""; + const numEvents = Math.min(5, Calendar.get_default().getEventsForDay(day).length); + return `day ${isToday} ${day.month !== current.get().month ? "dim" : ""} events-${numEvents}`; +}; + +const getDayTooltip = (day: ical.Time) => { + const events = Calendar.get_default().getEventsForDay(day); + if (!events.length) return ""; + const eventsStr = events + .map(e => { + const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime()); + const end = GLib.DateTime.new_from_unix_local(e.endDate.toUnixTime()); + const sameAmPm = start.format("%P") === end.format("%P"); + const time = `${start.format(`%-I:%M${sameAmPm ? "" : "%P"}`)} — ${end.format("%-I:%M%P")}`; + return `<b>${e.event.summary.replaceAll("&", "&")}</b> • ${time}`; + }) + .join("\n"); + return `${events.length} event${events.length === 1 ? "" : "s"}\n${eventsStr}`; +}; + +const Day = ({ day, shown, current }: { day: ical.Time; shown: Variable<string>; current: Variable<ical.Time> }) => ( + <button + className={bind(Calendar.get_default(), "calendars").as(() => getDayClassName(day, current))} + cursor="pointer" + onClicked={() => { + shown.set("events"); + current.set(day); + }} + setup={self => + setupCustomTooltip( + self, + bind(Calendar.get_default(), "calendars").as(() => getDayTooltip(day)), + { useMarkup: true } + ) + } + > + <box vertical> + <label label={day.day.toString()} /> + <box className="indicator" /> + </box> + </button> +); + +const CalendarView = ({ shown, current }: { shown: Variable<string>; current: Variable<ical.Time> }) => ( + <box vertical className="calendar-view" name="calendar"> + <box className="header"> + <button + cursor="pointer" + onClicked={() => current.set(ical.Time.now())} + label={bind(current).as(dateToMonthYear)} + /> + <box hexpand /> + <button cursor="pointer" onClicked={() => current.set(addMonths(current.get(), -1))} label="" /> + <button cursor="pointer" onClicked={() => current.set(addMonths(current.get(), 1))} label="" /> + </box> + <box halign={Gtk.Align.CENTER} className="weekdays"> + {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map(d => ( + <label label={d} /> + ))} + </box> + <box vertical halign={Gtk.Align.CENTER} className="month"> + {bind(current).as(c => + getCalendarLayout(c).map(r => ( + <box className="week"> + {r.map(d => ( + <Day day={d} shown={shown} current={current} /> + ))} + </box> + )) + )} + </box> + </box> +); +const Events = ({ shown, current }: { shown: Variable<string>; current: Variable<ical.Time> }) => ( + <box className="events" name="events"></box> +); + +export default () => { + const shown = Variable("calendar"); + const current = Variable(ical.Time.now()); + + return ( + <box vertical className="calendar"> + <stack + transitionType={Gtk.StackTransitionType.SLIDE_LEFT_RIGHT} + transitionDuration={150} + shown={bind(shown)} + > + <CalendarView shown={shown} current={current} /> + <Events shown={shown} current={current} /> + </stack> + </box> + ); +}; diff --git a/src/modules/sidebar/modules/upcoming.tsx b/src/modules/sidebar/modules/upcoming.tsx index 816dff8..0023e31 100644 --- a/src/modules/sidebar/modules/upcoming.tsx +++ b/src/modules/sidebar/modules/upcoming.tsx @@ -4,7 +4,7 @@ import { bind, GLib } from "astal"; import { Gtk } from "astal/gtk3"; const getDateHeader = (events: IEvent[]) => { - const date = events[0].event.startDate; + const date = events[0].startDate; const isToday = date.toJSDate().toDateString() === new Date().toDateString(); return ( (isToday ? "Today • " : "") + @@ -14,13 +14,13 @@ const getDateHeader = (events: IEvent[]) => { }; const getEventHeader = (e: IEvent) => { - const start = GLib.DateTime.new_from_unix_local(e.event.startDate.toUnixTime()); + const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime()); const time = `${start.format("%-I")}${start.get_minute() > 0 ? `:${start.get_minute()}` : ""}${start.format("%P")}`; return `${time} <b>${e.event.summary}</b>`; }; const getEventTooltip = (e: IEvent) => { - const start = GLib.DateTime.new_from_unix_local(e.event.startDate.toUnixTime()); + const start = GLib.DateTime.new_from_unix_local(e.startDate.toUnixTime()); const end = GLib.DateTime.new_from_unix_local(e.event.endDate.toUnixTime()); const sameAmPm = start.format("%P") === end.format("%P"); const time = `${start.format(`%A, %-d %B • %-I:%M${sameAmPm ? "" : "%P"}`)} — ${end.format("%-I:%M%P")}`; @@ -31,7 +31,7 @@ const getEventTooltip = (e: IEvent) => { const Event = (event: IEvent) => ( <box className="event" setup={self => setupCustomTooltip(self, getEventTooltip(event), { useMarkup: true })}> - <box className={`calendar-indicator c${Calendar.get_default().getCalendarIndex(event.calendar)}`} /> + <box className={`calendar-indicator calendar-${Calendar.get_default().getCalendarIndex(event.calendar)}`} /> <box vertical> <label truncate useMarkup xalign={0} label={getEventHeader(event)} /> {event.event.location && <label truncate xalign={0} label={event.event.location} className="sublabel" />} @@ -55,7 +55,7 @@ const List = () => ( <box vertical valign={Gtk.Align.START}> {bind(Calendar.get_default(), "upcoming").as(u => Object.values(u) - .sort((a, b) => a[0].event.startDate.compare(b[0].event.startDate)) + .sort((a, b) => a[0].startDate.compare(b[0].startDate)) .map(e => <Day events={e} />) )} </box> diff --git a/src/modules/sidebar/time.tsx b/src/modules/sidebar/time.tsx new file mode 100644 index 0000000..c7b68ba --- /dev/null +++ b/src/modules/sidebar/time.tsx @@ -0,0 +1,14 @@ +import Calendar from "./modules/calendar"; +import Upcoming from "./modules/upcoming"; + +const TimeDate = () => <box></box>; + +export default () => ( + <box vertical className="pane time" name="time"> + <TimeDate /> + <box className="separator" /> + <Upcoming /> + <box className="separator" /> + <Calendar /> + </box> +); diff --git a/src/services/calendar.ts b/src/services/calendar.ts index 9372066..a2cdd30 100644 --- a/src/services/calendar.ts +++ b/src/services/calendar.ts @@ -17,6 +17,8 @@ import ical from "ical.js"; export interface IEvent { calendar: string; event: ical.Event; + startDate: ical.Time; + endDate: ical.Time; } @register({ GTypeName: "Calendar" }) @@ -35,6 +37,8 @@ export default class Calendar extends GObject.Object { #loading: boolean = false; #calendars: { [name: string]: ical.Component } = {}; #upcoming: { [date: string]: IEvent[] } = {}; + #cachedEvents: { [date: string]: IEvent[] } = {}; + #cachedMonths: Set<string> = new Set(); @property(Boolean) get loading() { @@ -60,6 +64,59 @@ export default class Calendar extends GObject.Object { return Object.keys(this.#calendars).indexOf(name) + 1; } + getEventsForMonth(date: ical.Time) { + const start = date.startOfMonth(); + + if (this.#cachedMonths.has(start.toJSDate().toDateString())) return this.#cachedEvents; + + this.#cachedMonths.add(start.toJSDate().toDateString()); + const end = date.endOfMonth(); + + const modDates = new Set<string>(); + + 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(end) <= 0; next = iter.next()) + if (next.compare(start) >= 0) { + const date = next.toJSDate().toDateString(); + if (!this.#cachedEvents.hasOwnProperty(date)) this.#cachedEvents[date] = []; + + const end = next.clone(); + end.addDuration(event.duration); + this.#cachedEvents[date].push({ calendar: name, event, startDate: next, endDate: end }); + modDates.add(date); + } + } else if (event.startDate.compare(start) >= 0 && event.startDate.compare(end) <= 0) { + const date = event.startDate.toJSDate().toDateString(); + if (!this.#cachedEvents.hasOwnProperty(date)) this.#cachedEvents[date] = []; + this.#cachedEvents[date].push({ + calendar: name, + event, + startDate: event.startDate, + endDate: event.endDate, + }); + modDates.add(date); + } + } + } + + for (const date of modDates) this.#cachedEvents[date].sort((a, b) => a.startDate.compare(b.startDate)); + + return this.#cachedEvents; + } + + getEventsForDay(date: ical.Time) { + return this.getEventsForMonth(date)[date.toJSDate().toDateString()] ?? []; + } + async updateCalendars() { this.#loading = true; this.notify("loading"); @@ -87,6 +144,9 @@ export default class Calendar extends GObject.Object { writeFileAsync(`${this.#cacheDir}/${webcal}`, icalStr).catch(console.error); } } + this.#cachedEvents = {}; + this.#cachedMonths.clear(); + this.notify("calendars"); this.updateUpcoming(); @@ -98,61 +158,34 @@ export default class Calendar extends GObject.Object { 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 (let i = 0; i < config.upcomingDays.get(); i++) { + const date = ical.Time.now().adjust(i, 0, 0, 0); + const events = this.getEventsForDay(date); + if (events.length > 0) this.#upcoming[date.toJSDate().toDateString()] = events; } - 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) { + #notifyEvent(event: IEvent) { 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` : ""; + const locIfExists = event.event.location ? ` ${event.event.location}\n` : ""; + const descIfExists = event.event.description ? ` ${event.event.description}\n` : ""; notify({ - summary: ` ${event.summary} `, - body: `${time}\n${locIfExists}${descIfExists} ${calendar}`, + summary: ` ${event.event.summary} `, + body: `${time}\n${locIfExists}${descIfExists} ${event.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))); + #createReminder(event: IEvent) { + const diff = event.startDate.toJSDate().getTime() - ical.Time.now().toJSDate().getTime(); + if (diff > 0) this.#reminders.push(timeout(diff, () => this.#notifyEvent(event))); } setReminders() { @@ -161,25 +194,7 @@ export default class Calendar extends GObject.Object { 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); - } - } + for (const events of Object.values(this.#upcoming)) for (const event of events) this.#createReminder(event); } constructor() { |