sidebar: create dashboard
This commit is contained in:
parent
d1daba8aac
commit
fe978092e8
12 changed files with 592 additions and 35 deletions
2
app.tsx
2
app.tsx
|
|
@ -4,6 +4,7 @@ import NotifPopups from "@/modules/notifpopups";
|
|||
import Osds from "@/modules/osds";
|
||||
import Popdowns from "@/modules/popdowns";
|
||||
import Session from "@/modules/session";
|
||||
import SideBar from "@/modules/sidebar";
|
||||
import Monitors from "@/services/monitors";
|
||||
import Palette from "@/services/palette";
|
||||
import Players from "@/services/players";
|
||||
|
|
@ -73,6 +74,7 @@ App.start({
|
|||
<NotifPopups />;
|
||||
<Osds />;
|
||||
<Session />;
|
||||
Monitors.get_default().forEach(m => <SideBar monitor={m} />);
|
||||
Monitors.get_default().forEach(m => <Bar monitor={m} />);
|
||||
<Popdowns />;
|
||||
|
||||
|
|
|
|||
|
|
@ -61,25 +61,4 @@ label.icon {
|
|||
font-size: lib.s(14);
|
||||
color: scheme.$subtext0;
|
||||
}
|
||||
|
||||
.actions {
|
||||
@include lib.spacing;
|
||||
|
||||
& > * {
|
||||
@include lib.rounded(5);
|
||||
@include lib.element-decel;
|
||||
|
||||
padding: lib.s(5) lib.s(10);
|
||||
background-color: scheme.$surface0;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: scheme.$surface1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: scheme.$surface2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
@use "lib";
|
||||
@use "font";
|
||||
|
||||
@mixin popup($colour, $alpha) {
|
||||
@mixin popup($colour) {
|
||||
.separator {
|
||||
background-color: $colour;
|
||||
}
|
||||
|
|
@ -29,16 +29,16 @@
|
|||
@include lib.shadow;
|
||||
|
||||
&.low {
|
||||
@include popup(scheme.$overlay0, 0.3);
|
||||
@include popup(scheme.$overlay0);
|
||||
}
|
||||
|
||||
&.normal {
|
||||
@include popup(scheme.$primary, 0.3);
|
||||
@include popup(scheme.$primary);
|
||||
}
|
||||
|
||||
&.critical {
|
||||
@include lib.border(scheme.$error, 0.5);
|
||||
@include popup(scheme.$error, 0.8);
|
||||
@include popup(scheme.$error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
221
scss/sidebar.scss
Normal file
221
scss/sidebar.scss
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
@use "sass:color";
|
||||
@use "scheme";
|
||||
@use "lib";
|
||||
@use "font";
|
||||
|
||||
@mixin notification($accent) {
|
||||
.separator {
|
||||
background-color: $accent;
|
||||
}
|
||||
|
||||
.image {
|
||||
@include lib.border($accent, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
@include font.mono;
|
||||
|
||||
background-color: scheme.$mantle;
|
||||
color: scheme.$text;
|
||||
padding: lib.s(18) lib.s(20);
|
||||
min-width: lib.s(380);
|
||||
|
||||
.pane {
|
||||
@include lib.spacing(20, true);
|
||||
}
|
||||
|
||||
.separator {
|
||||
background-color: if(scheme.$light, scheme.$surface1, scheme.$overlay0);
|
||||
margin: 0 lib.s(10);
|
||||
}
|
||||
|
||||
.user {
|
||||
@include lib.spacing(15);
|
||||
|
||||
.face {
|
||||
@include lib.rounded(10);
|
||||
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
min-width: lib.s(96);
|
||||
min-height: lib.s(96);
|
||||
font-size: lib.s(48);
|
||||
font-weight: bold;
|
||||
background-color: scheme.$base;
|
||||
}
|
||||
|
||||
.details {
|
||||
font-size: lib.s(14);
|
||||
color: scheme.$yellow;
|
||||
|
||||
@include lib.spacing(8, true);
|
||||
|
||||
.name {
|
||||
font-size: lib.s(18);
|
||||
color: scheme.$text;
|
||||
margin-bottom: lib.s(10);
|
||||
}
|
||||
|
||||
.uptime {
|
||||
color: scheme.$blue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.media {
|
||||
@include lib.spacing(15);
|
||||
|
||||
.cover-art {
|
||||
@include lib.rounded(10);
|
||||
@include lib.element-decel;
|
||||
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
min-width: lib.s(128);
|
||||
min-height: lib.s(128);
|
||||
font-size: lib.s(64);
|
||||
font-weight: bold;
|
||||
background-color: scheme.$base;
|
||||
}
|
||||
|
||||
.details {
|
||||
font-size: lib.s(14);
|
||||
|
||||
.title {
|
||||
font-size: lib.s(16);
|
||||
color: scheme.$text;
|
||||
}
|
||||
|
||||
.artist {
|
||||
color: scheme.$green;
|
||||
}
|
||||
|
||||
.controls {
|
||||
margin-top: lib.s(20);
|
||||
margin-bottom: lib.s(5);
|
||||
font-size: lib.s(24);
|
||||
|
||||
& > * {
|
||||
@include lib.element-decel;
|
||||
|
||||
&:disabled {
|
||||
color: scheme.$overlay0;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: scheme.$subtext0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: scheme.$overlay2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.slider {
|
||||
@include lib.rounded(5);
|
||||
@include lib.fluent-decel(1000ms);
|
||||
|
||||
min-height: lib.s(8);
|
||||
background-color: scheme.$surface0;
|
||||
color: scheme.$overlay0;
|
||||
}
|
||||
|
||||
.time {
|
||||
margin-top: lib.s(5);
|
||||
font-size: lib.s(13);
|
||||
color: scheme.$subtext0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notifications {
|
||||
.header-bar {
|
||||
margin-bottom: lib.s(10);
|
||||
margin-right: lib.s(-10);
|
||||
|
||||
@include lib.spacing;
|
||||
|
||||
& > 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 {
|
||||
background-color: scheme.$primary;
|
||||
color: scheme.$base;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: color.mix(scheme.$primary, scheme.$base, 80%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: color.mix(scheme.$primary, scheme.$base, 70%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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.rounded(10);
|
||||
@include lib.element-decel;
|
||||
|
||||
padding: lib.s(5) lib.s(10);
|
||||
background-color: color.change(scheme.$surface1, $alpha: 0.5);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: color.change(scheme.$surface2, $alpha: 0.5);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: color.change(scheme.$overlay0, $alpha: 0.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
123
src/modules/sidebar/dashboard.tsx
Normal file
123
src/modules/sidebar/dashboard.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import Players from "@/services/players";
|
||||
import { osIcon, osId } from "@/utils/system";
|
||||
import Slider from "@/widgets/slider";
|
||||
import { bind, GLib, monitorFile, Variable } from "astal";
|
||||
import { Gtk } from "astal/gtk3";
|
||||
import AstalMpris from "gi://AstalMpris";
|
||||
import Notifications from "./modules/notifications";
|
||||
|
||||
const lengthStr = (length: number) =>
|
||||
`${Math.floor(length / 60)}:${Math.floor(length % 60)
|
||||
.toString()
|
||||
.padStart(2, "0")}`;
|
||||
|
||||
const FaceFallback = () => (
|
||||
<label
|
||||
setup={self => {
|
||||
const name = GLib.get_real_name();
|
||||
if (name !== "Unknown")
|
||||
self.label = name
|
||||
.split(" ")
|
||||
.map(s => s[0].toUpperCase())
|
||||
.join("");
|
||||
else {
|
||||
self.label = "";
|
||||
self.xalign = 0.44;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const User = () => {
|
||||
const uptime = Variable("").poll(5000, "uptime -p");
|
||||
const hasFace = Variable(GLib.file_test(HOME + "/.face", GLib.FileTest.EXISTS));
|
||||
monitorFile(HOME + "/.face", () => hasFace.set(GLib.file_test(HOME + "/.face", GLib.FileTest.EXISTS)));
|
||||
|
||||
return (
|
||||
<box className="user">
|
||||
<box
|
||||
homogeneous
|
||||
className="face"
|
||||
setup={self => {
|
||||
self.css = `background-image: url("${HOME}/.face");`;
|
||||
monitorFile(HOME + "/.face", () => (self.css = `background-image: url("${HOME}/.face");`));
|
||||
}}
|
||||
onDestroy={() => hasFace.drop()}
|
||||
>
|
||||
{bind(hasFace).as(h => (h ? <box visible={false} /> : <FaceFallback />))}
|
||||
</box>
|
||||
<box vertical hexpand valign={Gtk.Align.CENTER} className="details">
|
||||
<label xalign={0} className="name" label={`${osIcon} ${GLib.get_user_name()}`} />
|
||||
<label xalign={0} label={(GLib.getenv("XDG_CURRENT_DESKTOP") ?? osId).toUpperCase()} />
|
||||
<label truncate xalign={0} className="uptime" label={bind(uptime)} onDestroy={() => uptime.drop()} />
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
const QuickToggles = () => <box></box>;
|
||||
|
||||
const Media = ({ player }: { player: AstalMpris.Player }) => {
|
||||
const position = Variable.derive([bind(player, "position"), bind(player, "length")], (p, l) => p / l);
|
||||
|
||||
return (
|
||||
<box className="media" onDestroy={() => position.drop()}>
|
||||
<box
|
||||
homogeneous
|
||||
className="cover-art"
|
||||
css={bind(player, "coverArt").as(a => `background-image: url("${a}");`)}
|
||||
>
|
||||
{bind(player, "coverArt").as(a => (a ? <box visible={false} /> : <label xalign={0.31} label="" />))}
|
||||
</box>
|
||||
<box vertical className="details">
|
||||
<label truncate className="title" label={bind(player, "title")} />
|
||||
<label truncate className="artist" label={bind(player, "artist")} />
|
||||
<box hexpand className="controls">
|
||||
<button
|
||||
hexpand
|
||||
sensitive={bind(player, "canGoPrevious")}
|
||||
cursor="pointer"
|
||||
onClicked={() => player.next()}
|
||||
label=""
|
||||
/>
|
||||
<button
|
||||
hexpand
|
||||
sensitive={bind(player, "canControl")}
|
||||
cursor="pointer"
|
||||
onClicked={() => player.play_pause()}
|
||||
label={bind(player, "playbackStatus").as(s =>
|
||||
s === AstalMpris.PlaybackStatus.PLAYING ? "" : ""
|
||||
)}
|
||||
/>
|
||||
<button
|
||||
hexpand
|
||||
sensitive={bind(player, "canGoNext")}
|
||||
cursor="pointer"
|
||||
onClicked={() => player.next()}
|
||||
label=""
|
||||
/>
|
||||
</box>
|
||||
<Slider value={bind(position)} />
|
||||
<box className="time">
|
||||
<label label={bind(player, "position").as(lengthStr)} />
|
||||
<box hexpand />
|
||||
<label label={bind(player, "length").as(lengthStr)} />
|
||||
</box>
|
||||
</box>
|
||||
</box>
|
||||
);
|
||||
};
|
||||
|
||||
const Today = () => <box></box>;
|
||||
|
||||
export default () => (
|
||||
<box vertical className="pane dashboard" name="dashboard">
|
||||
<User />
|
||||
<box className="separator" />
|
||||
{bind(Players.get_default(), "lastPlayer").as(p => (
|
||||
<Media player={p} />
|
||||
))}
|
||||
<box className="separator" />
|
||||
<Notifications />
|
||||
</box>
|
||||
);
|
||||
34
src/modules/sidebar/index.tsx
Normal file
34
src/modules/sidebar/index.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { Monitor } from "@/services/monitors";
|
||||
import { bind, register, Variable } from "astal";
|
||||
import { App, Astal, Gtk, Widget } from "astal/gtk3";
|
||||
import Dashboard from "./dashboard";
|
||||
|
||||
@register()
|
||||
export default class SideBar extends Widget.Window {
|
||||
readonly shown: Variable<string> = Variable("dashboard");
|
||||
|
||||
constructor({ monitor }: { monitor: Monitor }) {
|
||||
super({
|
||||
application: App,
|
||||
name: "sidebar",
|
||||
namespace: "caelestia-sidebar",
|
||||
monitor: monitor.id,
|
||||
anchor: Astal.WindowAnchor.LEFT | Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM,
|
||||
exclusivity: Astal.Exclusivity.EXCLUSIVE,
|
||||
// visible: false,
|
||||
});
|
||||
|
||||
this.add(
|
||||
<box vertical className="sidebar">
|
||||
<stack
|
||||
vexpand
|
||||
transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
|
||||
transitionDuration={200}
|
||||
shown={bind(this.shown)}
|
||||
>
|
||||
<Dashboard />
|
||||
</stack>
|
||||
</box>
|
||||
);
|
||||
}
|
||||
}
|
||||
67
src/modules/sidebar/modules/hwresources.tsx
Normal file
67
src/modules/sidebar/modules/hwresources.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import Cpu from "@/services/cpu";
|
||||
import Gpu from "@/services/gpu";
|
||||
import Memory from "@/services/memory";
|
||||
import Storage from "@/services/storage";
|
||||
import Slider from "@/widgets/slider";
|
||||
import { bind, type Binding } from "astal";
|
||||
import { Gtk, type Widget } from "astal/gtk3";
|
||||
|
||||
const fmt = (bytes: number, pow: number) => +(bytes / 1024 ** pow).toFixed(2);
|
||||
const format = ({ total, used }: { total: number; used: number }) => {
|
||||
if (total >= 1024 ** 4) return `${fmt(used, 4)}/${fmt(total, 4)} TiB`;
|
||||
if (total >= 1024 ** 3) return `${fmt(used, 3)}/${fmt(total, 3)} GiB`;
|
||||
if (total >= 1024 ** 2) return `${fmt(used, 2)}/${fmt(total, 2)} MiB`;
|
||||
if (total >= 1024) return `${fmt(used, 1)}/${fmt(total, 1)} KiB`;
|
||||
return `${used}/${total} B`;
|
||||
};
|
||||
|
||||
const Resource = ({
|
||||
icon,
|
||||
name,
|
||||
value,
|
||||
labelSetup,
|
||||
}: {
|
||||
icon: string;
|
||||
name: string;
|
||||
value: Binding<number>;
|
||||
labelSetup?: (self: Widget.Label) => void;
|
||||
}) => (
|
||||
<box vertical className={`resource ${name}`}>
|
||||
<box className="inner">
|
||||
<label label={icon} />
|
||||
<Slider value={value.as(v => v / 100)} />
|
||||
</box>
|
||||
<label halign={Gtk.Align.END} label={labelSetup ? "" : value.as(v => `${+v.toFixed(2)}%`)} setup={labelSetup} />
|
||||
</box>
|
||||
);
|
||||
|
||||
export default () => (
|
||||
<box vertical className="hw-resources">
|
||||
{Gpu.get_default().available && <Resource icon="" name="gpu" value={bind(Gpu.get_default(), "usage")} />}
|
||||
<Resource icon="" name="cpu" value={bind(Cpu.get_default(), "usage")} />
|
||||
<Resource
|
||||
icon=""
|
||||
name="memory"
|
||||
value={bind(Memory.get_default(), "usage")}
|
||||
labelSetup={self => {
|
||||
const mem = Memory.get_default();
|
||||
const update = () => (self.label = format(mem));
|
||||
self.hook(mem, "notify::used", update);
|
||||
self.hook(mem, "notify::total", update);
|
||||
update();
|
||||
}}
|
||||
/>
|
||||
<Resource
|
||||
icon=""
|
||||
name="storage"
|
||||
value={bind(Storage.get_default(), "usage")}
|
||||
labelSetup={self => {
|
||||
const storage = Storage.get_default();
|
||||
const update = () => (self.label = format(storage));
|
||||
self.hook(storage, "notify::used", update);
|
||||
self.hook(storage, "notify::total", update);
|
||||
update();
|
||||
}}
|
||||
/>
|
||||
</box>
|
||||
);
|
||||
70
src/modules/sidebar/modules/notifications.tsx
Normal file
70
src/modules/sidebar/modules/notifications.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import Notification from "@/widgets/notification";
|
||||
import { bind } from "astal";
|
||||
import { Astal, Gtk } from "astal/gtk3";
|
||||
import AstalNotifd from "gi://AstalNotifd";
|
||||
|
||||
const List = () => (
|
||||
<box
|
||||
vertical
|
||||
valign={Gtk.Align.START}
|
||||
className="list"
|
||||
setup={self => {
|
||||
const notifd = AstalNotifd.get_default();
|
||||
const map = new Map<number, Notification>();
|
||||
|
||||
const addNotification = (notification: AstalNotifd.Notification) => {
|
||||
const notif = (<Notification notification={notification} compact />) as Notification;
|
||||
notif.connect("destroy", () => map.get(notification.id) === notif && map.delete(notification.id));
|
||||
map.get(notification.id)?.destroyWithAnims();
|
||||
map.set(notification.id, notif);
|
||||
|
||||
const widget = (
|
||||
<eventbox
|
||||
// Dismiss on middle click
|
||||
onClick={(_, event) => event.button === Astal.MouseButton.MIDDLE && notification.dismiss()}
|
||||
setup={self => self.hook(notif, "destroy", () => self.destroy())}
|
||||
>
|
||||
{notif}
|
||||
</eventbox>
|
||||
);
|
||||
|
||||
self.pack_end(widget, false, false, 0);
|
||||
};
|
||||
|
||||
notifd
|
||||
.get_notifications()
|
||||
.sort((a, b) => a.time - b.time)
|
||||
.forEach(addNotification);
|
||||
|
||||
self.hook(notifd, "notified", (_, id) => addNotification(notifd.get_notification(id)));
|
||||
self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export default () => (
|
||||
<box vertical className="notifications">
|
||||
<box className="header-bar">
|
||||
<label
|
||||
label={bind(AstalNotifd.get_default(), "notifications").as(
|
||||
n => `${n.length} notification${n.length === 1 ? "" : "s"}`
|
||||
)}
|
||||
/>
|
||||
<box hexpand />
|
||||
<button
|
||||
className={bind(AstalNotifd.get_default(), "dontDisturb").as(d => (d ? "enabled" : ""))}
|
||||
cursor="pointer"
|
||||
onClicked={() => (AstalNotifd.get_default().dontDisturb = !AstalNotifd.get_default().dontDisturb)}
|
||||
label=" Silence"
|
||||
/>
|
||||
<button
|
||||
cursor="pointer"
|
||||
onClicked={() => AstalNotifd.get_default().notifications.forEach(n => n.dismiss())}
|
||||
label=" Clear"
|
||||
/>
|
||||
</box>
|
||||
<scrollable expand hscroll={Gtk.PolicyType.NEVER}>
|
||||
<List />
|
||||
</scrollable>
|
||||
</box>
|
||||
);
|
||||
0
src/services/calendar.ts
Normal file
0
src/services/calendar.ts
Normal file
|
|
@ -51,19 +51,19 @@ const AppIcon = ({ appIcon, desktopEntry }: { appIcon: string; desktopEntry: str
|
|||
return icon ? <icon className="app-icon" icon={icon} /> : null;
|
||||
};
|
||||
|
||||
const Image = ({ popup, icon }: { popup?: boolean; icon: string }) => {
|
||||
const Image = ({ compact, icon }: { compact?: boolean; icon: string }) => {
|
||||
if (GLib.file_test(icon, GLib.FileTest.EXISTS))
|
||||
return (
|
||||
<box
|
||||
valign={Gtk.Align.START}
|
||||
className={`image ${popup ? "small" : ""}`}
|
||||
className={`image ${compact ? "small" : ""}`}
|
||||
css={`
|
||||
background-image: url("${icon}");
|
||||
`}
|
||||
/>
|
||||
);
|
||||
if (Astal.Icon.lookup_icon(icon))
|
||||
return <icon valign={Gtk.Align.START} className={`image ${popup ? "small" : ""}`} icon={icon} />;
|
||||
return <icon valign={Gtk.Align.START} className={`image ${compact ? "small" : ""}`} icon={icon} />;
|
||||
return null;
|
||||
};
|
||||
|
||||
|
|
@ -72,7 +72,15 @@ export default class Notification extends Widget.Box {
|
|||
readonly #revealer;
|
||||
#destroyed = false;
|
||||
|
||||
constructor({ notification, popup }: { notification: AstalNotifd.Notification; popup?: boolean }) {
|
||||
constructor({
|
||||
notification,
|
||||
popup,
|
||||
compact = popup,
|
||||
}: {
|
||||
notification: AstalNotifd.Notification;
|
||||
popup?: boolean;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
super({ className: "notification" });
|
||||
|
||||
const time = Variable(getTime(notification.time)).poll(60000, () => getTime(notification.time));
|
||||
|
|
@ -94,17 +102,17 @@ export default class Notification extends Widget.Box {
|
|||
</box>
|
||||
<box hexpand className="separator" />
|
||||
<box className="content">
|
||||
{notification.image && <Image popup={popup} icon={notification.image} />}
|
||||
{notification.image && <Image compact={compact} icon={notification.image} />}
|
||||
<box vertical>
|
||||
<label className="summary" xalign={0} label={notification.summary} truncate />
|
||||
{notification.body && (
|
||||
<label
|
||||
className="body"
|
||||
xalign={0}
|
||||
label={popup ? notification.body.split("\n")[0] : notification.body}
|
||||
label={compact ? notification.body.split("\n")[0] : notification.body}
|
||||
wrap
|
||||
lines={popup ? 1 : -1}
|
||||
truncate={popup}
|
||||
lines={compact ? 1 : -1}
|
||||
truncate={compact}
|
||||
/>
|
||||
)}
|
||||
</box>
|
||||
|
|
@ -132,7 +140,7 @@ export default class Notification extends Widget.Box {
|
|||
|
||||
// Init animation
|
||||
const width = this.get_preferred_width()[1];
|
||||
this.css = `margin-left: ${width}px; margin-right: -${width}px;`;
|
||||
if (popup) this.css = `margin-left: ${width}px; margin-right: -${width}px;`;
|
||||
timeout(1, () => {
|
||||
this.#revealer.revealChild = true;
|
||||
this.css = `transition: 300ms cubic-bezier(0.05, 0.9, 0.1, 1.1); margin-left: 0; margin-right: 0;`;
|
||||
|
|
|
|||
53
src/widgets/slider.tsx
Normal file
53
src/widgets/slider.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { bind, type Binding } from "astal";
|
||||
import { Gtk } from "astal/gtk3";
|
||||
import type cairo from "cairo";
|
||||
|
||||
export default ({ value }: { value: Binding<number> }) => (
|
||||
<drawingarea
|
||||
hexpand
|
||||
valign={Gtk.Align.CENTER}
|
||||
className="slider"
|
||||
css={bind(value).as(v => `font-size: ${v}px;`)}
|
||||
setup={self => {
|
||||
const halfPi = Math.PI / 2;
|
||||
|
||||
const styleContext = self.get_style_context();
|
||||
self.set_size_request(-1, styleContext.get_property("min-height", Gtk.StateFlags.NORMAL) as number);
|
||||
|
||||
self.connect("draw", (_, cr: cairo.Context) => {
|
||||
const styleContext = self.get_style_context();
|
||||
|
||||
const width = self.get_allocated_width();
|
||||
const height = styleContext.get_property("min-height", Gtk.StateFlags.NORMAL) as number;
|
||||
self.set_size_request(-1, height);
|
||||
|
||||
const progressValue = styleContext.get_property("font-size", Gtk.StateFlags.NORMAL) as number;
|
||||
let radius = styleContext.get_property("border-radius", Gtk.StateFlags.NORMAL) as number;
|
||||
|
||||
const bg = styleContext.get_background_color(Gtk.StateFlags.NORMAL);
|
||||
cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
|
||||
|
||||
// Background
|
||||
cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
|
||||
cr.arc(width - radius, radius, radius, -halfPi, 0); // Top right
|
||||
cr.arc(width - radius, height - radius, radius, 0, halfPi); // Bottom right
|
||||
cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
|
||||
cr.fill();
|
||||
|
||||
// Flatten when near 0
|
||||
radius = Math.min(radius, Math.min(width * progressValue, height) / 2);
|
||||
|
||||
const progressPosition = width * progressValue - radius;
|
||||
const fg = styleContext.get_color(Gtk.StateFlags.NORMAL);
|
||||
cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha);
|
||||
|
||||
// Foreground
|
||||
cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
|
||||
cr.arc(progressPosition, radius, radius, -halfPi, 0); // Top right
|
||||
cr.arc(progressPosition, height - radius, radius, 0, halfPi); // Bottom right
|
||||
cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
|
||||
cr.fill();
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
|
@ -10,8 +10,8 @@
|
|||
@use "scss/notifpopups";
|
||||
@use "scss/launcher";
|
||||
@use "scss/osds";
|
||||
@use "scss/popdowns";
|
||||
@use "scss/session";
|
||||
@use "scss/sidebar";
|
||||
|
||||
* {
|
||||
all: unset; // Remove GTK theme styles
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue