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 Osds from "@/modules/osds";
|
||||||
import Popdowns from "@/modules/popdowns";
|
import Popdowns from "@/modules/popdowns";
|
||||||
import Session from "@/modules/session";
|
import Session from "@/modules/session";
|
||||||
|
import SideBar from "@/modules/sidebar";
|
||||||
import Monitors from "@/services/monitors";
|
import Monitors from "@/services/monitors";
|
||||||
import Palette from "@/services/palette";
|
import Palette from "@/services/palette";
|
||||||
import Players from "@/services/players";
|
import Players from "@/services/players";
|
||||||
|
|
@ -73,6 +74,7 @@ App.start({
|
||||||
<NotifPopups />;
|
<NotifPopups />;
|
||||||
<Osds />;
|
<Osds />;
|
||||||
<Session />;
|
<Session />;
|
||||||
|
Monitors.get_default().forEach(m => <SideBar monitor={m} />);
|
||||||
Monitors.get_default().forEach(m => <Bar monitor={m} />);
|
Monitors.get_default().forEach(m => <Bar monitor={m} />);
|
||||||
<Popdowns />;
|
<Popdowns />;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,25 +61,4 @@ label.icon {
|
||||||
font-size: lib.s(14);
|
font-size: lib.s(14);
|
||||||
color: scheme.$subtext0;
|
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 "lib";
|
||||||
@use "font";
|
@use "font";
|
||||||
|
|
||||||
@mixin popup($colour, $alpha) {
|
@mixin popup($colour) {
|
||||||
.separator {
|
.separator {
|
||||||
background-color: $colour;
|
background-color: $colour;
|
||||||
}
|
}
|
||||||
|
|
@ -29,16 +29,16 @@
|
||||||
@include lib.shadow;
|
@include lib.shadow;
|
||||||
|
|
||||||
&.low {
|
&.low {
|
||||||
@include popup(scheme.$overlay0, 0.3);
|
@include popup(scheme.$overlay0);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.normal {
|
&.normal {
|
||||||
@include popup(scheme.$primary, 0.3);
|
@include popup(scheme.$primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.critical {
|
&.critical {
|
||||||
@include lib.border(scheme.$error, 0.5);
|
@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;
|
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))
|
if (GLib.file_test(icon, GLib.FileTest.EXISTS))
|
||||||
return (
|
return (
|
||||||
<box
|
<box
|
||||||
valign={Gtk.Align.START}
|
valign={Gtk.Align.START}
|
||||||
className={`image ${popup ? "small" : ""}`}
|
className={`image ${compact ? "small" : ""}`}
|
||||||
css={`
|
css={`
|
||||||
background-image: url("${icon}");
|
background-image: url("${icon}");
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
if (Astal.Icon.lookup_icon(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;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -72,7 +72,15 @@ export default class Notification extends Widget.Box {
|
||||||
readonly #revealer;
|
readonly #revealer;
|
||||||
#destroyed = false;
|
#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" });
|
super({ className: "notification" });
|
||||||
|
|
||||||
const time = Variable(getTime(notification.time)).poll(60000, () => getTime(notification.time));
|
const time = Variable(getTime(notification.time)).poll(60000, () => getTime(notification.time));
|
||||||
|
|
@ -94,17 +102,17 @@ export default class Notification extends Widget.Box {
|
||||||
</box>
|
</box>
|
||||||
<box hexpand className="separator" />
|
<box hexpand className="separator" />
|
||||||
<box className="content">
|
<box className="content">
|
||||||
{notification.image && <Image popup={popup} icon={notification.image} />}
|
{notification.image && <Image compact={compact} icon={notification.image} />}
|
||||||
<box vertical>
|
<box vertical>
|
||||||
<label className="summary" xalign={0} label={notification.summary} truncate />
|
<label className="summary" xalign={0} label={notification.summary} truncate />
|
||||||
{notification.body && (
|
{notification.body && (
|
||||||
<label
|
<label
|
||||||
className="body"
|
className="body"
|
||||||
xalign={0}
|
xalign={0}
|
||||||
label={popup ? notification.body.split("\n")[0] : notification.body}
|
label={compact ? notification.body.split("\n")[0] : notification.body}
|
||||||
wrap
|
wrap
|
||||||
lines={popup ? 1 : -1}
|
lines={compact ? 1 : -1}
|
||||||
truncate={popup}
|
truncate={compact}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</box>
|
</box>
|
||||||
|
|
@ -132,7 +140,7 @@ export default class Notification extends Widget.Box {
|
||||||
|
|
||||||
// Init animation
|
// Init animation
|
||||||
const width = this.get_preferred_width()[1];
|
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, () => {
|
timeout(1, () => {
|
||||||
this.#revealer.revealChild = true;
|
this.#revealer.revealChild = true;
|
||||||
this.css = `transition: 300ms cubic-bezier(0.05, 0.9, 0.1, 1.1); margin-left: 0; margin-right: 0;`;
|
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/notifpopups";
|
||||||
@use "scss/launcher";
|
@use "scss/launcher";
|
||||||
@use "scss/osds";
|
@use "scss/osds";
|
||||||
@use "scss/popdowns";
|
|
||||||
@use "scss/session";
|
@use "scss/session";
|
||||||
|
@use "scss/sidebar";
|
||||||
|
|
||||||
* {
|
* {
|
||||||
all: unset; // Remove GTK theme styles
|
all: unset; // Remove GTK theme styles
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue