From 17bee0e3854163c9ce142b502d8dad30caa52c5d Mon Sep 17 00:00:00 2001
From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>
Date: Sun, 13 Apr 2025 22:15:25 +1000
Subject: feat: add music visualiser
---
app.tsx | 2 +
scss/mediadisplay.scss | 139 +++++++++++++++++++++++
src/modules/mediadisplay/index.tsx | 188 ++++++++++++++++++++++++++++++++
src/modules/mediadisplay/visualiser.tsx | 76 +++++++++++++
src/modules/sidebar/dashboard.tsx | 6 +-
src/modules/sidebar/modules/media.tsx | 6 +-
src/utils/strings.ts | 5 +
style.scss | 1 +
8 files changed, 413 insertions(+), 10 deletions(-)
create mode 100644 scss/mediadisplay.scss
create mode 100644 src/modules/mediadisplay/index.tsx
create mode 100644 src/modules/mediadisplay/visualiser.tsx
diff --git a/app.tsx b/app.tsx
index 3ea6413..88312e1 100644
--- a/app.tsx
+++ b/app.tsx
@@ -1,5 +1,6 @@
import Bar from "@/modules/bar";
import Launcher from "@/modules/launcher";
+import MediaDisplay from "@/modules/mediadisplay";
import NavBar from "@/modules/navbar";
import NotifPopups from "@/modules/notifpopups";
import Osds from "@/modules/osds";
@@ -78,6 +79,7 @@ App.start({
;
;
Monitors.get_default().forEach(m => );
+ Monitors.get_default().forEach(m => );
Monitors.get_default().forEach(m => );
Monitors.get_default().forEach(m => );
Monitors.get_default().forEach(m => );
diff --git a/scss/mediadisplay.scss b/scss/mediadisplay.scss
new file mode 100644
index 0000000..701a0ce
--- /dev/null
+++ b/scss/mediadisplay.scss
@@ -0,0 +1,139 @@
+@use "sass:color";
+@use "scheme";
+@use "lib";
+@use "font";
+
+.mediadisplay {
+ @include font.mono;
+
+ background-color: scheme.$mantle;
+ color: scheme.$text;
+ padding: lib.s(20);
+ min-height: lib.s(200);
+
+ .visualiser {
+ background-color: scheme.$primary; // Visualiser colour
+ margin-right: lib.s(5); // Gaps between bars
+ min-width: lib.s(10); // Bar width
+ color: scheme.$error;
+ font-size: lib.s(24);
+ font-weight: bold;
+ }
+
+ .cover-art {
+ @include lib.rounded(10);
+ @include lib.element-decel;
+
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ min-width: lib.s(196);
+ min-height: lib.s(196);
+ font-size: lib.s(96);
+ font-weight: bold;
+ background-color: scheme.$surface0;
+ color: scheme.$subtext0;
+ }
+
+ .details {
+ @include font.title;
+
+ font-size: lib.s(14);
+ margin-top: lib.s(5);
+ margin-left: lib.s(15);
+
+ .title {
+ font-size: lib.s(28);
+ font-weight: 500;
+ color: scheme.$text;
+ }
+
+ .artist {
+ font-size: lib.s(18);
+ color: scheme.$secondary;
+ }
+
+ .controls {
+ @include lib.rounded(1000);
+ @include font.icon;
+
+ margin-top: lib.s(10);
+ background-color: color.change(scheme.$overlay0, $alpha: 0.4);
+ font-size: lib.s(28);
+ padding: lib.s(3) lib.s(8);
+
+ @include lib.spacing;
+
+ & > button {
+ @include lib.element-decel;
+
+ &:hover,
+ &:focus {
+ color: color.mix(scheme.$subtext1, scheme.$subtext0, 50%);
+ }
+
+ &:active {
+ color: scheme.$subtext0;
+ }
+
+ &:disabled {
+ color: scheme.$subtext0;
+ }
+ }
+ }
+ }
+
+ .center-module {
+ @include lib.rounded(20);
+ margin: 0 lib.s(40);
+ background-color: color.change(scheme.$surface1, $alpha: 0.4);
+ }
+
+ .selector {
+ @include lib.rounded(15);
+ @include lib.element-decel;
+
+ background-color: color.change(scheme.$overlay0, $alpha: 0.4);
+ padding: lib.s(8) lib.s(15);
+
+ .identity {
+ @include lib.spacing(8);
+ }
+
+ button {
+ @include lib.element-decel;
+
+ &:hover,
+ &:focus {
+ color: color.mix(scheme.$subtext1, scheme.$subtext0, 50%);
+ }
+
+ &:active {
+ color: scheme.$subtext0;
+ }
+ }
+
+ .list > button {
+ margin-top: lib.s(5);
+ color: scheme.$subtext1;
+
+ &:hover,
+ &:focus {
+ color: color.mix(scheme.$subtext1, scheme.$subtext0, 50%);
+ }
+
+ &:active {
+ color: scheme.$subtext0;
+ }
+ }
+ }
+
+ .time {
+ @include lib.rounded(1000);
+
+ font-size: lib.s(16);
+ background-color: color.change(scheme.$overlay0, $alpha: 0.4);
+ padding: lib.s(5) lib.s(10);
+ margin-bottom: lib.s(10);
+ }
+}
diff --git a/src/modules/mediadisplay/index.tsx b/src/modules/mediadisplay/index.tsx
new file mode 100644
index 0000000..307087c
--- /dev/null
+++ b/src/modules/mediadisplay/index.tsx
@@ -0,0 +1,188 @@
+import type { Monitor } from "@/services/monitors";
+import Players from "@/services/players";
+import { lengthStr } from "@/utils/strings";
+import { bind, Variable } from "astal";
+import { App, Astal, Gtk } from "astal/gtk3";
+import AstalMpris from "gi://AstalMpris";
+import Visualiser from "./visualiser";
+
+type Selected = Variable;
+
+const bindIcon = (player: AstalMpris.Player) =>
+ bind(player, "identity").as(i => {
+ const icon = `caelestia-${i?.toLowerCase().replaceAll(" ", "-")}-symbolic`;
+ return Astal.Icon.lookup_icon(icon) ? icon : "caelestia-media-generic-symbolic";
+ });
+
+const PlayerButton = ({
+ player,
+ selected,
+ showDropdown,
+}: {
+ player: AstalMpris.Player;
+ selected: Selected;
+ showDropdown: Variable;
+}) => (
+
+);
+
+const Selector = ({ player, selected }: { player?: AstalMpris.Player; selected: Selected }) => {
+ const showDropdown = Variable(false);
+
+ return (
+
+
+
+
+ {bind(Players.get_default(), "list").as(ps =>
+ ps
+ .filter(p => p !== player)
+ .map(p => )
+ )}
+
+
+
+ );
+};
+
+const NoMedia = ({ selected }: { selected: Selected }) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+);
+
+const Player = ({ player, selected }: { player: AstalMpris.Player; selected: Selected }) => {
+ const time = Variable.derive(
+ [bind(player, "position"), bind(player, "length")],
+ (p, l) => lengthStr(p) + " / " + lengthStr(l)
+ );
+
+ return (
+
+ `background-image: url("${a}");`)}
+ >
+ {bind(player, "coverArt").as(a => (a ? : ))}
+
+
+
+
+
+ time.drop()}
+ />
+ }
+ >
+
+
+
+
+
+
+ );
+};
+
+export default ({ monitor }: { monitor: Monitor }) => {
+ const selected = Variable(Players.get_default().lastPlayer);
+ selected.observe(Players.get_default(), "notify::last-player", () => Players.get_default().lastPlayer);
+
+ return (
+
+ selected.drop()}>
+ {bind(selected).as(p =>
+ p ? :
+ )}
+
+
+ );
+};
diff --git a/src/modules/mediadisplay/visualiser.tsx b/src/modules/mediadisplay/visualiser.tsx
new file mode 100644
index 0000000..fa1adc6
--- /dev/null
+++ b/src/modules/mediadisplay/visualiser.tsx
@@ -0,0 +1,76 @@
+import { Gtk } from "astal/gtk3";
+import cairo from "cairo";
+import AstalCava from "gi://AstalCava";
+import PangoCairo from "gi://PangoCairo";
+
+export default () => (
+ {
+ const cava = AstalCava.get_default();
+
+ if (cava) {
+ cava.set_stereo(true);
+ cava.set_noise_reduction(0.77);
+ cava.set_input(AstalCava.Input.PIPEWIRE);
+
+ self.hook(cava, "notify::values", () => self.queue_draw());
+ self.connect("size-allocate", () => {
+ const width = self.get_allocated_width();
+ const barWidth = self
+ .get_style_context()
+ .get_property("min-width", Gtk.StateFlags.NORMAL) as number;
+ const gaps = self.get_style_context().get_margin(Gtk.StateFlags.NORMAL).right;
+ const bars = Math.floor((width - gaps) / (barWidth + gaps));
+ if (bars > 0) cava.set_bars(bars);
+ });
+ }
+
+ self.connect("draw", (_, cr: cairo.Context) => {
+ const { width, height } = self.get_allocation();
+
+ if (!cava) {
+ // Show error text if cava unavailable
+ const fg = self.get_style_context().get_color(Gtk.StateFlags.NORMAL);
+ cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha);
+ const layout = self.create_pango_layout("Visualiser unavailable");
+ const [w, h] = layout.get_pixel_size();
+ cr.moveTo((width - w) / 2, (height - h) / 2);
+ cr.setAntialias(cairo.Antialias.BEST);
+ PangoCairo.show_layout(cr, layout);
+
+ return;
+ }
+
+ const bg = self.get_style_context().get_background_color(Gtk.StateFlags.NORMAL);
+ cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
+ const barWidth = self.get_style_context().get_property("min-width", Gtk.StateFlags.NORMAL) as number;
+ const gaps = self.get_style_context().get_margin(Gtk.StateFlags.NORMAL).right;
+
+ const values = cava.get_values();
+ const len = values.length - 1;
+ const radius = barWidth / 2;
+ const xOff = (width - len * (barWidth + gaps) - gaps) / 2 - radius;
+ const center = height / 2;
+ const half = Math.floor(len / 2);
+
+ // Render channels facing each other
+ for (let i = half - 1; i >= 0; i--) {
+ const x = (half - i) * (barWidth + gaps) + xOff;
+ const value = center * values[i];
+ cr.arc(x, center + value, radius, 0, Math.PI);
+ cr.arc(x, center - value, radius, Math.PI, Math.PI * 2);
+ cr.fill();
+ }
+
+ for (let i = half; i < len; i++) {
+ const x = (i + 1) * (barWidth + gaps) + xOff;
+ const value = center * values[i];
+ cr.arc(x, center + value, radius, 0, Math.PI);
+ cr.arc(x, center - value, radius, Math.PI, Math.PI * 2);
+ cr.fill();
+ }
+ });
+ }}
+ />
+);
diff --git a/src/modules/sidebar/dashboard.tsx b/src/modules/sidebar/dashboard.tsx
index df7be2b..1a8626f 100644
--- a/src/modules/sidebar/dashboard.tsx
+++ b/src/modules/sidebar/dashboard.tsx
@@ -1,4 +1,5 @@
import Players from "@/services/players";
+import { lengthStr } from "@/utils/strings";
import { bindCurrentTime, osIcon } from "@/utils/system";
import Slider from "@/widgets/slider";
import { bind, GLib, monitorFile, Variable } from "astal";
@@ -7,11 +8,6 @@ import AstalMpris from "gi://AstalMpris";
import Notifications from "./modules/notifications";
import Upcoming from "./modules/upcoming";
-const lengthStr = (length: number) =>
- `${Math.floor(length / 60)}:${Math.floor(length % 60)
- .toString()
- .padStart(2, "0")}`;
-
const noNull = (s: string | null) => s ?? "-";
const FaceFallback = () => (
diff --git a/src/modules/sidebar/modules/media.tsx b/src/modules/sidebar/modules/media.tsx
index 4872f9e..169a98d 100644
--- a/src/modules/sidebar/modules/media.tsx
+++ b/src/modules/sidebar/modules/media.tsx
@@ -1,14 +1,10 @@
import Players from "@/services/players";
+import { lengthStr } from "@/utils/strings";
import Slider from "@/widgets/slider";
import { bind, timeout, Variable } from "astal";
import { Gtk } from "astal/gtk3";
import AstalMpris from "gi://AstalMpris";
-const lengthStr = (length: number) =>
- `${Math.floor(length / 60)}:${Math.floor(length % 60)
- .toString()
- .padStart(2, "0")}`;
-
const noNull = (s: string | null) => s ?? "-";
const NoMedia = () => (
diff --git a/src/utils/strings.ts b/src/utils/strings.ts
index 77608e8..1edad67 100644
--- a/src/utils/strings.ts
+++ b/src/utils/strings.ts
@@ -11,3 +11,8 @@ export const pathToFileName = (path: string, ext?: string) => {
};
export const capitalize = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
+
+export const lengthStr = (length: number) =>
+ `${Math.floor(length / 60)}:${Math.floor(length % 60)
+ .toString()
+ .padStart(2, "0")}`;
diff --git a/style.scss b/style.scss
index 2d63bdf..c7eb766 100644
--- a/style.scss
+++ b/style.scss
@@ -13,6 +13,7 @@
@use "scss/session";
@use "scss/sidebar";
@use "scss/navbar";
+@use "scss/mediadisplay";
* {
all: unset; // Remove GTK theme styles
--
cgit v1.2.3-freya