From 0216fd3dd2831e19bc4312e1315283c53676af27 Mon Sep 17 00:00:00 2001
From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
Date: Wed, 2 Apr 2025 13:20:26 +1100
Subject: sidebar: time pane
Also some fixes for calendar recurring events
Also fix reminders time
---
scss/sidebar.scss | 169 +++++++++++++++++++++++++++-
src/modules/sidebar/index.tsx | 3 +-
src/modules/sidebar/modules/calendar.tsx | 186 +++++++++++++++++++++++++++++++
src/modules/sidebar/modules/upcoming.tsx | 10 +-
src/modules/sidebar/time.tsx | 14 +++
src/services/calendar.ts | 131 ++++++++++++----------
6 files changed, 446 insertions(+), 67 deletions(-)
create mode 100644 src/modules/sidebar/modules/calendar.tsx
create mode 100644 src/modules/sidebar/time.tsx
diff --git a/scss/sidebar.scss b/scss/sidebar.scss
index 10139e2..d0f221a 100644
--- a/scss/sidebar.scss
+++ b/scss/sidebar.scss
@@ -1,4 +1,5 @@
@use "sass:color";
+@use "sass:list";
@use "scheme";
@use "lib";
@use "font";
@@ -315,9 +316,9 @@
$-colours: scheme.$red, scheme.$sapphire, scheme.$flamingo, scheme.$maroon, scheme.$pink, scheme.$sky,
scheme.$peach, scheme.$yellow, scheme.$green, scheme.$rosewater, scheme.$mauve, scheme.$teal,
scheme.$blue;
- @for $i from 1 through length($-colours) {
- &.c#{$i} {
- background-color: nth($-colours, $i);
+ @for $i from 1 through list.length($-colours) {
+ &.calendar-#{$i} {
+ background-color: list.nth($-colours, $i);
}
}
}
@@ -790,4 +791,166 @@
}
}
}
+
+ .calendar {
+ @include lib.rounded(20);
+
+ background-color: color.change(scheme.$surface1, $alpha: 0.4);
+ padding: lib.s(15);
+
+ .calendar-view {
+ @include lib.spacing(10, true);
+
+ .header {
+ @include lib.spacing(10);
+
+ & > button {
+ @include lib.rounded(1000);
+ @include lib.element-decel;
+
+ background-color: color.change(scheme.$surface2, $alpha: 0.4);
+ min-width: lib.s(28);
+ min-height: lib.s(28);
+ font-size: lib.s(18);
+
+ &:first-child {
+ padding: 0 lib.s(10);
+ }
+
+ &:hover,
+ &:focus {
+ background-color: color.change(scheme.$surface2, $alpha: 0.6);
+ }
+
+ &:active {
+ background-color: color.change(scheme.$surface2, $alpha: 0.8);
+ }
+ }
+ }
+
+ .weekdays {
+ @include lib.spacing(10);
+
+ & > label {
+ min-width: lib.s(40);
+ font-weight: bold;
+ color: scheme.$subtext1;
+ }
+ }
+
+ .month {
+ @include lib.spacing(10, true);
+ }
+
+ .week {
+ @include lib.spacing(10);
+ }
+
+ .day {
+ @include lib.rounded(1000);
+ @include lib.element-decel;
+
+ min-width: lib.s(40);
+ min-height: lib.s(40);
+
+ &.dim {
+ color: scheme.$subtext0;
+ }
+
+ &.today:not(.dim) {
+ background-color: scheme.$primary;
+ color: color.change(scheme.$base, $alpha: 1);
+ }
+
+ &:hover,
+ &:focus {
+ color: scheme.$subtext0;
+ }
+
+ &:active {
+ color: color.change(scheme.$overlay2, $alpha: 1);
+ }
+
+ &.dim {
+ color: scheme.$subtext0;
+
+ &:hover,
+ &:focus {
+ color: color.change(scheme.$overlay2, $alpha: 1);
+ }
+
+ &:active {
+ color: color.change(scheme.$overlay1, $alpha: 1);
+ }
+ }
+
+ &.today:not(.dim) {
+ background-color: scheme.$primary;
+ color: color.change(scheme.$base, $alpha: 1);
+
+ &:hover,
+ &:focus {
+ background-color: color.mix(scheme.$primary, scheme.$base, 80%);
+ }
+
+ &:active {
+ background-color: color.mix(scheme.$primary, scheme.$base, 70%);
+ }
+ }
+
+ label {
+ margin-top: lib.s(8);
+ }
+
+ .indicator {
+ @include lib.rounded(10);
+ @include lib.element-decel;
+
+ min-height: lib.s(3);
+ margin: 0 lib.s(8);
+ }
+
+ $-max: 5;
+ @for $i from 1 through $-max {
+ &.events-#{$i} {
+ $-colour: color.mix(scheme.$red, scheme.$green, calc(100% / $-max) * $i);
+
+ .indicator {
+ background-color: $-colour;
+ }
+
+ &:hover .indicator,
+ &:focus .indicator {
+ background-color: color.mix($-colour, scheme.$base, 80%);
+ }
+
+ &:active .indicator {
+ background-color: color.mix($-colour, scheme.$base, 70%);
+ }
+
+ &.dim .indicator {
+ background-color: color.mix($-colour, scheme.$base, 60%);
+ }
+
+ &.today:not(.dim) {
+ $-colour: color.mix($-colour, color.complement(scheme.$primary), 50%);
+
+ .indicator {
+ background-color: $-colour;
+ }
+
+ &:hover .indicator,
+ &:focus .indicator {
+ background-color: color.mix($-colour, scheme.$base, 80%);
+ }
+
+ &:active .indicator {
+ background-color: color.mix($-colour, scheme.$base, 70%);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
}
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 = [, , , , ];
+ const panes = [, , , , , ];
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) => {
+ 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 `${e.event.summary.replaceAll("&", "&")} • ${time}`;
+ })
+ .join("\n");
+ return `${events.length} event${events.length === 1 ? "" : "s"}\n${eventsStr}`;
+};
+
+const Day = ({ day, shown, current }: { day: ical.Time; shown: Variable; current: Variable }) => (
+
+);
+
+const CalendarView = ({ shown, current }: { shown: Variable; current: Variable }) => (
+
+
+
+
+ {["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map(d => (
+
+ ))}
+
+
+ {bind(current).as(c =>
+ getCalendarLayout(c).map(r => (
+
+ {r.map(d => (
+
+ ))}
+
+ ))
+ )}
+
+
+);
+const Events = ({ shown, current }: { shown: Variable; current: Variable }) => (
+
+);
+
+export default () => {
+ const shown = Variable("calendar");
+ const current = Variable(ical.Time.now());
+
+ return (
+
+
+
+
+
+
+ );
+};
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} ${e.event.summary}`;
};
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) => (
setupCustomTooltip(self, getEventTooltip(event), { useMarkup: true })}>
-
+
{event.event.location && }
@@ -55,7 +55,7 @@ const List = () => (
{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 => )
)}
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 = () => ;
+
+export default () => (
+
+
+
+
+
+
+
+);
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 = 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();
+
+ 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() {
--
cgit v1.2.3-freya