refactor(ags): typecheck notif logic

This commit is contained in:
matt1432 2023-12-20 17:14:07 -05:00
parent f7ced94c21
commit 42a168762f
10 changed files with 236 additions and 149 deletions

View file

@ -9,7 +9,10 @@ import { Box, Entry, Icon, Label, ListBox, Revealer, Scrollable } from 'resource
import PopupWindow from '../misc/popup.js';
import AppItem from './app-item.js';
/** @typedef {import('types/service/applications.js').Application} App */
/**
* @typedef {import('types/service/applications.js').Application} App
* @typedef {typeof imports.gi.Gtk.ListBoxRow} ListBoxRow
*/
const Applauncher = ({ window_name = 'applauncher' } = {}) => {
@ -32,17 +35,23 @@ const Applauncher = ({ window_name = 'applauncher' } = {}) => {
fzfResults = fzf.find(text);
// @ts-expect-error
list.set_sort_func((a, b) => {
const row1 = a.get_children()[0]?.attribute.app.name;
const row2 = b.get_children()[0]?.attribute.app.name;
list.set_sort_func(
/**
* @param {ListBoxRow} a
* @param {ListBoxRow} b
*/
(a, b) => {
const row1 = a.get_children()[0]?.attribute.app.name;
const row2 = b.get_children()[0]?.attribute.app.name;
if (!row1 || !row2) {
return 0;
}
if (!row1 || !row2) {
return 0;
}
return fzfResults.indexOf(row1) -
return fzfResults.indexOf(row1) -
fzfResults.indexOf(row1) || 0;
});
},
);
};
const makeNewChildren = () => {
@ -96,7 +105,7 @@ const Applauncher = ({ window_name = 'applauncher' } = {}) => {
setSort(text);
let visibleApps = 0;
/** @type Array<typeof imports.gi.Gtk.ListBoxRow> */
/** @type Array<ListBoxRow> */
// @ts-expect-error
const rows = list.get_children();

View file

@ -135,6 +135,7 @@ export default () => {
const updateWorkspaces = () => {
Hyprland.workspaces.forEach((ws) => {
const currentWs = self.children.find((ch) => {
// @ts-expect-error
return ch.attribute.id === ws.id;
});

View file

@ -9,12 +9,13 @@ const ANIM_DURATION = 500;
const TRANSITION = `transition: margin ${ANIM_DURATION}ms ease,
opacity ${ANIM_DURATION}ms ease;`;
/**
* @typedef {import('types/widgets/overlay').OverlayProps} OverlayProps
* @typedef {import('types/widgets/overlay').default} Overlay
*
* @param {OverlayProps & {
*/
/** @param {OverlayProps & {
* setup?: function(Overlay):void
* }} o
*/

View file

@ -4,12 +4,13 @@ import { EventBox } from 'resource:///com/github/Aylur/ags/widget.js';
const { Gtk } = imports.gi;
/**
* @typedef {import('types/widgets/eventbox').EventBoxProps} EventBoxProps
* @typedef {import('types/widgets/eventbox').default} EventBox
*
* @param {EventBoxProps & {
*/
/** @param {EventBoxProps & {
* on_primary_click_release?: function(EventBox):void
* }} o
*/
@ -23,7 +24,7 @@ export default ({
const CanRun = Variable(true);
const Disabled = Variable(false);
let widget; // eslint-disable-line
let widget = EventBox();
const wrapper = EventBox({
cursor: 'pointer',
@ -35,7 +36,7 @@ export default ({
get_child: () => widget.child,
/** @param {import('types/widget').Widget} new_child */
/** @param {import('types/widgets/box').default} new_child */
set_child: (new_child) => {
widget.child = new_child;
},

View file

@ -23,8 +23,8 @@ import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
export default ({
transition = 'slide_down',
transition_duration = 500,
onOpen = () => { /**/ },
onClose = () => { /**/ },
onOpen = () => {/**/},
onClose = () => {/**/},
// Window props
name,
@ -42,8 +42,12 @@ export default ({
...props,
attribute: {
/**
* @param {typeof imports.gi.Gtk.Allocation} alloc
* @param {'left'|'right'} side
*/
set_x_pos: (
alloc = {},
alloc,
side = 'right',
) => {
const width = window.get_display()

View file

@ -8,28 +8,35 @@ import { lookUpIcon } from 'resource:///com/github/Aylur/ags/utils.js';
const { GLib } = imports.gi;
/** @typedef {import('types/service/notifications').Notification} Notification */
/** @param {number} time */
const setTime = (time) => {
return GLib.DateTime
.new_from_unix_local(time)
.format('%H:%M');
};
const getDragState = (box) => box.get_parent().get_parent()
.get_parent().get_parent().get_parent()._dragging;
/** @param {import('types/widgets/eventbox').default} box */
const getDragState = (box) => box.get_parent()?.get_parent()
// @ts-expect-error
?.get_parent()?.get_parent()?.get_parent()?.attribute.dragging;
import Gesture from './gesture.js';
import CursorBox from '../misc/cursorbox.js';
/** @param {Notification} notif */
const NotificationIcon = (notif) => {
let iconCmd = () => { /**/ };
/** @type function(import('types/widgets/eventbox').default):void */
let iconCmd = () => {/**/};
if (Applications.query(notif.appEntry).length > 0) {
const app = Applications.query(notif.appEntry)[0];
if (notif._appEntry && Applications.query(notif._appEntry).length > 0) {
const app = Applications.query(notif._appEntry)[0];
let wmClass = app.app.get_string('StartupWMClass');
if (app.app.get_filename().includes('discord')) {
if (app.app?.get_filename()?.includes('discord')) {
wmClass = 'discord';
}
@ -45,17 +52,19 @@ const NotificationIcon = (notif) => {
'togglespecialworkspace spot');
}
else {
Hyprland.sendMessage('j/clients').then((out) => {
out = JSON.parse(out);
Hyprland.sendMessage('j/clients').then((msg) => {
/** @type {Array<import('types/service/hyprland').Client>} */
const clients = JSON.parse(msg);
const classes = [];
for (const key of out) {
for (const key of clients) {
if (key.class) {
classes.push(key.class);
}
}
if (classes.includes(wmClass)) {
if (wmClass && classes.includes(wmClass)) {
Hyprland.sendMessage('dispatch ' +
`focuswindow ^(${wmClass})`);
}
@ -81,7 +90,7 @@ const NotificationIcon = (notif) => {
child: Box({
vpack: 'start',
hexpand: false,
className: 'icon img',
class_name: 'icon img',
css: `
background-image: url("${notif.image}");
background-size: contain;
@ -96,13 +105,13 @@ const NotificationIcon = (notif) => {
let icon = 'dialog-information-symbolic';
if (lookUpIcon(notif.appIcon)) {
icon = notif.appIcon;
if (lookUpIcon(notif._appIcon)) {
icon = notif._appIcon;
}
if (lookUpIcon(notif.appEntry)) {
icon = notif.appEntry;
if (notif._appEntry && lookUpIcon(notif._appEntry)) {
icon = notif._appEntry;
}
@ -112,7 +121,7 @@ const NotificationIcon = (notif) => {
child: Box({
vpack: 'start',
hexpand: false,
className: 'icon',
class_name: 'icon',
css: `
min-width: 78px;
min-height: 78px;
@ -132,11 +141,18 @@ const NotificationIcon = (notif) => {
// to know when there are notifs or not
export const HasNotifs = Variable(false);
/**
* @param {{
* notif: Notification
* slideIn?: 'Left'|'Right'
* command?: () => void
* }} o
*/
export const Notification = ({
notif,
slideIn = 'Left',
command = () => { /**/ },
} = {}) => {
}) => {
if (!notif) {
return;
}
@ -145,7 +161,7 @@ export const Notification = ({
'Spotify',
];
if (BlockedApps.find((app) => app === notif.appName)) {
if (BlockedApps.find((app) => app === notif._appName)) {
notif.close();
return;
@ -161,8 +177,9 @@ export const Notification = ({
});
// Add body to notif
// @ts-expect-error
notifWidget.child.add(Box({
className: `notification ${notif.urgency}`,
class_name: `notification ${notif.urgency}`,
vexpand: false,
// Notification
@ -174,6 +191,7 @@ export const Notification = ({
Box({
children: [
NotificationIcon(notif),
Box({
hexpand: true,
vertical: true,
@ -185,21 +203,21 @@ export const Notification = ({
// Title
Label({
className: 'title',
class_name: 'title',
xalign: 0,
justification: 'left',
hexpand: true,
maxWidthChars: 24,
max_width_chars: 24,
truncate: 'end',
wrap: true,
label: notif.summary,
useMarkup: notif.summary
use_markup: notif.summary
.startsWith('<'),
}),
// Time
Label({
className: 'time',
class_name: 'time',
vpack: 'start',
label: setTime(notif.time),
}),
@ -207,9 +225,12 @@ export const Notification = ({
// Close button
CursorBox({
child: Button({
className: 'close-button',
class_name: 'close-button',
vpack: 'start',
onClicked: () => notif.close(),
on_primary_click_release: () =>
notif.close(),
child: Icon('window-close' +
'-symbolic'),
}),
@ -219,9 +240,9 @@ export const Notification = ({
// Description
Label({
className: 'description',
class_name: 'description',
hexpand: true,
useMarkup: true,
use_markup: true,
xalign: 0,
justification: 'left',
label: notif.body,
@ -234,11 +255,13 @@ export const Notification = ({
// Actions
Box({
className: 'actions',
class_name: 'actions',
children: notif.actions.map((action) => Button({
className: 'action-button',
onClicked: () => notif.invoke(action.id),
class_name: 'action-button',
hexpand: true,
on_primary_click_release: () => notif.invoke(action.id),
child: Label(action.label),
})),
}),

View file

@ -7,7 +7,16 @@ import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
import { Notification, HasNotifs } from './base.js';
import CursorBox from '../misc/cursorbox.js';
/**
* @typedef {import('types/service/notifications').Notification} NotifObj
* @typedef {import('types/widgets/box').default} Box
*/
/**
* @param {Box} box
* @param {NotifObj} notif
*/
const addNotif = (box, notif) => {
if (notif) {
const NewNotif = Notification({
@ -27,7 +36,7 @@ const NotificationList = () => Box({
vertical: true,
vexpand: true,
vpack: 'start',
binds: [['visible', HasNotifs]],
visible: HasNotifs.bind(),
setup: (self) => {
self
@ -40,31 +49,42 @@ const NotificationList = () => Box({
}
else if (id) {
addNotif(box, Notifications.getNotification(id));
const notifObj = Notifications.getNotification(id);
if (notifObj) {
addNotif(box, notifObj);
}
}
}, 'notified')
.hook(Notifications, (box, id) => {
const notif = box.children.find((ch) => ch._id === id);
// @ts-expect-error
const notif = box.children.find((ch) => ch.attribute.id === id);
if (notif?.sensitive) {
notif.slideAway('Right');
// @ts-expect-error
notif.attribute.slideAway('Right');
}
}, 'closed');
},
});
// TODO: use cursorbox feature for this
// Needs to be wrapped to still have onHover when disabled
const ClearButton = () => CursorBox({
child: Button({
onPrimaryClickRelease: () => {
sensitive: HasNotifs.bind(),
on_primary_click_release: () => {
Notifications.clear();
timeout(1000, () => App.closeWindow('notification-center'));
},
binds: [['sensitive', HasNotifs]],
child: Box({
children: [
Label('Clear '),
Icon({
setup: (self) => {
self.hook(Notifications, () => {
@ -80,7 +100,7 @@ const ClearButton = () => CursorBox({
});
const Header = () => Box({
className: 'header',
class_name: 'header',
children: [
Label({
label: 'Notifications',
@ -93,14 +113,17 @@ const Header = () => Box({
const Placeholder = () => Revealer({
transition: 'crossfade',
binds: [['revealChild', HasNotifs, 'value', (value) => !value]],
reveal_child: HasNotifs.bind()
.transform((v) => !v),
child: Box({
className: 'placeholder',
class_name: 'placeholder',
vertical: true,
vpack: 'center',
hpack: 'center',
vexpand: true,
hexpand: true,
children: [
Icon('notification-disabled-symbolic'),
Label('Your inbox is empty'),
@ -109,22 +132,27 @@ const Placeholder = () => Revealer({
});
export default () => Box({
className: 'notification-center',
class_name: 'notification-center',
vertical: true,
children: [
Header(),
Box({
className: 'notification-wallpaper-box',
class_name: 'notification-wallpaper-box',
children: [
Scrollable({
className: 'notification-list-box',
class_name: 'notification-list-box',
hscroll: 'never',
vscroll: 'automatic',
child: Box({
className: 'notification-list',
class_name: 'notification-list',
vertical: true,
children: [
NotificationList(),
Placeholder(),
],
}),

View file

@ -5,90 +5,97 @@ import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
import { HasNotifs } from './base.js';
import Gtk from 'gi://Gtk';
const { Gtk } = imports.gi;
const MAX_OFFSET = 200;
const OFFSCREEN = 300;
const ANIM_DURATION = 500;
const SLIDE_MIN_THRESHOLD = 10;
const TRANSITION = 'transition: margin 0.5s ease, opacity 0.5s ease;';
const SQUEEZED = 'margin-bottom: -70px; margin-top: -70px;';
const MAX_LEFT = `
margin-left: -${Number(MAX_OFFSET + OFFSCREEN)}px;
margin-right: ${Number(MAX_OFFSET + OFFSCREEN)}px;
`;
const MAX_RIGHT = `
margin-left: ${Number(MAX_OFFSET + OFFSCREEN)}px;
margin-right: -${Number(MAX_OFFSET + OFFSCREEN)}px;
`;
const slideLeft = `${TRANSITION} ${MAX_LEFT}
margin-top: 0px;
margin-bottom: 0px;
opacity: 0;`;
const squeezeLeft = `${TRANSITION} ${MAX_LEFT} ${SQUEEZED} opacity: 0;`;
const slideRight = `${TRANSITION} ${MAX_RIGHT}
margin-top: 0px;
margin-bottom: 0px;
opacity: 0;`;
const squeezeRight = `${TRANSITION} ${MAX_RIGHT} ${SQUEEZED} opacity: 0;`;
const defaultStyle = `${TRANSITION} margin: unset; opacity: 1;`;
export default ({
id,
slideIn = 'Left',
command = () => { /**/ },
command = () => {/**/},
...props
}) => {
const widget = EventBox({
...props,
cursor: 'grab',
onHover: (self) => {
if (!self._hovered) {
self._hovered = true;
on_hover: (self) => {
if (!self.attribute.hovered) {
self.attribute.hovered = true;
}
},
onHoverLost: (self) => {
if (self._hovered) {
self._hovered = false;
on_hover_lost: (self) => {
if (self.attribute.hovered) {
self.attribute.hovered = false;
}
},
attribute: {
dragging: false,
hovered: false,
ready: false,
id,
/** @param {'Left'|'Right'} side */
slideAway: (side) => {
// Slide away
// @ts-expect-error
widget.child.setCss(side === 'Left' ? slideLeft : slideRight);
// Make it uninteractable
widget.sensitive = false;
timeout(ANIM_DURATION - 100, () => {
// Reduce height after sliding away
// @ts-expect-error
widget.child?.setCss(side === 'Left' ?
squeezeLeft :
squeezeRight);
timeout(ANIM_DURATION, () => {
// Kill notif and update HasNotifs after anim is done
command();
HasNotifs.value = Notifications
.notifications.length > 0;
// @ts-expect-error
widget.get_parent()?.remove;
});
});
},
},
});
// Properties
widget._dragging = false;
widget._hovered = false;
widget._id = id;
widget.ready = false;
const gesture = Gtk.GestureDrag.new(widget);
const TRANSITION = 'transition: margin 0.5s ease, opacity 0.5s ease;';
const SQUEEZED = 'margin-bottom: -70px; margin-top: -70px;';
const MAX_LEFT = `
margin-left: -${Number(MAX_OFFSET + OFFSCREEN)}px;
margin-right: ${Number(MAX_OFFSET + OFFSCREEN)}px;
`;
const MAX_RIGHT = `
margin-left: ${Number(MAX_OFFSET + OFFSCREEN)}px;
margin-right: -${Number(MAX_OFFSET + OFFSCREEN)}px;
`;
const slideLeft = `${TRANSITION} ${MAX_LEFT}
margin-top: 0px;
margin-bottom: 0px;
opacity: 0;`;
const squeezeLeft = `${TRANSITION} ${MAX_LEFT} ${SQUEEZED} opacity: 0;`;
const slideRight = `${TRANSITION} ${MAX_RIGHT}
margin-top: 0px;
margin-bottom: 0px;
opacity: 0;`;
const squeezeRight = `${TRANSITION} ${MAX_RIGHT} ${SQUEEZED} opacity: 0;`;
const defaultStyle = `${TRANSITION} margin: unset; opacity: 1;`;
// Notif methods
widget.slideAway = (side) => {
// Slide away
widget.child.setCss(side === 'Left' ? slideLeft : slideRight);
// Makie it uninteractable
widget.sensitive = false;
timeout(ANIM_DURATION - 100, () => {
// Reduce height after sliding away
widget.child?.setCss(side === 'Left' ? squeezeLeft : squeezeRight);
timeout(ANIM_DURATION, () => {
// Kill notif and update HasNotifs after anim is done
command();
HasNotifs.value = Notifications.notifications.length > 0;
widget.get_parent()?.remove(widget);
});
});
};
widget.add(Box({
css: squeezeLeft,
setup: (self) => {
@ -123,14 +130,16 @@ export default ({
}
// Put a threshold on if a click is actually dragging
widget._dragging = Math.abs(offset) > SLIDE_MIN_THRESHOLD;
widget.attribute.dragging =
Math.abs(offset) > SLIDE_MIN_THRESHOLD;
widget.cursor = 'grabbing';
}, 'drag-update')
// On drag end
.hook(gesture, () => {
// Make it slide in on init
if (!widget.ready) {
if (!widget.attribute.ready) {
// Reverse of slideAway, so it started at squeeze, then we go to slide
self.setCss(slideIn === 'Left' ?
slideLeft :
@ -140,7 +149,7 @@ export default ({
// Then we go to center
self.setCss(defaultStyle);
timeout(ANIM_DURATION, () => {
widget.ready = true;
widget.attribute.ready = true;
});
});
@ -152,16 +161,16 @@ export default ({
// If crosses threshold after letting go, slide away
if (Math.abs(offset) > MAX_OFFSET) {
if (offset > 0) {
widget.slideAway('Right');
widget.attribute.slideAway('Right');
}
else {
widget.slideAway('Left');
widget.attribute.slideAway('Left');
}
}
else {
self.setCss(defaultStyle);
widget.cursor = 'grab';
widget._dragging = false;
widget.attribute.dragging = false;
}
}, 'drag-end');
},

View file

@ -16,7 +16,6 @@ export const NotifPopups = () => PopupWindow({
const TOP_MARGIN = 6;
// FIXME: opens at wrong place
export const NotifCenter = () => PopupWindow({
name: 'notification-center',
anchor: ['top', 'right'],

View file

@ -14,6 +14,7 @@ export default () => Box({
vertical: true,
setup: (self) => {
/** @param {number} id */
const addPopup = (id) => {
if (!id) {
return;
@ -21,38 +22,49 @@ export default () => Box({
const notif = Notifications.getNotification(id);
const NewNotif = Notification({
notif,
command: () => notif.dismiss(),
});
if (notif) {
const NewNotif = Notification({
notif,
command: () => notif.dismiss(),
});
if (NewNotif) {
// Use this instead of add to put it at the top
self.pack_end(NewNotif, false, false, 0);
self.show_all();
if (NewNotif) {
// Use this instead of add to put it at the top
self.pack_end(NewNotif, false, false, 0);
self.show_all();
}
}
};
/**
* @param {number} id
* @param {boolean} force
*/
const handleDismiss = (id, force = false) => {
const notif = self.children.find((ch) => ch._id === id);
// @ts-expect-error
const notif = self.children.find((ch) => ch.attribute.id === id);
if (!notif) {
return;
}
// If notif isn't hovered or was closed, slide away
if (!notif._hovered || force) {
notif.slideAway('Left');
// @ts-expect-error
if (!notif.attribute.hovered || force) {
// @ts-expect-error
notif.attribute.slideAway('Left');
}
// If notif is hovered, delay close
else if (notif._hovered) {
notif.interval = interval(DELAY, () => {
if (!notif._hovered && notif.interval) {
notif.slideAway('Left');
// @ts-expect-error
else if (notif.attribute.hovered) {
const intervalId = interval(DELAY, () => {
// @ts-expect-error
if (!notif.attribute.hovered && intervalId) {
// @ts-expect-error
notif.attribute.slideAway('Left');
GLib.source_remove(notif.interval);
notif.interval = null;
GLib.source_remove(intervalId);
}
});
}