From e8a40f31c904baeaa1817cd3e418df5ce71302c1 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 25 Mar 2025 21:51:59 +1100 Subject: sidebar: create upcoming module Requires ical.js and curl --- app.tsx | 2 + package-lock.json | 7 ++ package.json | 1 + scss/sidebar.scss | 158 +++++++++++++++++++++---------- src/config/defaults.ts | 4 + src/config/index.ts | 1 + src/config/types.ts | 2 + src/modules/sidebar/dashboard.tsx | 3 + src/modules/sidebar/modules/upcoming.tsx | 83 ++++++++++++++++ src/services/calendar.ts | 116 +++++++++++++++++++++++ src/utils/widgets.ts | 8 +- 11 files changed, 332 insertions(+), 53 deletions(-) create mode 100644 src/modules/sidebar/modules/upcoming.tsx diff --git a/app.tsx b/app.tsx index 5d862ca..135dce8 100644 --- a/app.tsx +++ b/app.tsx @@ -5,6 +5,7 @@ import Osds from "@/modules/osds"; import Popdowns from "@/modules/popdowns"; import Session from "@/modules/session"; import SideBar from "@/modules/sidebar"; +import Calendar from "@/services/calendar"; import Monitors from "@/services/monitors"; import Palette from "@/services/palette"; import Players from "@/services/players"; @@ -82,6 +83,7 @@ App.start({ timeout(1000, () => { idle(() => Schemes.get_default()); idle(() => Wallpapers.get_default()); + idle(() => Calendar.get_default()); }); console.log(`Caelestia started in ${Date.now() - now}ms`); diff --git a/package-lock.json b/package-lock.json index 6e56f15..c8c9317 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,6 +6,7 @@ "": { "dependencies": { "fuzzysort": "^3.1.0", + "ical.js": "^2.1.0", "mathjs": "^14.0.1" }, "devDependencies": { @@ -534,6 +535,12 @@ "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==", "license": "MIT" }, + "node_modules/ical.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ical.js/-/ical.js-2.1.0.tgz", + "integrity": "sha512-BOVfrH55xQ6kpS3muGvIXIg2l7p+eoe12/oS7R5yrO3TL/j/bLsR0PR+tYQESFbyTbvGgPHn9zQ6tI4FWyuSaQ==", + "license": "MPL-2.0" + }, "node_modules/javascript-natural-sort": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", diff --git a/package.json b/package.json index c530938..4574f96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "dependencies": { "fuzzysort": "^3.1.0", + "ical.js": "^2.1.0", "mathjs": "^14.0.1" }, "devDependencies": { diff --git a/scss/sidebar.scss b/scss/sidebar.scss index 84d1d61..249f516 100644 --- a/scss/sidebar.scss +++ b/scss/sidebar.scss @@ -30,6 +30,49 @@ margin: 0 lib.s(10); } + .header-bar { + margin-bottom: lib.s(10); + + @include lib.spacing; + + & > :not(button) { + font-weight: bold; + font-size: lib.s(16); + } + + & > button { + @include lib.element-decel; + @include lib.rounded(10); + + padding: lib.s(3) lib.s(8); + + &:hover, + &:focus { + color: scheme.$subtext0; + } + + &:active { + color: scheme.$overlay2; + } + + &.enabled { + $-base: color.change(scheme.$base, $alpha: 1); + + background-color: scheme.$primary; + color: $-base; + + &:hover, + &:focus { + background-color: color.mix(scheme.$primary, $-base, 80%); + } + + &:active { + background-color: color.mix(scheme.$primary, $-base, 70%); + } + } + } + } + .user { @include lib.spacing(15); @@ -79,6 +122,7 @@ font-size: lib.s(64); font-weight: bold; background-color: scheme.$base; + color: scheme.$subtext0; } .details { @@ -133,86 +177,98 @@ } } - .notifications { - .header-bar { - margin-bottom: lib.s(10); - margin-right: lib.s(-10); + .notification { + .wrapper { + padding-bottom: lib.s(10); + } + + .inner { + @include lib.rounded(20); + + background-color: color.change(scheme.$surface1, $alpha: 0.4); + + &.low { + @include notification(if(scheme.$light, scheme.$surface1, scheme.$overlay0)); + } + + &.normal { + @include lib.border(scheme.$primary, if(scheme.$light, 0.5, 0.3)); + @include notification(scheme.$primary); + } + + &.critical { + @include lib.border(scheme.$error, 0.8); + @include notification(scheme.$error); + } + } + .actions { @include lib.spacing; & > button { - @include lib.element-decel; @include lib.rounded(10); + @include lib.element-decel; - padding: lib.s(3) lib.s(8); + padding: lib.s(5) lib.s(10); + background-color: color.change(scheme.$surface1, $alpha: 0.5); &:hover, &:focus { - color: scheme.$subtext0; + background-color: color.change(scheme.$surface2, $alpha: 0.5); } &:active { - color: scheme.$overlay2; + background-color: color.change(scheme.$overlay0, $alpha: 0.5); } + } + } + } - &.enabled { - background-color: scheme.$primary; - color: scheme.$base; + .upcoming { + .list { + min-height: lib.s(300); + } - &:hover, - &:focus { - background-color: color.mix(scheme.$primary, scheme.$base, 80%); - } + .day { + @include lib.spacing($vertical: true); - &:active { - background-color: color.mix(scheme.$primary, scheme.$base, 70%); - } - } + &:not(:first-child) { + margin-top: lib.s(20); } - } - .notification { - .wrapper { - padding-bottom: lib.s(10); + .date { + margin-left: lib.s(10); } - .inner { + .sublabel { + font-size: lib.s(14); + color: scheme.$subtext0; + } + + .events { @include lib.rounded(20); background-color: color.change(scheme.$surface1, $alpha: 0.4); + padding: lib.s(10) lib.s(15); - &.low { - @include notification(if(scheme.$light, scheme.$surface1, scheme.$overlay0)); - } - - &.normal { - @include lib.border(scheme.$primary, if(scheme.$light, 0.5, 0.3)); - @include notification(scheme.$primary); - } - - &.critical { - @include lib.border(scheme.$error, 0.8); - @include notification(scheme.$error); - } + @include lib.spacing(10, true); } - .actions { - @include lib.spacing; - - & > button { - @include lib.rounded(10); - @include lib.element-decel; + .event { + @include lib.spacing(8); + } - padding: lib.s(5) lib.s(10); - background-color: color.change(scheme.$surface1, $alpha: 0.5); + .calendar-indicator { + @include lib.rounded(5); - &:hover, - &:focus { - background-color: color.change(scheme.$surface2, $alpha: 0.5); - } + min-width: lib.s(1); - &:active { - background-color: color.change(scheme.$overlay0, $alpha: 0.5); + $-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); } } } diff --git a/src/config/defaults.ts b/src/config/defaults.ts index d699a45..b6bee4f 100644 --- a/src/config/defaults.ts +++ b/src/config/defaults.ts @@ -143,4 +143,8 @@ export default { }, ], }, + calendar: { + webcals: [] as string[], // An array of urls to ICS files which you can curl + upcomingDays: 7, // Number of days which count as upcoming + }, }; diff --git a/src/config/index.ts b/src/config/index.ts index d09a668..3f9bf7a 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -18,5 +18,6 @@ export const { memory, storage, wallpapers, + calendar, } = config; export default config; diff --git a/src/config/types.ts b/src/config/types.ts index aa0d921..cf828b6 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -68,4 +68,6 @@ export default { "memory.interval": NUM, "storage.interval": NUM, "wallpapers.paths": OBJ_ARR({ recursive: BOOL, path: STR }), + "calendar.webcals": ARR(STR), + "calendar.upcomingDays": NUM, } as { [k: string]: string | string[] | number[] }; diff --git a/src/modules/sidebar/dashboard.tsx b/src/modules/sidebar/dashboard.tsx index 86921e6..936502b 100644 --- a/src/modules/sidebar/dashboard.tsx +++ b/src/modules/sidebar/dashboard.tsx @@ -5,6 +5,7 @@ import { bind, GLib, monitorFile, Variable } from "astal"; import { Gtk } from "astal/gtk3"; import AstalMpris from "gi://AstalMpris"; import Notifications from "./modules/notifications"; +import Upcoming from "./modules/upcoming"; const lengthStr = (length: number) => `${Math.floor(length / 60)}:${Math.floor(length % 60) @@ -127,5 +128,7 @@ export default () => ( ))} + + ); diff --git a/src/modules/sidebar/modules/upcoming.tsx b/src/modules/sidebar/modules/upcoming.tsx new file mode 100644 index 0000000..e2389e8 --- /dev/null +++ b/src/modules/sidebar/modules/upcoming.tsx @@ -0,0 +1,83 @@ +import Calendar, { type IEvent } from "@/services/calendar"; +import { setupCustomTooltip } from "@/utils/widgets"; +import { bind, GLib } from "astal"; +import { Gtk } from "astal/gtk3"; + +const getDateHeader = (events: IEvent[]) => { + const date = events[0].event.startDate; + const isToday = date.toJSDate().toDateString() === new Date().toDateString(); + return ( + (isToday ? "Today • " : "") + + GLib.DateTime.new_from_unix_local(date.toUnixTime()).format("%B %-d • %A") + + ` • ${events.length} event${events.length === 1 ? "" : "s"}` + ); +}; + +const getEventHeader = (e: IEvent) => { + const start = GLib.DateTime.new_from_unix_local(e.event.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 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")}`; + const locIfExists = e.event.location ? ` ${e.event.location}\n` : ""; + const descIfExists = e.event.description ? `󰒿 ${e.event.description}\n` : ""; + return `${e.event.summary}\n${time}\n${locIfExists}${descIfExists}󰃭 ${e.calendar}`; +}; + +const Event = (event: IEvent) => ( + setupCustomTooltip(self, getEventTooltip(event), { useMarkup: true })}> + + + + +); + +const Day = ({ events }: { events: IEvent[] }) => ( + + +); + +const List = () => ( + + {bind(Calendar.get_default(), "upcoming").as(u => + Object.values(u) + .sort((a, b) => a[0].event.startDate.compare(b[0].event.startDate)) + .map(e => ) + )} + +); + +export default () => ( + + +