-
Notifications
You must be signed in to change notification settings - Fork 0
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
base: main
Are you sure you want to change the base?
Changes from 16 commits
13b0c8c
d62adee
6441bff
a81e6df
ce1219e
95d7eb8
d5aa719
27a4c2d
ed28a87
6eef9c1
0058b66
9bcd2de
f16d1bf
0a1732f
a4c5919
36797f0
eb337f7
f871653
7bea86d
b99a10e
84305e5
537abc0
b5749ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,18 @@ | ||||||
export function SetupCssHotReload(dest: string) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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}`); | ||||||
}); | ||||||
}); | ||||||
} |
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; | ||
} |
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. lmao |
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,6 +29,7 @@ export let config = { | |
enable: opt<boolean>(true), | ||
}, | ||
}, | ||
development: opt<boolean>(false), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe instead an environment variable could be used for this |
||
}; | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -346,7 +346,6 @@ window.darkened { | |
} | ||
} | ||
} | ||
|
||
} | ||
|
||
.osd-popup { | ||
|
@@ -385,4 +384,61 @@ window.darkened { | |
|
||
} | ||
} | ||
} | ||
} | ||
|
||
.timemenu { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
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.; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. GNOME uses EDIT: |
||
|
||
return Widget.Box({ | ||
className: "image", | ||
child: Widget.Icon({ | ||
icon, | ||
className: "icon", | ||
hpack: "center", | ||
vpack: "center", | ||
hexpand: true, | ||
vexpand: true, | ||
}), | ||
}); | ||
}; |
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)], | ||
}); | ||
}; |
There was a problem hiding this comment.
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