Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a notification panel #18

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
13b0c8c
feat-wip: Scaffold timemenu window
exellentcoin26 Oct 21, 2024
d62adee
feat-wip: Struggle with css (HARD)
exellentcoin26 Oct 21, 2024
6441bff
feat-wip(notification-menu): Use popup window helper
exellentcoin26 Nov 1, 2024
a81e6df
refactor: Move setup functionality to `Box::setup`
exellentcoin26 Nov 3, 2024
ce1219e
fix(notification-menu): Inconsistent amount of notifications
exellentcoin26 Nov 3, 2024
95d7eb8
Merge remote-tracking branch 'origin/main' into feat/notification-menu
exellentcoin26 Nov 4, 2024
d5aa719
fix(notification-menu): Remove notifications that override other noti…
exellentcoin26 Nov 5, 2024
27a4c2d
style-wip(notification-menu): Start styling notification menu
exellentcoin26 Nov 5, 2024
ed28a87
feat-wip(notification-menu): Start improving notification features
exellentcoin26 Nov 5, 2024
6eef9c1
style-wip(notification-menu): Continue styling notifications
exellentcoin26 Nov 9, 2024
0058b66
feat(dev): Add hot reloading off css styles
exellentcoin26 Nov 13, 2024
9bcd2de
fix(notification-menu): Move around classnames to be more consistent
exellentcoin26 Nov 13, 2024
f16d1bf
Merge remote-tracking branch 'origin/main' into feat/notification-menu
exellentcoin26 Nov 13, 2024
0a1732f
feat-wip(notification-menu): Hide the close button when hovering
exellentcoin26 Nov 13, 2024
a4c5919
format: Run biome format
exellentcoin26 Nov 14, 2024
36797f0
chore: Only enable css hot-reload in development
exellentcoin26 Nov 14, 2024
eb337f7
refactor: make notification center wider
AndreasHGK Nov 15, 2024
f871653
refactor: change placeholder styling
AndreasHGK Nov 15, 2024
7bea86d
refactor: make notification styling more like GNOME
AndreasHGK Dec 12, 2024
b99a10e
chore: remove unused icons file
AndreasHGK Dec 12, 2024
84305e5
refactor: reduce notification text size
AndreasHGK Dec 12, 2024
537abc0
refactor: improve notification close button positioning
AndreasHGK Dec 12, 2024
b5749ff
chore: remove unused code
AndreasHGK Dec 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ags/bar/widgets/time.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ export const Time = () =>
className: "time",
label: time.bind(),
}),
onClicked: () => Utils.execAsync("swaync-client -t"),
on_clicked: () => App.toggleWindow("timemenu"),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a better name for the window would be notification-center, same for the file name

});
18 changes: 18 additions & 0 deletions ags/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,24 @@ async function compileMain(dest) {
await compileStyles(dest);
await compileMain(dest);

// Set up hot reloading of styles.
Utils.monitorFile(`${App.configDir}/style.scss`, (_, event) => {
if (event !== 0) {
// Not a changed event.
return;
}

print("Hot reloading styles.");
Promise.resolve(compileStyles(dest))
.then((_) => {
App.resetCss();
App.applyCss(`${dest}/style.css`);
})
.catch((reason) => {
print(`Hot reloading error: ${reason}`);
});
});
Comment on lines +38 to +54
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you intend to include the code for hot reloading twice? (here and in the develop.ts file)


(await import(`file://${dest}/main.js`)).main(dest);
} catch (err) {
console.error(err);
Expand Down
18 changes: 18 additions & 0 deletions ags/lib/develop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function SetupCssHotReload(dest: string) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
export function SetupCssHotReload(dest: string) {
export function setupCssHotReload(dest: string) {

Utils.monitorFile(`${App.configDir}/style.scss`, (_, event) => {
if (event !== 0) {
// Not a changed event.
return;
}

print("Hot reloading styles.");
Promise.resolve(compileStyles(dest))
.then((_) => {
App.resetCss();
App.applyCss(`${dest}/style.css`);
})
.catch((reason) => {
print(`Hot reloading error: ${reason}`);
});
});
}
9 changes: 9 additions & 0 deletions ags/lib/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import GLib from "gi://GLib";

export function formatTime(value: number, format = "%H:%M"): string {
const result = GLib.DateTime.new_from_unix_local(value).format(format);
if (result === null) {
throw new Error(`Failed to format time using \`${format}\` format.`);
}
return result;
}
49 changes: 49 additions & 0 deletions ags/lib/option-rs.ts
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lmao
(ps: why not name it option.ts?)

Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
export abstract class Option<T> {
abstract orElse(fn: () => Option<T>): Option<T>;
abstract andThen<U>(fn: (value: T) => Option<U>): Option<U>;
abstract unwrapOr(value: T): T;

static from<T>(value: T | null | undefined): Some<T> | None<T> {
return value !== null && value !== undefined ? new Some(value) : new None();
}

isSome(): boolean {
return this instanceof Some;
}

isNone(): boolean {
return this instanceof None;
}
}

export class Some<T> extends Option<T> {
constructor(public value: T) {
super();
}

orElse(_fn: () => Option<T>): Option<T> {
return this;
}

andThen<U>(fn: (value: T) => Option<U>): Option<U> {
return fn(this.value);
}

unwrapOr(_value: T): T {
return this.value;
}
}

export class None<T> extends Option<T> {
orElse(fn: () => Option<T>): Option<T> {
return fn();
}

andThen<U>(_fn: (value: T) => Option<U>): Option<U> {
return new None();
}

unwrapOr(value: T): T {
return value;
}
}
1 change: 1 addition & 0 deletions ags/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export let config = {
enable: opt<boolean>(true),
},
},
development: opt<boolean>(false),
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe instead an environment variable could be used for this

};

/**
Expand Down
1 change: 0 additions & 1 deletion ags/lib/widgets.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type Gtk from "gi://Gtk?version=3.0";
import { Binding } from "types/service";

/** Return only the children that are not equal to null. */
export function conditionalChildren(children: (Gtk.Widget | null)[]): Gtk.Widget[] {
Expand Down
7 changes: 6 additions & 1 deletion ags/main.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import Gdk from "gi://Gdk";
import type Gtk from "gi://Gtk?version=3.0";

import { SetupCssHotReload } from "lib/develop";
import { VolumePopup } from "osd-popup/osd-popup.js";
import { Bar } from "./bar/bar.js";
import { config, readConfig } from "./lib/settings.js";
import { Quicksettings } from "./quicksettings/quicksettings.js";
import TimeMenu from "./timemenu/timemenu";

function forMonitors(widget: (monitor: number) => Gtk.Window) {
const n = Gdk.Display.get_default()?.get_n_monitors() || 1;
Expand All @@ -13,10 +15,13 @@ function forMonitors(widget: (monitor: number) => Gtk.Window) {

export function main(dest: string): void {
readConfig();

if (config.development) SetupCssHotReload(dest);

App.config({
style: `${dest}/style.css`,
windows: () => {
const windows = [...forMonitors(Bar), Quicksettings()];
const windows = [...forMonitors(Bar), Quicksettings(), TimeMenu()];

if (config.popups?.volumePopup?.enable) {
windows.push(VolumePopup());
Expand Down
60 changes: 58 additions & 2 deletions ags/style.scss
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,6 @@ window.darkened {
}
}
}

}

.osd-popup {
Expand Down Expand Up @@ -385,4 +384,61 @@ window.darkened {

}
}
}
}

.timemenu {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also rename the style to notification-center

@include space-between-y(1.5em);

padding: 1em;
min-width: 24em;
min-height: 26em;

border-radius: 1.5em;

.notifications {

.placeholder {

>.icon,
>label {
font-size: 1.22em;
}

>.icon {
margin-right: 1em;
}
}

.list {
@include space-between-y(0.75em);

.notification>.event-box {
border-radius: 0.5em;
border: 1px solid scale-color($background1, $lightness: 7.5%);
background-color: $surface0;
box-shadow: 0 0.1em 0.25em rgba(20, 20, 20, 0.25);

&:hover {
box-shadow: inset 1px 1px 1px 1px $surface0;
background-color: hover-color($surface0);
}

.content {
padding: 0.5em;

.image {
$icon-width: 4em;

margin-right: 1em;
min-width: $icon-width;
min-height: $icon-width;

>.icon {
font-size: $icon-width;
}
}
}
}
}
}
}
38 changes: 38 additions & 0 deletions ags/timemenu/icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { Notification as NotificationInfo } from "types/service/notifications";

import { None, Option, Some } from "lib/option-rs";

const iconAsOption = (icon: string): Option<string> => {
return Utils.lookUpIcon(icon) ? new Some(icon) : new None();
};

export default ({ app_entry, app_icon, image }: NotificationInfo) => {
if (image !== undefined) {
return Widget.Box({
className: "image",
css: `
background-image: url("${image}");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
`,
});
}

const icon = Option.from(app_entry)
.andThen(iconAsOption)
.orElse(() => iconAsOption(app_icon))
.unwrapOr(""); // TODO: Add some fallback icon here.;
Copy link
Owner

@AndreasHGK AndreasHGK Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GNOME uses application-x-executable-symbolic, i think that is probably the most appropriate. Im assuming this is the icon of the app sending the notification and not the custom icon provided in the notification itself.

EDIT: item-missing-symbolic is also nice, idk which one i prefer


return Widget.Box({
className: "image",
child: Widget.Icon({
icon,
className: "icon",
hpack: "center",
vpack: "center",
hexpand: true,
vexpand: true,
}),
});
};
91 changes: 91 additions & 0 deletions ags/timemenu/notification-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { Variable as VariableType } from "types/variable";

import Notification from "./notification";

const notificationService = await Service.import("notifications");

type NotificationMap = VariableType<Map<number, ReturnType<typeof Notification>>>;

const Placeholder = (notifications: NotificationMap) =>
Widget.Box({
visible: notifications.bind().as((m) => m.size === 0),
className: "placeholder",
hexpand: true,
vexpand: true,
hpack: "center",
vpack: "center",
children: [
Widget.Icon({ className: "icon", icon: "notifications-disabled-symbolic" }),
Widget.Label("Your inbox is empty"),
],
});

const NotificationList = (notifications: NotificationMap) => {
const notify = (self: ReturnType<typeof Widget.Box>, id: number | undefined) => {
if (id === undefined) return;
const result = notifications.getValue();

// Remove notification with the same id.
// These are notifications that are meant to replace other notifications
// (e.g., currently playing song).
const replacedNotification = result.get(id);
if (replacedNotification !== undefined) {
result.delete(id);
replacedNotification.destroy();
}

const info = notificationService.getNotification(id);
if (info === undefined) return;

const notification = Notification(info);
notifications.setValue(result.set(id, notification));
self.children = [notification, ...self.children];
};

const remove = (id: number | undefined) => {
if (id === undefined) return;

const m = notifications.getValue();
const notification = m.get(id);
if (notification === undefined) {
return;
}

m.delete(id);
notification.destroy();
notifications.setValue(m);
};

return Widget.Scrollable({
visible: notifications.bind().as((n) => n.size > 0),
vexpand: true,
hscroll: "never",
child: Widget.Box({
className: "list",
vertical: true,
children: notificationService.notifications.map((info) => {
const notification = Notification(info);
notifications.setValue(notifications.getValue().set(info.id, notification));
return notification;
}),
setup(self) {
self
.hook(
notificationService,
(_: unknown, id: number | undefined) => notify(self, id),
"notified",
)
.hook(notificationService, (_: unknown, id: number | undefined) => remove(id), "closed");
},
}),
});
};

export default () => {
// Keep track of registered notifications.
const notifications: NotificationMap = Variable(new Map());
return Widget.Box({
className: "notifications",
children: [Placeholder(notifications), NotificationList(notifications)],
});
};
Loading