many changed, added nixos-hardware

This commit is contained in:
Charlie Root 2024-07-07 13:23:38 +02:00
commit 2accd81424
149 changed files with 19124 additions and 238 deletions

View file

@ -0,0 +1,78 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Revealer, Scrollable } = Widget;
export const MarginRevealer = ({
transition = 'slide_down',
child,
revealChild,
showClass = 'element-show', // These are for animation curve, they don't really hide
hideClass = 'element-hide', // Don't put margins in these classes!
extraSetup = () => { },
...rest
}) => {
const widget = Scrollable({
...rest,
attribute: {
'revealChild': true, // It'll be set to false after init if it's supposed to hide
'transition': transition,
'show': () => {
if (widget.attribute.revealChild) return;
widget.hscroll = 'never';
widget.vscroll = 'never';
child.toggleClassName(hideClass, false);
child.toggleClassName(showClass, true);
widget.attribute.revealChild = true;
child.css = 'margin: 0px;';
},
'hide': () => {
if (!widget.attribute.revealChild) return;
child.toggleClassName(hideClass, true);
child.toggleClassName(showClass, false);
widget.attribute.revealChild = false;
if (widget.attribute.transition == 'slide_left')
child.css = `margin-right: -${child.get_allocated_width()}px;`;
else if (widget.attribute.transition == 'slide_right')
child.css = `margin-left: -${child.get_allocated_width()}px;`;
else if (widget.attribute.transition == 'slide_up')
child.css = `margin-bottom: -${child.get_allocated_height()}px;`;
else if (widget.attribute.transition == 'slide_down')
child.css = `margin-top: -${child.get_allocated_height()}px;`;
},
'toggle': () => {
if (widget.attribute.revealChild) widget.attribute.hide();
else widget.attribute.show();
},
},
child: child,
hscroll: `${revealChild ? 'never' : 'always'}`,
vscroll: `${revealChild ? 'never' : 'always'}`,
setup: (self) => {
extraSetup(self);
}
});
child.toggleClassName(`${revealChild ? showClass : hideClass}`, true);
return widget;
}
// TODO: Allow reveal update. Currently this just helps at declaration
export const DoubleRevealer = ({
transition1 = 'slide_right',
transition2 = 'slide_left',
duration1 = 150,
duration2 = 150,
child,
revealChild,
}) => {
return Revealer({
transition: transition1,
transitionDuration: duration1,
revealChild: revealChild,
child: Revealer({
transition: transition2,
transitionDuration: duration2,
revealChild: revealChild,
child: child,
})
})
}

View file

@ -0,0 +1,106 @@
const { Gtk } = imports.gi;
const Lang = imports.lang;
import Widget from 'resource:///com/github/Aylur/ags/widget.js'
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
// -- Styling --
// min-height for diameter
// min-width for trough stroke
// padding for space between trough and progress
// margin for space between widget and parent
// background-color for trough color
// color for progress color
// -- Usage --
// font size for progress value (0-100px) (hacky i know, but i want animations)
export const AnimatedCircProg = ({
initFrom = 0,
initTo = 0,
initAnimTime = 2900,
initAnimPoints = 1,
extraSetup = () => { },
...rest
}) => Widget.DrawingArea({
...rest,
css: `${initFrom != initTo ? 'font-size: ' + initFrom + 'px; transition: ' + initAnimTime + 'ms linear;' : ''}`,
setup: (area) => {
const styleContext = area.get_style_context();
const width = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
const padding = styleContext.get_padding(Gtk.StateFlags.NORMAL).left;
const marginLeft = styleContext.get_margin(Gtk.StateFlags.NORMAL).left;
const marginRight = styleContext.get_margin(Gtk.StateFlags.NORMAL).right;
const marginTop = styleContext.get_margin(Gtk.StateFlags.NORMAL).top;
const marginBottom = styleContext.get_margin(Gtk.StateFlags.NORMAL).bottom;
area.set_size_request(width + marginLeft + marginRight, height + marginTop + marginBottom);
area.connect('draw', Lang.bind(area, (area, cr) => {
const styleContext = area.get_style_context();
const width = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
const height = styleContext.get_property('min-height', Gtk.StateFlags.NORMAL);
const padding = styleContext.get_padding(Gtk.StateFlags.NORMAL).left;
const marginLeft = styleContext.get_margin(Gtk.StateFlags.NORMAL).left;
const marginRight = styleContext.get_margin(Gtk.StateFlags.NORMAL).right;
const marginTop = styleContext.get_margin(Gtk.StateFlags.NORMAL).top;
const marginBottom = styleContext.get_margin(Gtk.StateFlags.NORMAL).bottom;
area.set_size_request(width + marginLeft + marginRight, height + marginTop + marginBottom);
const progressValue = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL) / 100.0;
const bg_stroke = styleContext.get_property('min-width', Gtk.StateFlags.NORMAL);
const fg_stroke = bg_stroke - padding;
const radius = Math.min(width, height) / 2.0 - Math.max(bg_stroke, fg_stroke) / 2.0;
const center_x = width / 2.0 + marginLeft;
const center_y = height / 2.0 + marginTop;
const start_angle = -Math.PI / 2.0;
const end_angle = start_angle + (2 * Math.PI * progressValue);
const start_x = center_x + Math.cos(start_angle) * radius;
const start_y = center_y + Math.sin(start_angle) * radius;
const end_x = center_x + Math.cos(end_angle) * radius;
const end_y = center_y + Math.sin(end_angle) * radius;
// Draw background
const background_color = styleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
cr.setSourceRGBA(background_color.red, background_color.green, background_color.blue, background_color.alpha);
cr.arc(center_x, center_y, radius, 0, 2 * Math.PI);
cr.setLineWidth(bg_stroke);
cr.stroke();
if (progressValue == 0) return;
// Draw progress
const color = styleContext.get_property('color', Gtk.StateFlags.NORMAL);
cr.setSourceRGBA(color.red, color.green, color.blue, color.alpha);
cr.arc(center_x, center_y, radius, start_angle, end_angle);
cr.setLineWidth(fg_stroke);
cr.stroke();
// Draw rounded ends for progress arcs
cr.setLineWidth(0);
cr.arc(start_x, start_y, fg_stroke / 2, 0, 0 - 0.01);
cr.fill();
cr.arc(end_x, end_y, fg_stroke / 2, 0, 0 - 0.01);
cr.fill();
}));
// Init animation
if (initFrom != initTo) {
area.css = `font-size: ${initFrom}px; transition: ${initAnimTime}ms linear;`;
Utils.timeout(20, () => {
area.css = `font-size: ${initTo}px;`;
}, area)
const transitionDistance = initTo - initFrom;
const oneStep = initAnimTime / initAnimPoints;
area.css = `
font-size: ${initFrom}px;
transition: ${oneStep}ms linear;
`;
for (let i = 0; i < initAnimPoints; i++) {
Utils.timeout(Math.max(10, i * oneStep), () => {
if(!area) return;
area.css = `${initFrom != initTo ? 'font-size: ' + (initFrom + (transitionDistance / initAnimPoints * (i + 1))) + 'px;' : ''}`;
});
}
}
else area.css = 'font-size: 0px;';
extraSetup(area);
},
})

View file

@ -0,0 +1,85 @@
function checkLeapYear(year) {
return (
year % 400 == 0 ||
(year % 4 == 0 && year % 100 != 0));
}
function getMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 31;
if (month == 2 && leapYear) return 29;
if (month == 2 && !leapYear) return 28;
return 30;
}
function getNextMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if (month == 1 && leapYear) return 29;
if (month == 1 && !leapYear) return 28;
if (month == 12) return 31;
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 30;
return 31;
}
function getPrevMonthDays(month, year) {
const leapYear = checkLeapYear(year);
if (month == 3 && leapYear) return 29;
if (month == 3 && !leapYear) return 28;
if (month == 1) return 31;
if ((month <= 7 && month % 2 == 1) || (month >= 8 && month % 2 == 0)) return 30;
return 31;
}
export function getCalendarLayout(dateObject, highlight) {
if (!dateObject) dateObject = new Date();
const weekday = (dateObject.getDay() + 6) % 7; // MONDAY IS THE FIRST DAY OF THE WEEK
const day = dateObject.getDate();
const month = dateObject.getMonth() + 1;
const year = dateObject.getFullYear();
const weekdayOfMonthFirst = (weekday + 35 - (day - 1)) % 7;
const daysInMonth = getMonthDays(month, year);
const daysInNextMonth = getNextMonthDays(month, year);
const daysInPrevMonth = getPrevMonthDays(month, year);
// Fill
var monthDiff = (weekdayOfMonthFirst == 0 ? 0 : -1);
var toFill, dim;
if(weekdayOfMonthFirst == 0) {
toFill = 1;
dim = daysInMonth;
}
else {
toFill = (daysInPrevMonth - (weekdayOfMonthFirst - 1));
dim = daysInPrevMonth;
}
var calendar = [...Array(6)].map(() => Array(7));
var i = 0, j = 0;
while (i < 6 && j < 7) {
calendar[i][j] = {
"day": toFill,
"today": ((toFill == day && monthDiff == 0 && highlight) ? 1 : (
monthDiff == 0 ? 0 :
-1
))
};
// Increment
toFill++;
if (toFill > dim) { // Next month?
monthDiff++;
if (monthDiff == 0)
dim = daysInMonth;
else if (monthDiff == 1)
dim = daysInNextMonth;
toFill = 1;
}
// Next tile
j++;
if (j == 7) {
j = 0;
i++;
}
}
return calendar;
}

View file

@ -0,0 +1,126 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
import { MaterialIcon } from './materialicon.js';
import { setupCursorHover } from './cursorhover.js';
const { Box, Button, Label, Revealer } = Widget;
export const ConfigToggle = ({ icon, name, desc = '', initValue, onChange, ...rest }) => {
let value = initValue;
const toggleIcon = Label({
className: `icon-material txt-bold ${value ? '' : 'txt-poof'}`,
label: `${value ? 'check' : ''}`,
})
const toggleButtonIndicator = Box({
className: `switch-fg ${value ? 'switch-fg-true' : ''}`,
vpack: 'center',
hpack: 'start',
homogeneous: true,
children: [toggleIcon,],
});
const toggleButton = Box({
hpack: 'end',
className: `switch-bg ${value ? 'switch-bg-true' : ''}`,
homogeneous: true,
children: [toggleButtonIndicator,],
});
const widgetContent = Box({
tooltipText: desc,
className: 'txt spacing-h-5 configtoggle-box',
children: [
MaterialIcon(icon, 'norm'),
Label({
className: 'txt txt-small',
label: name,
}),
Box({ hexpand: true }),
toggleButton,
]
});
const interactionWrapper = Button({
attribute: {
toggle: (newValue) => {
value = !value;
toggleIcon.toggleClassName('switch-fg-toggling-false', false);
if (!value) {
toggleIcon.label = '';
toggleIcon.toggleClassName('txt-poof', true);
}
toggleButtonIndicator.toggleClassName('switch-fg-true', value);
toggleButton.toggleClassName('switch-bg-true', value);
if (value) Utils.timeout(1, () => {
toggleIcon.label = 'check';
toggleIcon.toggleClassName('txt-poof', false);
})
onChange(interactionWrapper, value);
}
},
child: widgetContent,
onClicked: (self) => self.attribute.toggle(self),
setup: (button) => {
setupCursorHover(button),
button.connect('pressed', () => { // mouse down
toggleIcon.toggleClassName('txt-poof', true);
toggleIcon.toggleClassName('switch-fg-true', false);
if (!value) toggleIcon.toggleClassName('switch-fg-toggling-false', true);
});
},
...rest,
});
return interactionWrapper;
}
export const ConfigSegmentedSelection = ({
icon, name, desc = '',
options = [{ name: 'Option 1', value: 0 }, { name: 'Option 2', value: 1 }],
initIndex = 0,
onChange,
...rest
}) => {
let lastSelected = initIndex;
let value = options[initIndex].value;
const widget = Box({
tooltipText: desc,
className: 'segment-container',
// homogeneous: true,
children: options.map((option, id) => {
const selectedIcon = Revealer({
revealChild: id == initIndex,
transition: 'slide_right',
transitionDuration: 150,
child: MaterialIcon('check', 'norm')
});
return Button({
setup: setupCursorHover,
className: `segment-btn ${id == initIndex ? 'segment-btn-enabled' : ''}`,
child: Box({
hpack: 'center',
className: 'spacing-h-5',
children: [
selectedIcon,
Label({
label: option.name,
})
]
}),
onClicked: (self) => {
value = option.value;
const kids = widget.get_children();
kids[lastSelected].toggleClassName('segment-btn-enabled', false);
kids[lastSelected].get_children()[0].get_children()[0].revealChild = false;
lastSelected = id;
self.toggleClassName('segment-btn-enabled', true);
selectedIcon.revealChild = true;
onChange(option.value, option.name);
}
})
}),
...rest,
});
return widget;
}
export const ConfigGap = ({ vertical = true, size = 5, ...rest }) => Box({
className: `gap-${vertical ? 'v' : 'h'}-${size}`,
...rest,
})

View file

@ -0,0 +1,57 @@
const { Gdk } = imports.gi;
export function setupCursorHover(button) { // Hand pointing cursor on hover
const display = Gdk.Display.get_default();
button.connect('enter-notify-event', () => {
const cursor = Gdk.Cursor.new_from_name(display, 'pointer');
button.get_window().set_cursor(cursor);
});
button.connect('leave-notify-event', () => {
const cursor = Gdk.Cursor.new_from_name(display, 'default');
button.get_window().set_cursor(cursor);
});
}
export function setupCursorHoverAim(button) { // Crosshair cursor on hover
button.connect('enter-notify-event', () => {
const display = Gdk.Display.get_default();
const cursor = Gdk.Cursor.new_from_name(display, 'crosshair');
button.get_window().set_cursor(cursor);
});
button.connect('leave-notify-event', () => {
const display = Gdk.Display.get_default();
const cursor = Gdk.Cursor.new_from_name(display, 'default');
button.get_window().set_cursor(cursor);
});
}
export function setupCursorHoverGrab(button) { // Hand ready to grab on hover
button.connect('enter-notify-event', () => {
const display = Gdk.Display.get_default();
const cursor = Gdk.Cursor.new_from_name(display, 'grab');
button.get_window().set_cursor(cursor);
});
button.connect('leave-notify-event', () => {
const display = Gdk.Display.get_default();
const cursor = Gdk.Cursor.new_from_name(display, 'default');
button.get_window().set_cursor(cursor);
});
}
export function setupCursorHoverInfo(button) { // "?" mark cursor on hover
const display = Gdk.Display.get_default();
button.connect('enter-notify-event', () => {
const cursor = Gdk.Cursor.new_from_name(display, 'help');
button.get_window().set_cursor(cursor);
});
button.connect('leave-notify-event', () => {
const cursor = Gdk.Cursor.new_from_name(display, 'default');
button.get_window().set_cursor(cursor);
});
}

View file

@ -0,0 +1,7 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
export const MaterialIcon = (icon, size, props = {}) => Widget.Label({
className: `icon-material txt-${size}`,
label: icon,
...props,
})

View file

@ -0,0 +1,86 @@
// Converts from Markdown to Pango. This does not support code blocks.
// For illogical-impulse, code blocks are treated separately, in their own GtkSourceView widgets.
// Partly inherited from https://github.com/ubunatic/md2pango
const monospaceFonts = 'JetBrains Mono NF, JetBrains Mono Nerd Font, JetBrains Mono NL, SpaceMono NF, SpaceMono Nerd Font, monospace';
const replacements = {
'indents': [
{ name: 'BULLET', re: /^(\s*)([\*\-]\s)(.*)(\s*)$/, sub: ' $1- $3' },
{ name: 'NUMBERING', re: /^(\s*[0-9]+\.\s)(.*)(\s*)$/, sub: ' $1 $2' },
],
'escapes': [
{ name: 'COMMENT', re: /<!--.*-->/, sub: '' },
{ name: 'AMPERSTAND', re: /&/g, sub: '&amp;' },
{ name: 'LESSTHAN', re: /</g, sub: '&lt;' },
{ name: 'GREATERTHAN', re: />/g, sub: '&gt;' },
],
'sections': [
{ name: 'H1', re: /^(#\s+)(.*)(\s*)$/, sub: '<span font_weight="bold" size="170%">$2</span>' },
{ name: 'H2', re: /^(##\s+)(.*)(\s*)$/, sub: '<span font_weight="bold" size="150%">$2</span>' },
{ name: 'H3', re: /^(###\s+)(.*)(\s*)$/, sub: '<span font_weight="bold" size="125%">$2</span>' },
{ name: 'H4', re: /^(####\s+)(.*)(\s*)$/, sub: '<span font_weight="bold" size="100%">$2</span>' },
{ name: 'H5', re: /^(#####\s+)(.*)(\s*)$/, sub: '<span font_weight="bold" size="90%">$2</span>' },
],
'styles': [
{ name: 'BOLD', re: /(\*\*)(\S[\s\S]*?\S)(\*\*)/g, sub: "<b>$2</b>" },
{ name: 'UND', re: /(__)(\S[\s\S]*?\S)(__)/g, sub: "<u>$2</u>" },
{ name: 'EMPH', re: /\*(\S.*?\S)\*/g, sub: "<i>$1</i>" },
// { name: 'EMPH', re: /_(\S.*?\S)_/g, sub: "<i>$1</i>" },
{ name: 'HEXCOLOR', re: /#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/g, sub: '<span bgcolor="#$1" fgcolor="#000000" font_family="' + monospaceFonts + '">#$1</span>' },
{ name: 'INLCODE', re: /(`)([^`]*)(`)/g, sub: '<span font_weight="bold" font_family="' + monospaceFonts + '">$2</span>' },
// { name: 'UND', re: /(__|\*\*)(\S[\s\S]*?\S)(__|\*\*)/g, sub: "<u>$2</u>" },
],
}
const replaceCategory = (text, replaces) => {
for (const type of replaces) {
text = text.replace(type.re, type.sub);
}
return text;
}
// Main function
export default (text) => {
let lines = text.split('\n')
let output = [];
// Replace
for (const line of lines) {
let result = line;
result = replaceCategory(result, replacements.indents);
result = replaceCategory(result, replacements.escapes);
result = replaceCategory(result, replacements.sections);
result = replaceCategory(result, replacements.styles);
output.push(result)
}
// Remove trailing whitespaces
output = output.map(line => line.replace(/ +$/, ''))
return output.join('\n');
}
export const markdownTest = `# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
1. yes
2. no
127. well
- Bulletpoint starting with minus
* Bulletpoint starting with asterisk
---
- __Underline__ __ No underline __
- **Bold** ** No bold **
- _Italics1_ *Italics2* _ No Italics _
- A color: #D6BAFF
- nvidia green: #7ABB08
- sub-item
\`\`\`javascript
// A code block!
myArray = [23, 123, 43, 54, '6969'];
console.log('uwu');
\`\`\`
- Random instruction thing
- To update arch lincox, run \`sudo pacman -Syu\`
`;

View file

@ -0,0 +1,71 @@
const { Gtk } = imports.gi;
const Lang = imports.lang;
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
// min-height/min-width for height/width
// background-color/color for background/indicator color
// padding for pad of indicator
// font-size for selected index (0-based)
export const NavigationIndicator = (count, vertical, props) => Widget.DrawingArea({
...props,
setup: (area) => {
const styleContext = area.get_style_context();
const width = Math.max(styleContext.get_property('min-width', Gtk.StateFlags.NORMAL), area.get_allocated_width());
const height = Math.max(styleContext.get_property('min-height', Gtk.StateFlags.NORMAL), area.get_allocated_height());
area.set_size_request(width, height);
area.connect('draw', Lang.bind(area, (area, cr) => {
const styleContext = area.get_style_context();
const width = Math.max(styleContext.get_property('min-width', Gtk.StateFlags.NORMAL), area.get_allocated_width());
const height = Math.max(styleContext.get_property('min-height', Gtk.StateFlags.NORMAL), area.get_allocated_height());
// console.log('allocated width/height:', area.get_allocated_width(), '/', area.get_allocated_height())
area.set_size_request(width, height);
const paddingLeft = styleContext.get_padding(Gtk.StateFlags.NORMAL).left;
const paddingRight = styleContext.get_padding(Gtk.StateFlags.NORMAL).right;
const paddingTop = styleContext.get_padding(Gtk.StateFlags.NORMAL).top;
const paddingBottom = styleContext.get_padding(Gtk.StateFlags.NORMAL).bottom;
const selectedCell = styleContext.get_property('font-size', Gtk.StateFlags.NORMAL);
let cellWidth = width;
let cellHeight = height;
if (vertical) cellHeight /= count;
else cellWidth /= count;
const indicatorWidth = cellWidth - paddingLeft - paddingRight;
const indicatorHeight = cellHeight - paddingTop - paddingBottom;
const background_color = styleContext.get_property('background-color', Gtk.StateFlags.NORMAL);
const color = styleContext.get_property('color', Gtk.StateFlags.NORMAL);
cr.setLineWidth(2);
// Background
cr.setSourceRGBA(background_color.red, background_color.green, background_color.blue, background_color.alpha);
cr.rectangle(0, 0, width, height);
cr.fill();
// The indicator line
cr.setSourceRGBA(color.red, color.green, color.blue, color.alpha);
if (vertical) {
cr.rectangle(paddingLeft, paddingTop + cellHeight * selectedCell + indicatorWidth / 2, indicatorWidth, indicatorHeight - indicatorWidth);
cr.stroke();
cr.rectangle(paddingLeft, paddingTop + cellHeight * selectedCell + indicatorWidth / 2, indicatorWidth, indicatorHeight - indicatorWidth);
cr.fill();
cr.arc(paddingLeft + indicatorWidth / 2, paddingTop + cellHeight * selectedCell + indicatorWidth / 2, indicatorWidth / 2, Math.PI, 2 * Math.PI);
cr.fill();
cr.arc(paddingLeft + indicatorWidth / 2, paddingTop + cellHeight * selectedCell + indicatorHeight - indicatorWidth / 2, indicatorWidth / 2, 0, Math.PI);
cr.fill();
}
else {
cr.rectangle(paddingLeft + cellWidth * selectedCell + indicatorHeight / 2, paddingTop, indicatorWidth - indicatorHeight, indicatorHeight);
cr.stroke();
cr.rectangle(paddingLeft + cellWidth * selectedCell + indicatorHeight / 2, paddingTop, indicatorWidth - indicatorHeight, indicatorHeight);
cr.fill();
cr.arc(paddingLeft + cellWidth * selectedCell + indicatorHeight / 2, paddingTop + indicatorHeight / 2, indicatorHeight / 2, 0.5 * Math.PI, 1.5 * Math.PI);
cr.fill();
cr.arc(paddingLeft + cellWidth * selectedCell + indicatorWidth - indicatorHeight / 2, paddingTop + indicatorHeight / 2, indicatorHeight / 2, -0.5 * Math.PI, 0.5 * Math.PI);
cr.fill();
}
}))
},
})

View file

@ -0,0 +1,445 @@
// This file is for the actual widget for each single notification
const { GLib, Gdk, Gtk } = imports.gi;
import Widget from 'resource:///com/github/Aylur/ags/widget.js'
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js'
const { Box, EventBox, Icon, Overlay, Label, Button, Revealer } = Widget;
import { MaterialIcon } from "./materialicon.js";
import { setupCursorHover } from "./cursorhover.js";
import { AnimatedCircProg } from "./animatedcircularprogress.js";
function guessMessageType(summary) {
if (summary.includes('recording')) return 'screen_record';
if (summary.includes('battery') || summary.includes('power')) return 'power';
if (summary.includes('screenshot')) return 'screenshot_monitor';
if (summary.includes('welcome')) return 'waving_hand';
if (summary.includes('time')) return 'scheduleb';
if (summary.includes('installed')) return 'download';
if (summary.includes('update')) return 'update';
if (summary.startsWith('file')) return 'folder_copy';
return 'chat';
}
function exists(widget) {
return widget !== null;
}
const NotificationIcon = (notifObject) => {
// { appEntry, appIcon, image }, urgency = 'normal'
if (notifObject.image) {
return Box({
valign: Gtk.Align.CENTER,
hexpand: false,
className: 'notif-icon',
css: `
background-image: url("${notifObject.image}");
background-size: auto 100%;
background-repeat: no-repeat;
background-position: center;
`,
});
}
let icon = 'NO_ICON';
if (Utils.lookUpIcon(notifObject.appIcon))
icon = notifObject.appIcon;
if (Utils.lookUpIcon(notifObject.appEntry))
icon = notifObject.appEntry;
return Box({
vpack: 'center',
hexpand: false,
className: `notif-icon notif-icon-material-${notifObject.urgency}`,
homogeneous: true,
children: [
(icon != 'NO_ICON' ?
Icon({
vpack: 'center',
icon: icon,
})
:
MaterialIcon(`${notifObject.urgency == 'critical' ? 'release_alert' : guessMessageType(notifObject.summary.toLowerCase())}`, 'hugerass', {
hexpand: true,
})
)
],
});
};
export default ({
notifObject,
isPopup = false,
props = {},
} = {}) => {
const popupTimeout = notifObject.timeout || (notifObject.urgency == 'critical' ? 8000 : 3000);
const command = (isPopup ?
() => notifObject.dismiss() :
() => notifObject.close()
)
const destroyWithAnims = () => {
widget.sensitive = false;
notificationBox.setCss(middleClickClose);
Utils.timeout(200, () => {
if (wholeThing) wholeThing.revealChild = false;
}, wholeThing);
Utils.timeout(400, () => {
command();
if (wholeThing) {
wholeThing.destroy();
wholeThing = null;
}
}, wholeThing);
}
const widget = EventBox({
onHover: (self) => {
self.window.set_cursor(Gdk.Cursor.new_from_name(display, 'grab'));
if (!wholeThing.attribute.hovered)
wholeThing.attribute.hovered = true;
},
onHoverLost: (self) => {
self.window.set_cursor(null);
if (wholeThing.attribute.hovered)
wholeThing.attribute.hovered = false;
if (isPopup) {
command();
}
},
onMiddleClick: (self) => {
destroyWithAnims();
},
setup: (self) => {
self.on("button-press-event", () => {
wholeThing.attribute.held = true;
notificationContent.toggleClassName(`${isPopup ? 'popup-' : ''}notif-clicked-${notifObject.urgency}`, true);
Utils.timeout(800, () => {
if (wholeThing?.attribute.held) {
Utils.execAsync(['wl-copy', `${notifObject.body}`])
notifTextSummary.label = notifObject.summary + " (copied)";
Utils.timeout(3000, () => notifTextSummary.label = notifObject.summary)
}
})
}).on("button-release-event", () => {
wholeThing.attribute.held = false;
notificationContent.toggleClassName(`${isPopup ? 'popup-' : ''}notif-clicked-${notifObject.urgency}`, false);
})
}
});
let wholeThing = Revealer({
attribute: {
'close': undefined,
'destroyWithAnims': destroyWithAnims,
'dragging': false,
'held': false,
'hovered': false,
'id': notifObject.id,
},
revealChild: false,
transition: 'slide_down',
transitionDuration: 200,
child: Box({ // Box to make sure css-based spacing works
homogeneous: true,
}),
});
const display = Gdk.Display.get_default();
const notifTextPreview = Revealer({
transition: 'slide_down',
transitionDuration: 120,
revealChild: true,
child: Label({
xalign: 0,
className: `txt-smallie notif-body-${notifObject.urgency}`,
useMarkup: true,
xalign: 0,
justify: Gtk.Justification.LEFT,
maxWidthChars: 24,
truncate: 'end',
label: notifObject.body.split("\n")[0],
}),
});
const notifTextExpanded = Revealer({
transition: 'slide_up',
transitionDuration: 120,
revealChild: false,
child: Box({
vertical: true,
className: 'spacing-v-10',
children: [
Label({
xalign: 0,
className: `txt-smallie notif-body-${notifObject.urgency}`,
useMarkup: true,
xalign: 0,
justify: Gtk.Justification.LEFT,
maxWidthChars: 24,
wrap: true,
label: notifObject.body,
}),
Box({
className: 'notif-actions spacing-h-5',
children: [
Button({
hexpand: true,
className: `notif-action notif-action-${notifObject.urgency}`,
onClicked: () => destroyWithAnims(),
child: Label({
label: 'Close',
})
}),
...notifObject.actions.map(action => Widget.Button({
hexpand: true,
className: `notif-action notif-action-${notifObject.urgency}`,
onClicked: () => notifObject.invoke(action.id),
child: Label({
label: action.label,
})
}))
],
})
]
}),
});
const notifIcon = Box({
vpack: 'start',
homogeneous: true,
children: [
Overlay({
child: NotificationIcon(notifObject),
overlays: isPopup ? [AnimatedCircProg({
className: `notif-circprog-${notifObject.urgency}`,
vpack: 'center', hpack: 'center',
initFrom: (isPopup ? 100 : 0),
initTo: 0,
initAnimTime: popupTimeout,
})] : [],
}),
]
});
let notifTime = '';
const messageTime = GLib.DateTime.new_from_unix_local(notifObject.time);
if (messageTime.get_day_of_year() == GLib.DateTime.new_now_local().get_day_of_year())
notifTime = messageTime.format('%H:%M');
else if (messageTime.get_day_of_year() == GLib.DateTime.new_now_local().get_day_of_year() - 1)
notifTime = 'Yesterday';
else
notifTime = messageTime.format('%d/%m');
const notifTextSummary = Label({
xalign: 0,
className: 'txt-small txt-semibold titlefont',
justify: Gtk.Justification.LEFT,
hexpand: true,
maxWidthChars: 24,
truncate: 'end',
ellipsize: 3,
useMarkup: notifObject.summary.startsWith('<'),
label: notifObject.summary,
});
const notifTextBody = Label({
vpack: 'center',
justification: 'right',
className: 'txt-smaller txt-semibold',
label: notifTime,
});
const notifText = Box({
valign: Gtk.Align.CENTER,
vertical: true,
hexpand: true,
children: [
Box({
children: [
notifTextSummary,
notifTextBody,
]
}),
notifTextPreview,
notifTextExpanded,
]
});
const notifExpandButton = Button({
vpack: 'start',
className: 'notif-expand-btn',
onClicked: (self) => {
if (notifTextPreview.revealChild) { // Expanding...
notifTextPreview.revealChild = false;
notifTextExpanded.revealChild = true;
self.child.label = 'expand_less';
expanded = true;
}
else {
notifTextPreview.revealChild = true;
notifTextExpanded.revealChild = false;
self.child.label = 'expand_more';
expanded = false;
}
},
child: MaterialIcon('expand_more', 'norm', {
vpack: 'center',
}),
setup: setupCursorHover,
});
const notificationContent = Box({
...props,
className: `${isPopup ? 'popup-' : ''}notif-${notifObject.urgency} spacing-h-10`,
children: [
notifIcon,
Box({
className: 'spacing-h-5',
children: [
notifText,
notifExpandButton,
]
})
]
})
// Gesture stuff
const gesture = Gtk.GestureDrag.new(widget);
var initDirX = 0;
var initDirVertical = -1; // -1: unset, 0: horizontal, 1: vertical
var expanded = false;
// in px
const startMargin = 0;
const MOVE_THRESHOLD = 10;
const DRAG_CONFIRM_THRESHOLD = 100;
// in rem
const maxOffset = 10.227;
const endMargin = 20.455;
const disappearHeight = 6.818;
const leftAnim1 = `transition: 200ms cubic-bezier(0.05, 0.7, 0.1, 1);
margin-left: -${Number(maxOffset + endMargin)}rem;
margin-right: ${Number(maxOffset + endMargin)}rem;
opacity: 0;`;
const rightAnim1 = `transition: 200ms cubic-bezier(0.05, 0.7, 0.1, 1);
margin-left: ${Number(maxOffset + endMargin)}rem;
margin-right: -${Number(maxOffset + endMargin)}rem;
opacity: 0;`;
const middleClickClose = `transition: 200ms cubic-bezier(0.85, 0, 0.15, 1);
margin-left: ${Number(maxOffset + endMargin)}rem;
margin-right: -${Number(maxOffset + endMargin)}rem;
opacity: 0;`;
const notificationBox = Box({
attribute: {
'leftAnim1': leftAnim1,
'rightAnim1': rightAnim1,
'middleClickClose': middleClickClose,
'ready': false,
},
homogeneous: true,
children: [notificationContent],
setup: (self) => self
.hook(gesture, self => {
var offset_x = gesture.get_offset()[1];
var offset_y = gesture.get_offset()[2];
// Which dir?
if (initDirVertical == -1) {
if (Math.abs(offset_y) > MOVE_THRESHOLD)
initDirVertical = 1;
if (initDirX == 0 && Math.abs(offset_x) > MOVE_THRESHOLD) {
initDirVertical = 0;
initDirX = (offset_x > 0 ? 1 : -1);
}
}
// Horizontal drag
if (initDirVertical == 0 && offset_x > MOVE_THRESHOLD) {
if (initDirX < 0)
self.setCss(`margin-left: 0px; margin-right: 0px;`);
else
self.setCss(`
margin-left: ${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
margin-right: -${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
`);
}
else if (initDirVertical == 0 && offset_x < -MOVE_THRESHOLD) {
if (initDirX > 0)
self.setCss(`margin-left: 0px; margin-right: 0px;`);
else {
offset_x = Math.abs(offset_x);
self.setCss(`
margin-right: ${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
margin-left: -${Number(offset_x + startMargin - MOVE_THRESHOLD)}px;
`);
}
}
// Update dragging
wholeThing.attribute.dragging = Math.abs(offset_x) > MOVE_THRESHOLD;
if (Math.abs(offset_x) > MOVE_THRESHOLD ||
Math.abs(offset_y) > MOVE_THRESHOLD) wholeThing.attribute.held = false;
widget.window?.set_cursor(Gdk.Cursor.new_from_name(display, 'grabbing'));
// Vertical drag
if (initDirVertical == 1 && offset_y > MOVE_THRESHOLD && !expanded) {
notifTextPreview.revealChild = false;
notifTextExpanded.revealChild = true;
expanded = true;
notifExpandButton.child.label = 'expand_less';
}
else if (initDirVertical == 1 && offset_y < -MOVE_THRESHOLD && expanded) {
notifTextPreview.revealChild = true;
notifTextExpanded.revealChild = false;
expanded = false;
notifExpandButton.child.label = 'expand_more';
}
}, 'drag-update')
.hook(gesture, self => {
if (!self.attribute.ready) {
wholeThing.revealChild = true;
self.attribute.ready = true;
return;
}
const offset_h = gesture.get_offset()[1];
if (Math.abs(offset_h) > DRAG_CONFIRM_THRESHOLD && offset_h * initDirX > 0) {
if (offset_h > 0) {
self.setCss(rightAnim1);
widget.sensitive = false;
}
else {
self.setCss(leftAnim1);
widget.sensitive = false;
}
Utils.timeout(200, () => {
if (wholeThing) wholeThing.revealChild = false;
}, wholeThing);
Utils.timeout(400, () => {
command();
if (wholeThing) {
wholeThing.destroy();
wholeThing = null;
}
}, wholeThing);
}
else {
self.setCss(`transition: margin 200ms cubic-bezier(0.05, 0.7, 0.1, 1), opacity 200ms cubic-bezier(0.05, 0.7, 0.1, 1);
margin-left: ${startMargin}px;
margin-right: ${startMargin}px;
margin-bottom: unset; margin-top: unset;
opacity: 1;`);
if (widget.window)
widget.window.set_cursor(Gdk.Cursor.new_from_name(display, 'grab'));
wholeThing.attribute.dragging = false;
}
initDirX = 0;
initDirVertical = -1;
}, 'drag-end')
,
})
widget.add(notificationBox);
wholeThing.child.children = [widget];
if (isPopup) Utils.timeout(popupTimeout, () => {
if (wholeThing) {
wholeThing.revealChild = false;
Utils.timeout(200, () => {
if (wholeThing) {
wholeThing.destroy();
wholeThing = null;
}
command();
}, wholeThing);
}
})
return wholeThing;
}

View file

@ -0,0 +1,30 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Box, Window } = Widget;
export default ({
name,
child,
showClassName,
hideClassName,
...props
}) => Window({
name,
popup: true,
visible: false,
layer: 'overlay',
...props,
child: Box({
className: `${showClassName} ${hideClassName}`,
setup: (self) => self
.hook(App, (self, currentName, visible) => {
if (currentName === name) {
self.toggleClassName(hideClassName, !visible);
}
})
,
child: child,
}),
});

View file

@ -0,0 +1,54 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
const { Gtk } = imports.gi;
const Lang = imports.lang;
import Cairo from 'gi://cairo?version=1.0';
export const dummyRegion = new Cairo.Region();
export const enableClickthrough = (self) => self.input_shape_combine_region(dummyRegion);
export const RoundedCorner = (place, props) => Widget.DrawingArea({
...props,
hpack: place.includes('left') ? 'start' : 'end',
vpack: place.includes('top') ? 'start' : 'end',
setup: (widget) => Utils.timeout(1, () => {
const c = widget.get_style_context().get_property('background-color', Gtk.StateFlags.NORMAL);
const r = widget.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
widget.set_size_request(r, r);
widget.connect('draw', Lang.bind(widget, (widget, cr) => {
const c = widget.get_style_context().get_property('background-color', Gtk.StateFlags.NORMAL);
const r = widget.get_style_context().get_property('border-radius', Gtk.StateFlags.NORMAL);
// const borderColor = widget.get_style_context().get_property('color', Gtk.StateFlags.NORMAL);
// const borderWidth = widget.get_style_context().get_border(Gtk.StateFlags.NORMAL).left; // ur going to write border-width: something anyway
widget.set_size_request(r, r);
switch (place) {
case 'topleft':
cr.arc(r, r, r, Math.PI, 3 * Math.PI / 2);
cr.lineTo(0, 0);
break;
case 'topright':
cr.arc(0, r, r, 3 * Math.PI / 2, 2 * Math.PI);
cr.lineTo(r, 0);
break;
case 'bottomleft':
cr.arc(r, 0, r, Math.PI / 2, Math.PI);
cr.lineTo(0, r);
break;
case 'bottomright':
cr.arc(0, 0, r, 0, Math.PI / 2);
cr.lineTo(r, r);
break;
}
cr.closePath();
cr.setSourceRGBA(c.red, c.green, c.blue, c.alpha);
cr.fill();
// cr.setLineWidth(borderWidth);
// cr.setSourceRGBA(borderColor.red, borderColor.green, borderColor.blue, borderColor.alpha);
// cr.stroke();
}));
}),
});

View file

@ -0,0 +1,5 @@
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
export const separatorLine = Widget.Box({
className: 'separator-line',
})

View file

@ -0,0 +1,253 @@
import App from 'resource:///com/github/Aylur/ags/app.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import * as Utils from 'resource:///com/github/Aylur/ags/utils.js';
import { MaterialIcon } from './materialicon.js';
import Bluetooth from 'resource:///com/github/Aylur/ags/service/bluetooth.js';
import Network from 'resource:///com/github/Aylur/ags/service/network.js';
import Notifications from 'resource:///com/github/Aylur/ags/service/notifications.js';
import { languages } from '../data/languages.js';
// A guessing func to try to support langs not listed in data/languages.js
function isLanguageMatch(abbreviation, word) {
const lowerAbbreviation = abbreviation.toLowerCase();
const lowerWord = word.toLowerCase();
let j = 0;
for (let i = 0; i < lowerWord.length; i++) {
if (lowerWord[i] === lowerAbbreviation[j]) {
j++;
}
if (j === lowerAbbreviation.length) {
return true;
}
}
return false;
}
export const NotificationIndicator = (notifCenterName = 'sideright') => {
const widget = Widget.Revealer({
transition: 150,
transition: 'slide_left',
revealChild: false,
setup: (self) => self
.hook(Notifications, (self, id) => {
if (!id || Notifications.dnd) return;
if (!Notifications.getNotification(id)) return;
self.revealChild = true;
}, 'notified')
.hook(App, (self, currentName, visible) => {
if (visible && currentName === notifCenterName) {
self.revealChild = false;
}
})
,
child: Widget.Box({
children: [
MaterialIcon('notifications', 'norm'),
Widget.Label({
className: 'txt-small titlefont',
attribute: {
unreadCount: 0,
update: (self) => self.label = `${self.attribute.unreadCount}`,
},
setup: (self) => self
.hook(Notifications, (self, id) => {
if (!id || Notifications.dnd) return;
if (!Notifications.getNotification(id)) return;
self.attribute.unreadCount++;
self.attribute.update(self);
}, 'notified')
.hook(App, (self, currentName, visible) => {
if (visible && currentName === notifCenterName) {
self.attribute.unreadCount = 0;
self.attribute.update(self);
}
})
,
})
]
})
});
return widget;
}
export const BluetoothIndicator = () => Widget.Stack({
transition: 'slide_up_down',
children: {
'false': Widget.Label({ className: 'txt-norm icon-material', label: 'bluetooth_disabled' }),
'true': Widget.Label({ className: 'txt-norm icon-material', label: 'bluetooth' }),
},
setup: (self) => self
.hook(Bluetooth, stack => {
stack.shown = String(Bluetooth.enabled);
})
,
});
const NetworkWiredIndicator = () => Widget.Stack({
transition: 'slide_up_down',
children: {
'fallback': SimpleNetworkIndicator(),
'unknown': Widget.Label({ className: 'txt-norm icon-material', label: 'wifi_off' }),
'disconnected': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_off' }),
'connected': Widget.Label({ className: 'txt-norm icon-material', label: 'lan' }),
'connecting': Widget.Label({ className: 'txt-norm icon-material', label: 'settings_ethernet' }),
},
setup: (self) => self.hook(Network, stack => {
if (!Network.wired)
return;
const { internet } = Network.wired;
if (['connecting', 'connected'].includes(internet))
stack.shown = internet;
else if (Network.connectivity !== 'full')
stack.shown = 'disconnected';
else
stack.shown = 'fallback';
}),
});
const SimpleNetworkIndicator = () => Widget.Icon({
setup: (self) => self.hook(Network, self => {
const icon = Network[Network.primary || 'wifi']?.iconName;
self.icon = icon || '';
self.visible = icon;
}),
});
const NetworkWifiIndicator = () => Widget.Stack({
transition: 'slide_up_down',
children: {
'disabled': Widget.Label({ className: 'txt-norm icon-material', label: 'wifi_off' }),
'disconnected': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_off' }),
'connecting': Widget.Label({ className: 'txt-norm icon-material', label: 'settings_ethernet' }),
'0': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_0_bar' }),
'1': Widget.Label({ className: 'txt-norm icon-material', label: 'network_wifi_1_bar' }),
'2': Widget.Label({ className: 'txt-norm icon-material', label: 'network_wifi_2_bar' }),
'3': Widget.Label({ className: 'txt-norm icon-material', label: 'network_wifi_3_bar' }),
'4': Widget.Label({ className: 'txt-norm icon-material', label: 'signal_wifi_4_bar' }),
},
setup: (self) => self.hook(Network, (stack) => {
if (!Network.wifi) {
return;
}
if (Network.wifi.internet == 'connected') {
stack.shown = String(Math.ceil(Network.wifi.strength / 25));
}
else if (["disconnected", "connecting"].includes(Network.wifi.internet)) {
stack.shown = Network.wifi.internet;
}
}),
});
export const NetworkIndicator = () => Widget.Stack({
transition: 'slide_up_down',
children: {
'fallback': SimpleNetworkIndicator(),
'wifi': NetworkWifiIndicator(),
'wired': NetworkWiredIndicator(),
},
setup: (self) => self.hook(Network, stack => {
if (!Network.primary) {
stack.shown = 'wifi';
return;
}
const primary = Network.primary || 'fallback';
if (['wifi', 'wired'].includes(primary))
stack.shown = primary;
else
stack.shown = 'fallback';
}),
});
const HyprlandXkbKeyboardLayout = async ({ useFlag } = {}) => {
try {
const Hyprland = (await import('resource:///com/github/Aylur/ags/service/hyprland.js')).default;
var initLangs = [];
var languageStackArray = [];
var currentKeyboard;
const updateCurrentKeyboards = () => {
currentKeyboard = JSON.parse(Utils.exec('hyprctl -j devices')).keyboards
.find(device => device.name === 'at-translated-set-2-keyboard');
if (currentKeyboard) {
initLangs = currentKeyboard.layout.split(',').map(lang => lang.trim());
}
languageStackArray = Array.from({ length: initLangs.length }, (_, i) => {
const lang = languages.find(lang => lang.layout == initLangs[i]);
// if (!lang) return [
// initLangs[i],
// Widget.Label({ label: initLangs[i] })
// ];
// return [
// lang.layout,
// Widget.Label({ label: (useFlag ? lang.flag : lang.layout) })
// ];
// Object
if (!lang) return {
[initLangs[i]]: Widget.Label({ label: initLangs[i] })
};
return {
[lang.layout]: Widget.Label({ label: (useFlag ? lang.flag : lang.layout) })
};
});
};
updateCurrentKeyboards();
const widgetRevealer = Widget.Revealer({
transition: 150,
transition: 'slide_left',
revealChild: languageStackArray.length > 1,
});
const widgetKids = {
...languageStackArray.reduce((obj, lang) => {
return { ...obj, ...lang };
}, {}),
'undef': Widget.Label({ label: '?' }),
}
const widgetContent = Widget.Stack({
transition: 'slide_up_down',
children: widgetKids,
setup: (self) => self.hook(Hyprland, (stack, kbName, layoutName) => {
if (!kbName) {
return;
}
var lang = languages.find(lang => layoutName.includes(lang.name));
if (lang) {
widgetContent.shown = lang.layout;
}
else { // Attempt to support langs not listed
lang = languageStackArray.find(lang => isLanguageMatch(lang[0], layoutName));
if (!lang) stack.shown = 'undef';
else stack.shown = lang[0];
}
}, 'keyboard-layout'),
});
widgetRevealer.child = widgetContent;
return widgetRevealer;
} catch {
return null;
}
}
const OptionalKeyboardLayout = async () => {
try {
return await HyprlandXkbKeyboardLayout({ useFlag: false });
} catch {
return null;
}
};
const optionalKeyboardLayoutInstance = await OptionalKeyboardLayout();
export const StatusIcons = (props = {}) => Widget.Box({
...props,
child: Widget.Box({
className: 'spacing-h-15',
children: [
optionalKeyboardLayoutInstance,
NotificationIndicator(),
NetworkIndicator(),
BluetoothIndicator(),
]
})
});