From a5b893dcc8b0a3fb48748ed797903b52cd58f844 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:00:40 +0300 Subject: [PATCH 01/64] Update OSD.ts --- ags/window/OSD.ts | 159 ++++++++++++++++++++++++++++++---------------- 1 file changed, 105 insertions(+), 54 deletions(-) diff --git a/ags/window/OSD.ts b/ags/window/OSD.ts index df43d8d8..d4846beb 100644 --- a/ags/window/OSD.ts +++ b/ags/window/OSD.ts @@ -1,14 +1,15 @@ import { bind, Binding, Variable } from "astal"; import { Astal, Gtk, Widget } from "astal/gtk3"; import { Wireplumber } from "../scripts/volume"; +import AstalWp from "gi://AstalWp"; export enum OSDModes { SINK, + SOURCE, BRIGHTNESS } let osdMode: (Variable|null); -let osdIcon: (Binding|null); export function setOSDMode(newMode: OSDModes): void { if(!osdMode) return; @@ -16,18 +17,101 @@ export function setOSDMode(newMode: OSDModes): void { osdMode.set(newMode); } +function createOSD( + props: Widget.BoxProps, + bindable: AstalWp.Endpoint, + iconName: string | Binding, + labelOSD: string | Binding, + valueOSD: number | Binding, + maxValueOSD: number | Binding + +) { + return new Widget.Box({ + ...props, + className: `osd ${props.className || ''}`, + expand: true, + children: [ + new Widget.Icon({ + className: "icon", + icon: iconName, + } as Widget.IconProps), + new Widget.Box({ + className: "volume", + orientation: Gtk.Orientation.VERTICAL, + valign: Gtk.Align.CENTER, + children: [ + new Widget.Label({ + className: "device", + label: labelOSD, + truncate: true, + } as Widget.LabelProps), + new Widget.Box({ + vexpand: false, + expand: false, + children: [ + new Widget.LevelBar({ + className: "levelbar", + width_request: 120, + value: valueOSD, + maxValue: maxValueOSD, + expand: true, + } as Widget.LevelBarProps), + /*new Widget.Label({ + className: "value", + label: bind(Wireplumber.getDefault().getDefaultSink(), "volume").as((volume: number) => + `${Math.floor(volume * 100)}%`), + vexpand: false, + expand: false, + halign: Gtk.Align.CENTER + } as Widget.LabelProps)*/ + ] + } as Widget.BoxProps) + ] + } as Widget.BoxProps) + ] + } as Widget.BoxProps) +} + +function OSDSink() { + const audio = Wireplumber.getDefault().getDefaultSink(); + return createOSD( + { name: "sink" }, + audio, + bind(audio, "volumeIcon").as(icon => + !Wireplumber.getDefault().isMutedSink() && Wireplumber.getDefault().getSinkVolume() > 0 ? + icon : "audio-volume-muted-symbolic"), + bind(audio, "description").as((description: string) => + description || "Speaker"), + bind(audio, "volume").as((volume: number) => + Math.floor(volume * 100)), + bind(Wireplumber.getWireplumber(), "defaultSpeaker").as(() => + Wireplumber.getDefault().getMaxSinkVolume()) + ) +} + +function OSDSource() { + const source = Wireplumber.getDefault().getDefaultSource(); + return createOSD( + { name: "source" }, + source, + bind(source, "volumeIcon").as(icon => + !Wireplumber.getDefault().isMutedSource() && Wireplumber.getDefault().getSourceVolume() > 0 ? + icon : "microphone-sensitivity-muted-symbolic"), + bind(source, "description").as((description: string) => + description || "Microphone"), + bind(source, "volume").as((volume: number) => + Math.floor(volume * 100)), + bind(Wireplumber.getWireplumber(), "defaultMicrophone").as(() => + Wireplumber.getDefault().getMaxSourceVolume()) + ) +} + export const OSD = (mon: number) => { osdMode = new Variable(OSDModes.SINK); - osdIcon = osdMode().as((mode: OSDModes) => { - switch(mode) { - case OSDModes.SINK: return "󰕾"; - case OSDModes.BRIGHTNESS: return "󰃠"; - default: return "󱧣"; - } - }); return new Widget.Window({ namespace: "osd", + className: "osd-window", layer: Astal.Layer.OVERLAY, anchor: Astal.WindowAnchor.BOTTOM, canFocus: false, @@ -39,54 +123,21 @@ export const OSD = (mon: number) => { osdMode?.drop(); osdMode = null; - osdIcon = null; }, - child: new Widget.Box({ - className: "osd", + child: new Widget.Stack({ + visibleChildName: bind(osdMode, "value").as((mode: OSDModes) => { + switch (mode) { + case OSDModes.SINK: return "sink"; + case OSDModes.SOURCE: return "source"; + default: return "sink"; + } + }), + onDestroy: () => { + osdMode = null + }, children: [ - new Widget.Icon({ - className: "icon", - icon: bind(Wireplumber.getDefault().getDefaultSink(), "volumeIcon").as(icon => - !Wireplumber.getDefault().isMutedSink() && Wireplumber.getDefault().getSinkVolume() > 0 ? icon : "audio-volume-muted-symbolic"), - } as Widget.IconProps), - new Widget.Box({ - className: "volume", - orientation: Gtk.Orientation.VERTICAL, - valign: Gtk.Align.CENTER, - children: [ - new Widget.Label({ - className: "device", - label: bind(Wireplumber.getDefault().getDefaultSink(), "name").as((name: string) => - name || "Speaker"), - halign: Gtk.Align.CENTER - } as Widget.LabelProps), - new Widget.Box({ - vexpand: false, - expand: false, - children: [ - new Widget.LevelBar({ - className: "levelbar", - width_request: 120, - value: bind(Wireplumber.getDefault().getDefaultSink(), "volume").as((volume: number) => - Math.floor(volume * 100)), - maxValue: bind(Wireplumber.getWireplumber(), "defaultSpeaker").as(() => - Wireplumber.getDefault().getMaxSinkVolume()), - vexpand: false, - expand: false, - halign: Gtk.Align.CENTER - } as Widget.LevelBarProps), - /*new Widget.Label({ - className: "value", - label: bind(Wireplumber.getDefault().getDefaultSink(), "volume").as((volume: number) => - `${Math.floor(volume * 100)}%`), - vexpand: false, - expand: false, - halign: Gtk.Align.CENTER - } as Widget.LabelProps)*/ - ] - } as Widget.BoxProps) - ] - } as Widget.BoxProps) + OSDSink(), + OSDSource(), ] } as Widget.BoxProps) } as Widget.WindowProps); From d94268ca53c2947529b27cde928976e497111ff2 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:01:56 +0300 Subject: [PATCH 02/64] Update Status.ts Add revealers for volume labels --- ags/widget/bar/Status.ts | 76 +++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 17 deletions(-) diff --git a/ags/widget/bar/Status.ts b/ags/widget/bar/Status.ts index c0759810..58c4a799 100644 --- a/ags/widget/bar/Status.ts +++ b/ags/widget/bar/Status.ts @@ -2,7 +2,7 @@ import AstalBluetooth from "gi://AstalBluetooth"; import AstalNetwork from "gi://AstalNetwork"; import AstalWp from "gi://AstalWp"; -import { bind, Binding, Variable } from "astal"; +import { bind, Binding, Variable, GLib } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import { Wireplumber } from "../../scripts/volume"; import { Notifications } from "../../scripts/notifications"; @@ -10,7 +10,9 @@ import { Windows } from "../../windows"; import { Recording } from "../../scripts/recording"; import { getDateTime } from "../../scripts/time"; import { tr } from "../../i18n/intl"; +import { Time, timeout, idle } from "astal/time"; +import { IconStatus } from "../../scripts/icons"; export function Status(): Gtk.Widget { const recordingTimer: Variable = Variable.derive([ @@ -88,24 +90,64 @@ export function Status(): Gtk.Widget { function volumeStatus(props: { className?: string, endpoint: AstalWp.Endpoint, icon?: (string|Binding) }): Gtk.Widget { return new Widget.EventBox({ className: props.className, - onScroll: (_, event) => - event.delta_y > 0 ? + onScroll: (_, event) => //ugh..... + event.delta_y > 0 ? Wireplumber.getDefault().decreaseEndpointVolume(props.endpoint, 5) : Wireplumber.getDefault().increaseEndpointVolume(props.endpoint, 5), - child: new Widget.Box({ - spacing: 2, - children: [ - new Widget.Icon({ - visible: props.icon, - icon: props.icon, - } as Widget.IconProps), - new Widget.Label({ - className: "volume", - label: bind(props.endpoint, "volume").as((volume: number) => - Math.floor(volume * 100) + "%") - } as Widget.LabelProps), - ] - } as Widget.BoxProps) + setup: (eventbox) => { + let timer: (Time|undefined); + const connections: Array = []; + + const showRevealer = (self: any) => { + self.revealChild = true; + + if (timer) { + timer.cancel(); + } + + timer = timeout(3000, () => { + self.revealChild = false + timer = undefined; + }); + } + + eventbox.add(new Widget.Box({ + spacing: 2, + children: [ + new Widget.Icon({ + visible: props.icon, + icon: props.icon, + }), + new Widget.Revealer({ + transitionDuration: 180, + transitionType: Gtk.RevealerTransitionType.SLIDE_LEFT, + setup: (self) => { + + connections.push( + eventbox.connect("hover", () => self.revealChild = true), + eventbox.connect("hover-lost", () => { + timeout(20, () => { //for smoothness (do not change it) + self.revealChild = false + }) + }), + Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () => { + showRevealer(self) + }), + Wireplumber.getDefault().getDefaultSource().connect("notify::volume", () => { + showRevealer(self) + }), + ) + }, + onDestroy: () => connections.map(id => eventbox.disconnect(id)), + child: new Widget.Label({ + className: "volume", + label: bind(props.endpoint, "volume").as((volume: number) => + Math.floor(volume * 100) + "%") + } as Widget.LabelProps), + } as Widget.RevealerProps) + ] + } as Widget.BoxProps)) + } } as Widget.EventBoxProps) } From cdc7f50fd52a393fe65aa060d50bce42c89ae70c Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:05:30 +0300 Subject: [PATCH 03/64] Update Network.ts Now I can finally activate or deactivate any of available interfaces --- ags/widget/control-center/pages/Network.ts | 229 ++++++++++++++++++--- 1 file changed, 195 insertions(+), 34 deletions(-) diff --git a/ags/widget/control-center/pages/Network.ts b/ags/widget/control-center/pages/Network.ts index 600d5c53..5d202fa0 100644 --- a/ags/widget/control-center/pages/Network.ts +++ b/ags/widget/control-center/pages/Network.ts @@ -1,11 +1,20 @@ import { Gtk, Widget } from "astal/gtk3"; import { Page, PageButton } from "./Page"; import AstalNetwork from "gi://AstalNetwork"; -import { bind } from "astal"; +import { bind, GLib } from "astal"; import NM from "gi://NM"; +import NMA from "gi://NMA"; import { Windows } from "../../../windows"; import { tr } from "../../../i18n/intl"; import { execApp } from "../../../scripts/apps"; +import { EntryPopup, EntryPopupProps } from "../../EntryPopup"; +import { Notifications } from "../../../scripts/notifications"; +import { AskPopup, AskPopupProps } from "../../AskPopup"; +import { encoder } from "../../../scripts/utils"; +import { setOSDMode } from "../../../window/OSD"; + +const client = AstalNetwork.get_default().get_client(); +export const Network = AstalNetwork.get_default(); export const PageNetwork: (() => Page) = () => new Page({ id: "network", @@ -17,10 +26,10 @@ export const PageNetwork: (() => Page) = () => new Page({ image: new Widget.Icon({ icon: "arrow-circular-top-right-symbolic" } as Widget.IconProps), - visible: bind(AstalNetwork.get_default(), "primary").as((primary) => + visible: bind(Network, "primary").as((primary) => primary === AstalNetwork.Primary.WIFI), tooltipText: "Re-scan connections", - onClick: () => AstalNetwork.get_default().wifi.scan() + onClick: () => Network.wifi.scan() } as Widget.ButtonProps) ], bottomButtons: [{ @@ -35,14 +44,13 @@ export const PageNetwork: (() => Page) = () => new Page({ className: "devices", hexpand: true, orientation: Gtk.Orientation.VERTICAL, - visible: bind(AstalNetwork.get_default().get_client(), "devices").as((devs) => devs.length > 0), - children: bind(AstalNetwork.get_default().get_client(), "devices").as((devices) => { + visible: bind(Network.get_client(), "devices").as((devs) => devs.length > 0), + children: bind(Network.get_client(), "devices").as((devices) => { devices = devices.filter(dev => dev.interface !== "lo"); return [ new Widget.Label({ label: tr("devices"), - xalign: 0, className: "sub-header", } as Widget.LabelProps), ...devices.filter(device => device.real).map(dev => PageButton({ @@ -53,6 +61,23 @@ export const PageNetwork: (() => Page) = () => new Page({ : "network-wired-symbolic"), title: bind(dev, "interface").as(iface => iface ?? tr("control_center.pages.network.interface")), + switches: [ + new Widget.Switch({ + css: `margin: 2px 0px; + font-size: 18px;`, + onNotifyActive: (self) => { + + const isDeviceActive = dev.state === NM.DeviceState.ACTIVATED + + if (self.active === isDeviceActive) { + return; + } + + controlConnection(dev, self.active) + }, + state: bind(dev, "state").as(state => state === NM.DeviceState.ACTIVATED ? true : false) + } as Widget.SwitchProps) + ], extraButtons: [ new Widget.Button({ image: new Widget.Icon({ @@ -72,35 +97,171 @@ export const PageNetwork: (() => Page) = () => new Page({ ] }) } as Widget.BoxProps), - new Widget.Box({ - className: "wireless-aps", - visible: bind(AstalNetwork.get_default(), "primary").as((primary) => primary === AstalNetwork.Primary.WIFI), - hexpand: true, - orientation: Gtk.Orientation.VERTICAL, - children: AstalNetwork.get_default().wifi ? bind(AstalNetwork.get_default().wifi.get_device(), "accessPoints").as((aps) => - aps.map(ap => new Widget.Button({ - hexpand: true, - onClick: () => console.log("connect to " + ap.get_ssid().toArray().toString()), // TODO I don't have a WiFi board :( - child: new Widget.Box({ - hexpand: true, - children: [ - new Widget.Icon({ - halign: Gtk.Align.START, - className: "icon", - icon: "network-wireless-signal-excellent-symbolic" - } as Widget.IconProps), - new Widget.Label({ - className: "ssid", - halign: Gtk.Align.START, - label: (getDecoded(ap.ssid.get_data()) ?? ap.ssid.get_data().toString()) ?? "Wi-Fi" - } as Widget.LabelProps), - new Widget.Label({ - className: "status", - } as Widget.LabelProps) + new Widget.Box({ + className: "wireless-aps", + visible: bind(Network, "primary").as((primary) => primary === AstalNetwork.Primary.WIFI), + hexpand: true, + orientation: Gtk.Orientation.VERTICAL, + children: Network.wifi ? bind(Network.wifi, "accessPoints").as((aps) => [ + new Widget.Label({ + className: "sub-header", + label: "Wi-Fi" + } as Widget.LabelProps), + ...aps.filter(ap => ap.ssid).map(ap => { + return PageButton({ + title: bind(ap, "ssid").as(ssid => + ssid ?? "Unknown SSID"), + icon: bind(ap, "iconName"), + endWidget: new Widget.Icon({ + // @ts-ignore ts-for-gir generated the types wrong + icon: bind(ap, "flags").as(flags => flags & NM["80211ApFlags" as keyof typeof NM].PRIVACY ? + "channel-secure-symbolic" + : "channel-insecure-symbolic"), + css: "font-size: 18px;" + } as Widget.IconProps), + extraButtons: [ + new Widget.Button({ + image: new Widget.Icon({ + icon: "window-close-symbolic", + css: "font-size: 18px;" + } as Widget.IconProps) + } as Widget.ButtonProps) + ], + onClick: () => { + + if (!ap.ssid) { + console.log("I dont see any of ssid..."); + return; + } + + const savedConnections = client.get_connections(); + let existingWifiConnection: NM.RemoteConnection | null; + + for (const conn of savedConnections) { + const wirelessSetting = conn.get_setting_wireless(); + + if (wirelessSetting && wirelessSetting.ssid && GLib.Bytes.new(encoder.encode(wirelessSetting.ssid)) === ap.ssid) { + console.log("Got ssid!"); + existingWifiConnection = conn; + break; + } + } + + if (existingWifiConnection) { + console.log("Connectiong by ssid..."); + client.activate_connection_async(existingWifiConnection, Network.wifi.get_device(), ap.dbus_path, null, (_, asyncRes) => errorNotif(asyncRes, "")) + } else { + console.log("Well, creating a new connection..."); + const uuid = NM.utils_uuid_generate(); + const ssidBytes = GLib.Bytes.new(encoder.encode(ap.ssid)); + + const Bssid = ap.bssid; + + const connection = NM.SimpleConnection.new(); + const connSetting = NM.SettingConnection.new(); + const wifiSetting = NM.SettingWireless.new(); + const wifiSecuritySetting = NM.SettingWirelessSecurity.new(); + const setting8021x = NM.Setting8021x.new(); + + // @ts-ignore yep, type-gen issues again + if(ap.rsnFlags !& NM["80211ApSecurityFlags"].KEY_MGMT_802_1X && + // @ts-ignore + ap.wpaFlags !& NM["80211ApSecurityFlags"].KEY_MGMT_802_1X) { + return; + } + + connSetting.uuid = uuid; + connection.add_setting(connSetting); + + connection.add_setting(wifiSetting); + wifiSetting.ssid = ssidBytes; + + wifiSecuritySetting.keyMgmt = "wpa-eap"; + connection.add_setting(wifiSecuritySetting); + + setting8021x.add_eap_method("ttls"); + setting8021x.phase2Auth = "mschapv2"; + connection.add_setting(setting8021x); + const nmAP = Network.wifi.get_device().accessPoints.filter(nmAccessPoint => nmAccessPoint.ssid === ssidBytes)[0]; + const dialog = NMA.WifiDialog.new( + Network.get_client(), connection, + Network.wifi.get_device(), nmAP, + false + ); + + dialog.show(); + } + } + }); + }) ] - } as Widget.BoxProps) - } as Widget.ButtonProps))) : [], - } as Widget.BoxProps) + ) : [], + } as Widget.BoxProps) ] }); +// For switches +function controlConnection(device: (NM.Device|null), check: boolean): void { + + const activeConnection = device.get_active_connection() ?? null; + + if (check === true) { + const availableConnections = device.get_available_connections(); + + if (availableConnections.length <= 0) { + return; + } + const connection = availableConnections[0]; + client.activate_connection_async(connection, device, null, null, (_, asyncRes) => { + try { + console.log(`Activation ${device.interface} was successful.`); + } catch (e) { + console.error(`Activation error: ${e.message}`); + } + }); + } else { + client.deactivate_connection_async(activeConnection, null, (_, asyncRes) => { + try { + console.log(`Deactivation ${device.interface} was successful.`); + } catch (e) { + console.error(`Deactivation error: ${e.message}`); + } + }) + } +} + +function activateWirelessConnection(connection: NM.RemoteConnection, ssid: string): void { + Network.get_client().activate_connection_async( + connection, Network.wifi.get_device(), null, null, (_, asyncRes) => errorNotif(asyncRes, ssid)); +} + +function errorNotif(asyncRes: any, ssid: string = ""): void { //change notify for connected and errors scenario + const activeConnection = Network.get_client().activate_connection_finish(asyncRes); + if(!activeConnection) { + Notifications.getDefault().sendNotification({ + appName: "network", + summary: "Couldn't activate wireless connection", + body: `An error occurred while activating the wireless connection "${ssid}"` + }); + return; + } +} +function notifyConnectionError(ssid: string): void { + Notifications.getDefault().sendNotification({ + appName: "network", + summary: "Coudn't connect Wi-Fi", + body: `An error occurred while trying to connect to the "${ssid}" access point. \nMaybe the password is invalid?` + }); +} +function saveToDisk(remoteConnection: NM.RemoteConnection, ssid: string): void { + AskPopup({ + text: `Save password for connection "${ssid}"?`, + acceptText: "Yes", + onAccept: () => remoteConnection.commit_changes_async(true, null, (_, asyncRes) => + !remoteConnection.commit_changes_finish(asyncRes) && Notifications.getDefault().sendNotification({ + appName: "network", + summary: "Couldn't save Wi-Fi password", + body: `An error occurred while trying to write the password for "${ssid}" to disk` + })) + } as AskPopupProps); +} From 7b58af39f9c880ce89ace8dece7f4eab78033937 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:09:43 +0300 Subject: [PATCH 04/64] Update Sound.ts Change method getting app icons, label showing --- ags/widget/control-center/pages/Sound.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ags/widget/control-center/pages/Sound.ts b/ags/widget/control-center/pages/Sound.ts index cd0d9fce..c6eed02b 100644 --- a/ags/widget/control-center/pages/Sound.ts +++ b/ags/widget/control-center/pages/Sound.ts @@ -1,9 +1,10 @@ import { Page, PageButton, PageProps } from "./Page"; import { bind, Variable } from "astal"; import { Astal, Gtk, Widget } from "astal/gtk3"; -import { getAppIcon } from "../../../scripts/apps"; +import { getAppIcon, getSymbolicIcon } from "../../../scripts/apps"; import { Wireplumber } from "../../../scripts/volume"; import { tr } from "../../../i18n/intl"; +import { analyser } from "../../../scripts/utils"; export function PageSound(): Page { const endpoints = Variable.derive([ @@ -26,7 +27,7 @@ export function PageSound(): Page { PageButton({ className: bind(speaker, "isDefault").as(isDefault => isDefault ? "default" : ""), icon: bind(speaker, "icon").as(icon => - Astal.Icon.lookup_icon(icon)? icon : "audio-card-symbolic"), + getSymbolicIcon(icon) ?? "audio-card-symbolic"), title: bind(speaker, "description").as(desc => desc ?? "Speaker"), onClick: () => speaker.set_is_default(true), endWidget: new Widget.Icon({ @@ -52,8 +53,8 @@ export function PageSound(): Page { orientation: Gtk.Orientation.HORIZONTAL, children: [ new Widget.Icon({ - icon: bind(stream, "name").as(name => - getAppIcon(name.split(' ')[0]) ?? "application-x-executable-symbolic"), + icon: bind(stream, "description").as(icon => + getSymbolicIcon(icon) ?? "application-x-executable-symbolic"), css: "font-size: 18px; margin-right: 6px;" } as Widget.IconProps), new Widget.Box({ @@ -69,7 +70,11 @@ export function PageSound(): Page { ), onDestroy: () => connections.map(id => eventbox.disconnect(id)), child: new Widget.Label({ - label: bind(stream, "name").as(name => name || "Unknown"), + label: bind(stream, "description").as(desc => { //need to add filter for "audio stream1", "" + const maxLength = (35 - (desc.length + 3)); + let title = `${stream.name.substring(0, maxLength)}${stream.name.length >= maxLength ? '...' : ""}` + return `${desc} - ${title}`; + }), truncate: true, tooltipText: bind(stream, "name"), className: "name", From 122268a938560592ab035d9bc3d2a80acf3234bb Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:10:08 +0300 Subject: [PATCH 05/64] Update Sound.ts --- ags/widget/control-center/pages/Sound.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ags/widget/control-center/pages/Sound.ts b/ags/widget/control-center/pages/Sound.ts index c6eed02b..1f8c8627 100644 --- a/ags/widget/control-center/pages/Sound.ts +++ b/ags/widget/control-center/pages/Sound.ts @@ -70,7 +70,7 @@ export function PageSound(): Page { ), onDestroy: () => connections.map(id => eventbox.disconnect(id)), child: new Widget.Label({ - label: bind(stream, "description").as(desc => { //need to add filter for "audio stream1", "" + label: bind(stream, "description").as(desc => { //need to add filter for "audio stream1" and etc... const maxLength = (35 - (desc.length + 3)); let title = `${stream.name.substring(0, maxLength)}${stream.name.length >= maxLength ? '...' : ""}` return `${desc} - ${title}`; From 706adeb27936b5feec00ed5b14a06e62f5d52a25 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:11:55 +0300 Subject: [PATCH 06/64] Update Media.ts Showing artist if player has information about it --- ags/widget/bar/Media.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/ags/widget/bar/Media.ts b/ags/widget/bar/Media.ts index 9e655834..0233b64d 100644 --- a/ags/widget/bar/Media.ts +++ b/ags/widget/bar/Media.ts @@ -1,11 +1,13 @@ -import { bind } from "astal"; +import { bind, exec } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import AstalMpris from "gi://AstalMpris"; import { getSymbolicIcon } from "../../scripts/apps"; import { Separator, SeparatorProps } from "../Separator"; import { Windows } from "../../windows"; +import { Clipboard } from "../../scripts/clipboard"; export function Media(): Gtk.Widget { + const connections: Array = []; const mediaControlsRevealer: Widget.Revealer = new Widget.Revealer({ @@ -24,9 +26,14 @@ export function Media(): Gtk.Widget { icon: "edit-paste-symbolic" } as Widget.IconProps), tooltipText: "Copy link to Clipboard", - visible: bind(players[0], "metadata").as((metadata) => - metadata["xesam:url"]?.get_string()[0] != null), - onClick: () => console.log(players[0].metadata["xesam:url"]?.get_string()[0]!) + // AstalMpris.Player.metadata works only sometimes, so I'm not using it + visible: bind(players[0], "metadata").as(Boolean), + onClick: async () => { + const link = exec(`playerctl --player=${ + players[0].busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") + } metadata xesam:url`); + link && Clipboard.getDefault().copyAsync(link); + } } as Widget.ButtonProps), new Widget.Button({ className: "previous", @@ -97,17 +104,18 @@ export function Media(): Gtk.Widget { truncate: true } as Widget.LabelProps), Separator({ + visible: bind(players[0], "artist").as(artist => artist ? true : false), orientation: Gtk.Orientation.HORIZONTAL, size: 1, margin: 5, - //cssColor: `rgb(180, 180, 180)`, alpha: .3 } as SeparatorProps), new Widget.Label({ className: "artist", - label: bind(players[0], "artist").as((artist: string) => artist || "No Artist"), + visible: bind(players[0], "artist").as(artist => artist ? true : false), + label: bind(players[0], "artist").as((artist: string) => artist), maxWidthChars: 18, - truncate: true + truncate: true, } as Widget.LabelProps) ] : new Widget.Label({ label: "Crazy to think this widget haven't disappeared yet!" From fc49d91808acac3344c67845fda0dde01cb4f3fa Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:13:29 +0300 Subject: [PATCH 07/64] Update Page.ts Add switcher --- ags/widget/control-center/pages/Page.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ags/widget/control-center/pages/Page.ts b/ags/widget/control-center/pages/Page.ts index a30f15aa..3a49de66 100644 --- a/ags/widget/control-center/pages/Page.ts +++ b/ags/widget/control-center/pages/Page.ts @@ -179,6 +179,7 @@ export function PageButton({ onDestroy, ...props }: { endWidget?: Gtk.Widget | Binding; description?: string | Binding; extraButtons?: Array | Binding>; + switches?: Array | Binding>; onDestroy?: (self: Widget.Box) => void; onClick?: (self: Widget.Button) => void; tooltipText?: string | Binding; @@ -213,7 +214,7 @@ export function PageButton({ onDestroy, ...props }: { new Widget.Label({ className: "title", xalign: 0, - // truncating is not working, so I had to do this + //truncate: true, label: (props.title instanceof Binding) ? props.title.as((title) => `${title.substring(0, 35)}${ @@ -221,7 +222,6 @@ export function PageButton({ onDestroy, ...props }: { : `${props.title.substring(0, 35)}${ props.title.length > 35 ? '…' : ""}`, tooltipText: props.title, - truncate: true, } as Widget.LabelProps), new Widget.Label({ className: "description", @@ -232,7 +232,7 @@ export function PageButton({ onDestroy, ...props }: { label: props.description, truncate: true, tooltipText: props.description - } as Widget.LabelProps) + } as Widget.LabelProps), ] } as Widget.BoxProps), new Widget.Box({ @@ -240,11 +240,16 @@ export function PageButton({ onDestroy, ...props }: { props.endWidget.as(Boolean) : props.endWidget, halign: Gtk.Align.END, - child: props.endWidget + child: props.endWidget, } as Widget.BoxProps) ] } as Widget.BoxProps) } as Widget.ButtonProps), + new Widget.Box({ + //className: "switches", + visible: (props.switches instanceof Binding) ? props.switches.as(Boolean) : Boolean(props.switches), + children: props.switches + } as Widget.BoxProps), new Widget.Box({ className: "extra-buttons button-row", visible: (props.extraButtons instanceof Binding) ? From 6ac17f5886340e1f3229deada495aa22c1075abb Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:21:41 +0300 Subject: [PATCH 08/64] Update style.scss --- ags/style.scss | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/ags/style.scss b/ags/style.scss index 33c00bf4..d981e736 100644 --- a/ags/style.scss +++ b/ags/style.scss @@ -213,7 +213,7 @@ menu { } } -.button-row { +.button-row, .top-row { & > button { background: colors.$bg-secondary; margin: 0 1px; @@ -223,7 +223,7 @@ menu { &:hover { background: colors.$bg-tertiary; } - + &:first-child { border-top-left-radius: 10px; border-bottom-left-radius: 10px; @@ -238,6 +238,24 @@ menu { } } +//Need some workouts/////////////// +switch { + padding: 4px; + border-radius: 16px; + background: functions.toRGB(color.adjust($color: wal.$color1, $lightness: -20%)); + &:checked { + background: colors.$bg-tertiary; + } +} + +switch slider { + border-radius: 12px; + color: colors.$bg-primary; + background: wal.$foreground; + transition: all 0.2s ease-in-out; +} +///////////////////////////////////// + selection { background: colors.$bg-tertiary; } From 4895a6393616145caad06b1701be50cf2fd251b4 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:23:43 +0300 Subject: [PATCH 09/64] Update _control-center.scss --- ags/style/_control-center.scss | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ags/style/_control-center.scss b/ags/style/_control-center.scss index 26a547a6..c73466c5 100644 --- a/ags/style/_control-center.scss +++ b/ags/style/_control-center.scss @@ -31,7 +31,7 @@ font-weight: 600; } - & > box:not(.button-row) icon { + & > box:not(.button-row):not(.top-row) icon { font-size: 12px; color: colors.$fg-disabled; margin-right: 3px; @@ -43,7 +43,7 @@ color: colors.$fg-disabled; } - & .button-row { + & .button-row, & .top-row { & button { padding: 7px; margin: { @@ -165,6 +165,7 @@ } box.history { + margin-top: 10px; background: colors.$bg-translucent; box-shadow: 0 0 6px 1px colors.$bg-translucent; border-radius: 24px; @@ -182,8 +183,8 @@ box.history { } } - & > .button-row { - margin-top: 12px; + & > .button-row, & > .top-row { + margin-bottom: 12px; & button { padding: 6px; From 6f0b478d73f41d9f7bccede1b3b5611888b6ca34 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:29:19 +0300 Subject: [PATCH 10/64] Update BigMedia.ts isFinite() doesn't work, so I add stupid checker --- ags/widget/center-window/BigMedia.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts index 34ce7d36..4f9403c8 100644 --- a/ags/widget/center-window/BigMedia.ts +++ b/ags/widget/center-window/BigMedia.ts @@ -1,7 +1,7 @@ -import { AstalIO, bind, Binding, execAsync, GLib, timeout } from "astal"; +import { AstalIO, bind, Binding, exec, timeout } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import AstalMpris from "gi://AstalMpris"; - +import { Players } from "../../scripts/player"; export function BigMedia(): Gtk.Widget { let dragTimer: (AstalIO.Time|undefined); @@ -22,8 +22,8 @@ export function BigMedia(): Gtk.Widget { hexpand: false, orientation: Gtk.Orientation.VERTICAL, marginTop: 6, - visible: getAlbumArt(players[0]).as(Boolean), - css: getAlbumArt(players[0]).as((artUrl: string|undefined) => + visible: Players.getDefault().getAlbumArt(players[0]).as(Boolean), + css: Players.getDefault().getAlbumArt(players[0]).as((artUrl: string|undefined) => artUrl ? `.image { background-image: url('${artUrl}'); }` : undefined), width_request: 132, height_request: 128 @@ -60,7 +60,8 @@ export function BigMedia(): Gtk.Widget { min: 0, hexpand: true, max: bind(players[0], "length").as((length: number) => - Math.floor(length)), + (length > 129600000) ? Math.floor(players[0].get_position()) + : Math.floor(length)), // for streams and players whitch dont have a length value: bind(players[0], "position").as((position: number) => Math.floor(position)), onDragged: (slider: Widget.Slider) => { @@ -101,9 +102,13 @@ export function BigMedia(): Gtk.Widget { icon: "edit-paste-symbolic" } as Widget.IconProps), tooltipText: "Copy link to Clipboard", - visible: bind(players[0], "metadata").as((_meta: GLib.HashTable) => - players[0].get_meta("xesam:url") === null), - onClick: () => execAsync(`sh -c "wl-copy \\"$(playerctl metadata 'xesam:url')\\""`) + visible: bind(players[0], "metadata").as(Boolean), + onClick: async () => { + const link = exec(`playerctl --player=${ + players[0].busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") + } metadata xesam:url`); + link && Clipboard.getDefault().copyAsync(link); + } } as Widget.ButtonProps), new Widget.Button({ className: "shuffle", @@ -189,7 +194,8 @@ export function BigMedia(): Gtk.Widget { halign: Gtk.Align.END, label: bind(players[0], "length").as((len/* bananananananana */: number) => { const sec: number = Math.floor(len % 60); - return (len > 0 && Number.isFinite(len)) ? + console.log(len); + return (len > 0 && len < 129600000) ? `${Math.floor(len / 60)}:${sec < 10 ? "0" : ""}${sec}` : "0:00"; }) From d0a0ae744146027a16f1aa03fb8828ae849279ab Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 28 Jun 2025 21:34:42 +0300 Subject: [PATCH 11/64] Update NotifHistory.ts Add switcher --- ags/widget/control-center/NotifHistory.ts | 65 ++++++++++++++--------- 1 file changed, 41 insertions(+), 24 deletions(-) diff --git a/ags/widget/control-center/NotifHistory.ts b/ags/widget/control-center/NotifHistory.ts index a5728056..de1a3013 100644 --- a/ags/widget/control-center/NotifHistory.ts +++ b/ags/widget/control-center/NotifHistory.ts @@ -10,6 +10,46 @@ export const NotifHistory = () => { orientation: Gtk.Orientation.VERTICAL, className: bind(Notifications.getDefault(), "history").as(history => history.length > 0 ? "history" : "history hide"), children: [ + new Widget.Box({ + vexpand: false, + hexpand: true, + className: "top-row", + children: [ + new Widget.Box({ + className: "dnd-box", + hexpand: true, + halign: Gtk.Align.START, + children: [ + new Widget.Label({ + css: "margin-right: 6px;", + label: tr("control_center.tiles.dnd.title") + } as Widget.LabelProps), + new Widget.Switch({ + onNotifyActive: (self) => Notifications.getDefault().getNotifd().dontDisturb = self.active, + state: Notifications.getDefault().getNotifd().dontDisturb, + } as Widget.SwitchProps) + ] + + } as Widget.BoxProps), + new Widget.Button({ + css: `border-radius: 10px;`, + className: "clear-all", + halign: Gtk.Align.END, + child: new Widget.Box({ + children: [ + new Widget.Icon({ + css: "margin-right: 6px;", + icon: "edit-clear-all-symbolic" + } as Widget.IconProps), + new Widget.Label({ + label: tr("clear") + } as Widget.LabelProps) + ] + } as Widget.BoxProps), + onClick: () => Notifications.getDefault().clearHistory(), + } as Widget.ButtonProps) + ] + }), new Widget.Scrollable({ className: "history", hscroll: Gtk.PolicyType.NEVER, @@ -36,30 +76,7 @@ export const NotifHistory = () => { () => Notifications.getDefault().removeHistory(notification.id), true) )) } as Widget.BoxProps) - } as Widget.ScrollableProps), - new Widget.Box({ - vexpand: false, - hexpand: true, - halign: Gtk.Align.END, - className: "button-row", - children: [ - new Widget.Button({ - className: "clear-all", - child: new Widget.Box({ - children: [ - new Widget.Icon({ - css: "margin-right: 6px;", - icon: "edit-clear-all-symbolic" - } as Widget.IconProps), - new Widget.Label({ - label: tr("clear") - } as Widget.LabelProps) - ] - } as Widget.BoxProps), - onClick: () => Notifications.getDefault().clearHistory(), - } as Widget.ButtonProps) - ] - }) + } as Widget.ScrollableProps) ] } as Widget.BoxProps); } From dc5bd7c6d6b71ab2a9a5823c451d8bcde589d1eb Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 28 Jun 2025 23:32:30 +0300 Subject: [PATCH 12/64] Update app.ts --- ags/app.ts | 125 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/ags/app.ts b/ags/app.ts index 7df09e16..9db147ff 100644 --- a/ags/app.ts +++ b/ags/app.ts @@ -101,7 +101,132 @@ function triggerOSD(osdModeParam: OSDModes) { osdTimer = undefined; Windows.close("osd"); }); +import AstalNotifd from "gi://AstalNotifd"; + +import { App, Astal } from "astal/gtk3" +import { Wireplumber } from "./scripts/volume"; + +import { handleArguments } from "./scripts/arg-handler"; +import { Time, timeout } from "astal/time"; + +import { OSDModes, setOSDMode } from "./window/OSD"; +import { Runner } from "./runner/Runner"; +import { PluginApps } from "./runner/plugins/apps"; +import { PluginShell } from "./runner/plugins/shell"; +import { PluginWebSearch } from "./runner/plugins/websearch"; +import { PluginMedia } from "./runner/plugins/media"; +import { Windows } from "./windows"; +import { Notifications } from "./scripts/notifications"; +import { GObject } from "astal"; +import { PluginWallpapers } from "./runner/plugins/wallpapers"; +import { Wallpaper } from "./scripts/wallpaper"; +import { Stylesheet } from "./scripts/stylesheet"; +import { Clipboard } from "./scripts/clipboard"; +import { PluginClipboard } from "./runner/plugins/clipboard"; +import { Config } from "./scripts/config"; + + +import { Players } from "./scripts/player"; + + +let osdTimer: (Time|undefined); +let osdDebounceTimer: (Time|undefined); +let connections = new Map | number)>(); + +const defaultWindows: Array = [ "bar" ]; +const runnerPlugins: Array = [ + PluginApps, + PluginShell, + PluginWebSearch, + PluginMedia, + new PluginWallpapers(), + PluginClipboard +]; + +App.start({ + instanceName: "astal", + icons: "icons/", + requestHandler: (request: string, response: (result: any) => void): void => { + response(handleArguments(request)); + }, + main: (..._args: Array) => { + console.log(`Initialized astal instance as: ${ App.instanceName || "astal" }`); + + + console.log("Config: initializing configuration file"); + Config.getDefault(); + + Stylesheet.getDefault().compileApply(); + + App.vfunc_dispose = () => { + console.log("Disconnecting stuff"); + connections.forEach((v, k) => Array.isArray(v) ? + v.map(id => k.disconnect(id)) + : k.disconnect(v)); + }; + + // Init clipboard module + Clipboard.getDefault(); + + connections.set(Wireplumber.getDefault(), [ + Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () => + triggerOSD(OSDModes.SINK)), + Wireplumber.getDefault().getDefaultSink().connect("notify::mute", () => + triggerOSD(OSDModes.SINK)), + Wireplumber.getDefault().getDefaultSource().connect("notify::volume", () => + triggerOSD(OSDModes.SOURCE)), + Wireplumber.getDefault().getDefaultSource().connect("notify::mute", () => + triggerOSD(OSDModes.SOURCE)), + ]); + + connections.set(Notifications.getDefault(), [ + Notifications.getDefault().connect("notification-added", (_, _notif: AstalNotifd.Notification) => { + Windows.open("floating-notifications"); + }), + Notifications.getDefault().connect("notification-removed", (_: Notifications, _id: number) => { + _.notifications.length === 0 && Windows.close("floating-notifications"); + }) + ]); + + console.log("Initializing wallpaper handler"); + Wallpaper.getDefault(); + + console.log("Adding runner plugins"); + runnerPlugins.map(plugin => Runner.addPlugin(plugin)); + + console.log("Opening default windows"); + // Open openOnStart windows + defaultWindows.map(name => { + if(Windows.isVisible(name)) return; + Windows.open(name); + }); + } +}); + +function triggerOSD(osdModeParam: OSDModes) { + if (Windows.isVisible("control-center")) return; + + if (osdDebounceTimer) { + osdDebounceTimer.cancel(); + } + + // For some reasons Wireplumber refreshin hole devices list if + // you trigger micro mute + osdDebounceTimer = timeout(1, () => { + Windows.open("osd"); + setOSDMode(osdModeParam); + + if (osdTimer) { + osdTimer.cancel(); + } + + osdTimer = timeout(3000, () => { + Windows.close("osd"); + osdTimer = undefined; + }); + }); +} return; } From 771d6e5bdf9b98e4859ed49cfc1b3007be35c293 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 1 Jul 2025 18:55:17 +0300 Subject: [PATCH 13/64] Fixing calls --- ags/window/OSD.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/ags/window/OSD.ts b/ags/window/OSD.ts index e8f0efbe..fe4a22ff 100644 --- a/ags/window/OSD.ts +++ b/ags/window/OSD.ts @@ -128,7 +128,6 @@ export const OSD = (mon: number) => { switch (mode) { case OSDModes.SINK: return "sink"; case OSDModes.SOURCE: return "source"; - default: return "sink"; } }), onDestroy: () => { From 4e836ae78f50170eb3547997555e1d41b5b081ea Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 1 Jul 2025 18:57:09 +0300 Subject: [PATCH 14/64] Fixed default call --- ags/app.ts | 133 +++-------------------------------------------------- 1 file changed, 7 insertions(+), 126 deletions(-) diff --git a/ags/app.ts b/ags/app.ts index 9db147ff..b3194c52 100644 --- a/ags/app.ts +++ b/ags/app.ts @@ -1,107 +1,5 @@ import AstalNotifd from "gi://AstalNotifd"; - -import { App } from "astal/gtk3" -import { Wireplumber } from "./scripts/volume"; - -import { handleArguments } from "./scripts/arg-handler"; -import { Time, timeout } from "astal/time"; - -import { OSDModes, setOSDMode } from "./window/OSD"; - -import { Runner } from "./runner/Runner"; -import { PluginApps } from "./runner/plugins/apps"; -import { PluginShell } from "./runner/plugins/shell"; -import { PluginWebSearch } from "./runner/plugins/websearch"; -import { PluginMedia } from "./runner/plugins/media"; -import { Windows } from "./windows"; -import { Notifications } from "./scripts/notifications"; -import { GObject } from "astal"; -import { PluginWallpapers } from "./runner/plugins/wallpapers"; -import { Wallpaper } from "./scripts/wallpaper"; -import { Stylesheet } from "./scripts/stylesheet"; -import { Clipboard } from "./scripts/clipboard"; -import { PluginClipboard } from "./runner/plugins/clipboard"; -import { Config } from "./scripts/config"; - - -let osdTimer: (Time|undefined); -let connections = new Map | number)>(); - -const defaultWindows: Array = [ "bar" ]; -const runnerPlugins: Array = [ - PluginApps, - PluginShell, - PluginWebSearch, - PluginMedia, - new PluginWallpapers(), - PluginClipboard -]; - -App.start({ - instanceName: "astal", - icons: "icons/", - requestHandler: (request: string, response: (result: any) => void): void => { - response(handleArguments(request)); - }, - main: (..._args: Array) => { - console.log(`Initialized astal instance as: ${ App.instanceName || "astal" }`); - - console.log("Config: initializing configuration file"); - Config.getDefault(); - - Stylesheet.getDefault().compileApply(); - - App.vfunc_dispose = () => { - console.log("Disconnecting stuff"); - connections.forEach((v, k) => Array.isArray(v) ? - v.map(id => k.disconnect(id)) - : k.disconnect(v)); - }; - - // Init clipboard module - Clipboard.getDefault(); - - connections.set(Wireplumber.getDefault(), [ - Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () => - triggerOSD(OSDModes.SINK)) - ]); - - connections.set(Notifications.getDefault(), [ - Notifications.getDefault().connect("notification-added", (_, _notif: AstalNotifd.Notification) => { - Windows.open("floating-notifications"); - }), - Notifications.getDefault().connect("notification-removed", (_: Notifications, _id: number) => { - _.notifications.length === 0 && Windows.close("floating-notifications"); - }) - ]); - - console.log("Initializing wallpaper handler"); - Wallpaper.getDefault(); - - console.log("Adding runner plugins"); - runnerPlugins.map(plugin => Runner.addPlugin(plugin)); - - console.log("Opening default windows"); - // Open openOnStart windows - defaultWindows.map(name => { - if(Windows.isVisible(name)) return; - Windows.open(name); - }); - } -}); - -function triggerOSD(osdModeParam: OSDModes) { - if(Windows.isVisible("control-center")) return; - - Windows.open("osd"); - - if(!osdTimer) { - setOSDMode(osdModeParam); - osdTimer = timeout(3000, () => { - osdTimer = undefined; - Windows.close("osd"); - }); -import AstalNotifd from "gi://AstalNotifd"; +import AstalHyprland from "gi://AstalHyprland"; import { App, Astal } from "astal/gtk3" import { Wireplumber } from "./scripts/volume"; @@ -109,7 +7,7 @@ import { Wireplumber } from "./scripts/volume"; import { handleArguments } from "./scripts/arg-handler"; import { Time, timeout } from "astal/time"; -import { OSDModes, setOSDMode } from "./window/OSD"; +import { OSDModes, setOSDMode, updateLayout } from "./window/OSD"; import { Runner } from "./runner/Runner"; import { PluginApps } from "./runner/plugins/apps"; @@ -131,7 +29,6 @@ import { Players } from "./scripts/player"; let osdTimer: (Time|undefined); -let osdDebounceTimer: (Time|undefined); let connections = new Map | number)>(); const defaultWindows: Array = [ "bar" ]; @@ -188,6 +85,7 @@ App.start({ _.notifications.length === 0 && Windows.close("floating-notifications"); }) ]); + console.log("Initializing wallpaper handler"); Wallpaper.getDefault(); @@ -207,30 +105,13 @@ App.start({ function triggerOSD(osdModeParam: OSDModes) { if (Windows.isVisible("control-center")) return; - if (osdDebounceTimer) { - osdDebounceTimer.cancel(); - } - - // For some reasons Wireplumber refreshin hole devices list if - // you trigger micro mute - osdDebounceTimer = timeout(1, () => { - Windows.open("osd"); - setOSDMode(osdModeParam); - - if (osdTimer) { - osdTimer.cancel(); - } + Windows.open("osd"); + setOSDMode(osdModeParam); - osdTimer = timeout(3000, () => { - Windows.close("osd"); - osdTimer = undefined; - }); - }); -} - return; + if (osdTimer) { + osdTimer.cancel(); } - osdTimer.cancel(); osdTimer = timeout(3000, () => { Windows.close("osd"); osdTimer = undefined; From 9467814d9defec66fcbff0d276eb6cb4b0fb9b9c Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 1 Jul 2025 18:57:43 +0300 Subject: [PATCH 15/64] ops --- ags/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ags/app.ts b/ags/app.ts index b3194c52..6745a773 100644 --- a/ags/app.ts +++ b/ags/app.ts @@ -7,7 +7,7 @@ import { Wireplumber } from "./scripts/volume"; import { handleArguments } from "./scripts/arg-handler"; import { Time, timeout } from "astal/time"; -import { OSDModes, setOSDMode, updateLayout } from "./window/OSD"; +import { OSDModes, setOSDMode } from "./window/OSD"; import { Runner } from "./runner/Runner"; import { PluginApps } from "./runner/plugins/apps"; From ce39f57bc46f3a4ed47fabfeaff19caf48c5e28b Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:39:31 +0300 Subject: [PATCH 16/64] Add Keyboard Layout connection for OSD --- ags/app.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ags/app.ts b/ags/app.ts index 6745a773..00a5361f 100644 --- a/ags/app.ts +++ b/ags/app.ts @@ -7,7 +7,7 @@ import { Wireplumber } from "./scripts/volume"; import { handleArguments } from "./scripts/arg-handler"; import { Time, timeout } from "astal/time"; -import { OSDModes, setOSDMode } from "./window/OSD"; +import { OSDModes, setOSDMode, updateLayout } from "./window/OSD"; import { Runner } from "./runner/Runner"; import { PluginApps } from "./runner/plugins/apps"; @@ -66,6 +66,13 @@ App.start({ // Init clipboard module Clipboard.getDefault(); + connections.set(AstalHyprland.get_default(), [ + AstalHyprland.get_default().connect("keyboard-layout", (_, Keyboard, layout) => { + updateLayout(layout); + triggerOSD(OSDModes.LAYOUT); + }) + ]); + connections.set(Wireplumber.getDefault(), [ Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () => triggerOSD(OSDModes.SINK)), @@ -85,7 +92,6 @@ App.start({ _.notifications.length === 0 && Windows.close("floating-notifications"); }) ]); - console.log("Initializing wallpaper handler"); Wallpaper.getDefault(); From 6767d9bd6c22700e3930cb4f867ef8586b632886 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:40:36 +0300 Subject: [PATCH 17/64] Add Layout Widget --- ags/window/OSD.ts | 57 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 6 deletions(-) diff --git a/ags/window/OSD.ts b/ags/window/OSD.ts index fe4a22ff..0b3d481f 100644 --- a/ags/window/OSD.ts +++ b/ags/window/OSD.ts @@ -1,15 +1,22 @@ -import { bind, Variable } from "astal"; +import { bind, Binding, Variable } from "astal"; import { Astal, Gtk, Widget } from "astal/gtk3"; import { Wireplumber } from "../scripts/volume"; import AstalWp from "gi://AstalWp"; +import AstalHyprland from "gi://AstalHyprland"; export enum OSDModes { SINK, SOURCE, + LAYOUT, BRIGHTNESS } let osdMode: (Variable|null); +const layoutVar = new Variable(""); + +export function updateLayout(layout: string) { + layoutVar.set(layout.substring(0, 2).toUpperCase()); +} export function setOSDMode(newMode: OSDModes): void { if(!osdMode) return; @@ -18,6 +25,28 @@ export function setOSDMode(newMode: OSDModes): void { } function createOSD( + props: Widget.BoxProps, + iconName: string | Binding, + labelOSD?: string | Binding +) { + return new Widget.Box({ + ...props, + className: `osd ${props.className || ''}`, + expand: true, + children: [ + new Widget.Icon({ + className: "icon", + icon: iconName, + } as Widget.IconProps), + new Widget.Label({ + className: "action", + label: labelOSD, + } as Widget.LabelProps) + ] + } as Widget.BoxProps) +} + +function createOSDLevelBar( props: Widget.BoxProps, bindable: AstalWp.Endpoint, iconName: string | Binding, @@ -74,7 +103,7 @@ function createOSD( function OSDSink() { const audio = Wireplumber.getDefault().getDefaultSink(); - return createOSD( + return createOSDLevelBar( { name: "sink" }, audio, bind(audio, "volumeIcon").as(icon => @@ -91,7 +120,7 @@ function OSDSink() { function OSDSource() { const source = Wireplumber.getDefault().getDefaultSource(); - return createOSD( + return createOSDLevelBar( { name: "source" }, source, bind(source, "volumeIcon").as(icon => @@ -106,6 +135,15 @@ function OSDSource() { ) } +function OSDLayout() { + const hyprland = AstalHyprland.get_default(); + return createOSD( + { name: "layout" }, + "input-keyboard-symbolic", + bind(layoutVar, "value") + ) +} + export const OSD = (mon: number) => { osdMode = new Variable(OSDModes.SINK); @@ -115,27 +153,34 @@ export const OSD = (mon: number) => { layer: Astal.Layer.OVERLAY, anchor: Astal.WindowAnchor.BOTTOM, canFocus: false, - clickThrough: true, - focusOnClick: false, marginBottom: 80, + focusOnClick: false, + clickThrough: true, monitor: mon, onDestroy: () => { osdMode?.drop(); osdMode = null; + //currenntLayout = null; }, child: new Widget.Stack({ + hhomogeneous: false, + vhomogeneous: false, visibleChildName: bind(osdMode, "value").as((mode: OSDModes) => { switch (mode) { case OSDModes.SINK: return "sink"; case OSDModes.SOURCE: return "source"; + case OSDModes.LAYOUT: return "layout"; + //default: return "sink"; } }), onDestroy: () => { - osdMode = null + osdMode = null; + //currenntLayout = null; }, children: [ OSDSink(), OSDSource(), + OSDLayout() ] } as Widget.BoxProps) } as Widget.WindowProps); From b3de49d02d26e55fb4b6324f80baf1fe64441f12 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:41:37 +0300 Subject: [PATCH 18/64] Update _osd.scss --- ags/style/_osd.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ags/style/_osd.scss b/ags/style/_osd.scss index 5740e113..a31b1636 100644 --- a/ags/style/_osd.scss +++ b/ags/style/_osd.scss @@ -7,7 +7,7 @@ background: funs.toRGB(color.change($color: wal.$background, $alpha: 65%)); padding: 16px; border-radius: 24px; - min-width: 180px; + min-width: 60px; .icon { margin-right: 10px; @@ -17,7 +17,7 @@ .volume { margin-top: -6px; - .device { + .device, .action { margin-bottom: 5px; font-size: 14px; font-weight: 600; From 56187bee711596ff7b523d86fd40d1bf6ee1f204 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 1 Jul 2025 19:58:05 +0300 Subject: [PATCH 19/64] Update Sound.ts --- ags/widget/control-center/pages/Sound.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ags/widget/control-center/pages/Sound.ts b/ags/widget/control-center/pages/Sound.ts index 1f8c8627..d81499e8 100644 --- a/ags/widget/control-center/pages/Sound.ts +++ b/ags/widget/control-center/pages/Sound.ts @@ -1,7 +1,7 @@ import { Page, PageButton, PageProps } from "./Page"; import { bind, Variable } from "astal"; import { Astal, Gtk, Widget } from "astal/gtk3"; -import { getAppIcon, getSymbolicIcon } from "../../../scripts/apps"; +import { getAppIcon, getIconByAppName, getSymbolicIcon } from "../../../scripts/apps"; import { Wireplumber } from "../../../scripts/volume"; import { tr } from "../../../i18n/intl"; import { analyser } from "../../../scripts/utils"; @@ -54,8 +54,8 @@ export function PageSound(): Page { children: [ new Widget.Icon({ icon: bind(stream, "description").as(icon => - getSymbolicIcon(icon) ?? "application-x-executable-symbolic"), - css: "font-size: 18px; margin-right: 6px;" + getSymbolicIcon(icon) ?? getIconByAppName(icon)), + css: "font-size: 20px; margin-right: 6px;" } as Widget.IconProps), new Widget.Box({ orientation: Gtk.Orientation.VERTICAL, @@ -70,7 +70,7 @@ export function PageSound(): Page { ), onDestroy: () => connections.map(id => eventbox.disconnect(id)), child: new Widget.Label({ - label: bind(stream, "description").as(desc => { //need to add filter for "audio stream1" and etc... + label: bind(stream, "description").as(desc => { //need to add filter for "audio stream1" const maxLength = (35 - (desc.length + 3)); let title = `${stream.name.substring(0, maxLength)}${stream.name.length >= maxLength ? '...' : ""}` return `${desc} - ${title}`; From b111be50c05351831f3b1efc5b8c3525eb328d4f Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 1 Jul 2025 20:38:24 +0300 Subject: [PATCH 20/64] Ops --- ags/style/_osd.scss | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ags/style/_osd.scss b/ags/style/_osd.scss index a31b1636..939dd729 100644 --- a/ags/style/_osd.scss +++ b/ags/style/_osd.scss @@ -9,6 +9,11 @@ border-radius: 24px; min-width: 60px; + .action { + font-size: 14px; + font-weight: 600; + } + .icon { margin-right: 10px; font-size: 24px; @@ -17,7 +22,7 @@ .volume { margin-top: -6px; - .device, .action { + .device { margin-bottom: 5px; font-size: 14px; font-weight: 600; From 0d9b7033b57715e06c2fa8387b2be0ba74fee21b Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 1 Jul 2025 21:13:40 +0300 Subject: [PATCH 21/64] Adjusted width_requedst value --- ags/window/OSD.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ags/window/OSD.ts b/ags/window/OSD.ts index 0b3d481f..c7d2b809 100644 --- a/ags/window/OSD.ts +++ b/ags/window/OSD.ts @@ -80,7 +80,7 @@ function createOSDLevelBar( children: [ new Widget.LevelBar({ className: "levelbar", - width_request: 120, + width_request: 140, value: valueOSD, maxValue: maxValueOSD, expand: true, From 52999b988015fdc3dd744469ef3cd2d3117357df Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Fri, 4 Jul 2025 13:01:18 +0300 Subject: [PATCH 22/64] Add hours, identify in `Big Media` --- ags/widget/center-window/BigMedia.ts | 43 ++++++++-------------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts index 768b3a5b..58f13538 100644 --- a/ags/widget/center-window/BigMedia.ts +++ b/ags/widget/center-window/BigMedia.ts @@ -1,7 +1,7 @@ import { AstalIO, bind, Binding, exec, timeout } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import AstalMpris from "gi://AstalMpris"; -import { Clipboard } from "../../scripts/clipboard"; +import { Players } from "../../scripts/player"; export function BigMedia(): Gtk.Widget { let dragTimer: (AstalIO.Time|undefined); @@ -44,8 +44,8 @@ export function BigMedia(): Gtk.Widget { } as Widget.LabelProps), new Widget.Label({ className: "artist", - tooltipText: bind(players[0], "artist").as((artist: string) => !artist ? "No Artist" : artist), - label: bind(players[0], "artist").as((artist: string) => !artist ? "No Artist" : artist), + tooltipText: bind(players[0], "artist").as((artist: string) => !artist ? (players[0].get_identity() ?? "No Artist") : artist), + label: bind(players[0], "artist").as((artist: string) => !artist ? (players[0].get_identity() ?? "No Artist") : artist), maxWidthChars: 28, truncate: true, } as Widget.LabelProps) @@ -88,9 +88,11 @@ export function BigMedia(): Gtk.Widget { halign: Gtk.Align.START, label: bind(players[0], "position").as((pos: number) => { const sec: number = Math.floor(pos % 60); + const min = Math.floor((pos % 3600) / 60); + const hours: number = Math.floor(pos / 3600); return pos > 0 && players[0].length > 0 ? - `${Math.floor(pos / 60)}:${sec < 10 ? "0" : ""}${sec}` - : `0:00`; + `${hours > 0 ? `${hours}:` : ''}${min < 10 && hours > 0 ? `0${min}` : `${min}`}:${sec < 10 ? `0${sec}` : `${sec}`}` + : `0:00`; }) } as Widget.LabelProps), centerWidget: new Widget.Box({ @@ -107,7 +109,6 @@ export function BigMedia(): Gtk.Widget { const link = exec(`playerctl --player=${ players[0].busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") } metadata xesam:url`); - link && Clipboard.getDefault().copyAsync(link); } } as Widget.ButtonProps), @@ -195,34 +196,14 @@ export function BigMedia(): Gtk.Widget { halign: Gtk.Align.END, label: bind(players[0], "length").as((len/* bananananananana */: number) => { const sec: number = Math.floor(len % 60); - console.log(len); - return (len > 0 && len < 129600000) ? - `${Math.floor(len / 60)}:${sec < 10 ? "0" : ""}${sec}` - : "0:00"; + const min = Math.floor((len % 3600) / 60); + const hours: number = Math.floor(len / 3600); + return (len > 0 && hours < 32) ? + `${hours > 0 ? `${hours}:` : ''}${min < 10 && hours > 0 ? `0${min}` : `${min}`}:${sec < 10 ? `0${sec}` : `${sec}`}` + : `0:00`; }) } as Widget.LabelProps) }) ]) } as Widget.BoxProps); } - - -/** - * This function handles album art/cover of playing media. If a file is provided - * by the player, it adds the "file://" uri as a prefix, so you can use it in css. - * - * @param player the player you want to pull album art from - * @returns Binding to player.artUrl containing the album art uri, or an undefined binding ig none was found. -* */ -function getAlbumArt(player: AstalMpris.Player): Binding { - return bind(player, "artUrl").as((artUrl: string) => { - - if(!artUrl) - return undefined; - - if(artUrl.startsWith("/")) - return "file://" + artUrl; - - return artUrl; - }); -} From f44150d21167ffd037bf1fede46ca13bc1d262d7 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Fri, 4 Jul 2025 13:22:07 +0300 Subject: [PATCH 23/64] Add Live stream scenario in Media player --- ags/widget/center-window/BigMedia.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts index 58f13538..9bade179 100644 --- a/ags/widget/center-window/BigMedia.ts +++ b/ags/widget/center-window/BigMedia.ts @@ -195,12 +195,15 @@ export function BigMedia(): Gtk.Widget { valign: Gtk.Align.START, halign: Gtk.Align.END, label: bind(players[0], "length").as((len/* bananananananana */: number) => { + const maxLen: number = 9223372036854; + const sec: number = Math.floor(len % 60); const min = Math.floor((len % 3600) / 60); - const hours: number = Math.floor(len / 3600); - return (len > 0 && hours < 32) ? - `${hours > 0 ? `${hours}:` : ''}${min < 10 && hours > 0 ? `0${min}` : `${min}`}:${sec < 10 ? `0${sec}` : `${sec}`}` - : `0:00`; + const hour: number = Math.floor(len / 3600); + + return (len > 0 && hour < maxLen / 10000000) ? + `${hour > 0 ? `${hour}:` : ''}${min < 10 && hour > 0 ? `0${min}` : `${min}`}:${sec < 10 ? `0${sec}` : `${sec}`}` + : (len <= 0 ? `0:00` : "Live"); }) } as Widget.LabelProps) }) From fd44116f44c673583b2b07349e0fe7af2e9fb42b Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:17:35 +0300 Subject: [PATCH 24/64] Now the icon is obtained in a simpler and faster way --- ags/widget/bar/Media.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/ags/widget/bar/Media.ts b/ags/widget/bar/Media.ts index ff233164..1c4ee4bb 100644 --- a/ags/widget/bar/Media.ts +++ b/ags/widget/bar/Media.ts @@ -89,14 +89,7 @@ export function Media(): Gtk.Widget { children: bind(AstalMpris.get_default(), "players").as((players: Array) => players[0] ? [ new Widget.Icon({ - icon: bind(players[0], "busName").as((busName: string) => { - const splitName = busName.split('.').filter(str => str !== "" && !str.toLowerCase().includes('instance')); - if (getSymbolicIcon(splitName[splitName.length - 1])) { - return getSymbolicIcon(splitName[splitName.length - 1]); - } else { - return "folder-music-symbolic" - }; - }) + icon: getSymbolicIcon(players[0].get_entry()) ?? "folder-music-symbolic" } as Widget.IconProps), new Widget.Label({ className: "title", From 427f926eb6b714df7887c14b45782da4fdf7d337 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Fri, 4 Jul 2025 14:43:32 +0300 Subject: [PATCH 25/64] Fixed copying text with quotes --- ags/scripts/clipboard.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ags/scripts/clipboard.ts b/ags/scripts/clipboard.ts index b4a98040..33a0550c 100644 --- a/ags/scripts/clipboard.ts +++ b/ags/scripts/clipboard.ts @@ -81,7 +81,8 @@ class Clipboard extends GObject.Object { } public async copyAsync(content: string): Promise { - await execAsync(`wl-copy "${content}"`).catch((err: Gio.IOErrorEnum) => { + const sanitizedContent = content.replace(/"/g, '\\"'); + await execAsync(`wl-copy "${sanitizedContent}"`).catch((err: Gio.IOErrorEnum) => { console.error(`Clipboard: Couldn't copy text using wl-copy. Stderr:\n\t${err.message } | Stack:\n\t\t${err.stack}`); }); From 42ef725149203c2ee842de1a165e354bdde2833a Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Fri, 4 Jul 2025 17:15:23 +0300 Subject: [PATCH 26/64] Update OSD.ts --- ags/window/OSD.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ags/window/OSD.ts b/ags/window/OSD.ts index c7d2b809..f6e0dc21 100644 --- a/ags/window/OSD.ts +++ b/ags/window/OSD.ts @@ -9,13 +9,22 @@ export enum OSDModes { SOURCE, LAYOUT, BRIGHTNESS + //CAPSLOCK, + //NUMLOCK, + //PLAYER } let osdMode: (Variable|null); const layoutVar = new Variable(""); -export function updateLayout(layout: string) { - layoutVar.set(layout.substring(0, 2).toUpperCase()); +//It can be used to get values from connected elements and further process them. +export function variableHandler(mode: OSDModes, value: any) { + console.log(`Got value ${value}`); + switch (mode) { + case OSDModes.LAYOUT: + layoutVar.set(value.substring(0, 2).toUpperCase()); + break; + } } export function setOSDMode(newMode: OSDModes): void { From d11a7822e34727301cf35018cff139a463d36a0c Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Fri, 4 Jul 2025 17:16:09 +0300 Subject: [PATCH 27/64] Update app.ts --- ags/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ags/app.ts b/ags/app.ts index 00a5361f..76cc7d50 100644 --- a/ags/app.ts +++ b/ags/app.ts @@ -68,7 +68,7 @@ App.start({ connections.set(AstalHyprland.get_default(), [ AstalHyprland.get_default().connect("keyboard-layout", (_, Keyboard, layout) => { - updateLayout(layout); + variableHandler(OSDModes.LAYOUT, layout); triggerOSD(OSDModes.LAYOUT); }) ]); From 74a01372c879b2cbd2227ada8854778694faa74f Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Fri, 4 Jul 2025 20:04:54 +0300 Subject: [PATCH 28/64] Update Sound.ts --- ags/widget/control-center/pages/Sound.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ags/widget/control-center/pages/Sound.ts b/ags/widget/control-center/pages/Sound.ts index d81499e8..2cf71ec0 100644 --- a/ags/widget/control-center/pages/Sound.ts +++ b/ags/widget/control-center/pages/Sound.ts @@ -54,7 +54,7 @@ export function PageSound(): Page { children: [ new Widget.Icon({ icon: bind(stream, "description").as(icon => - getSymbolicIcon(icon) ?? getIconByAppName(icon)), + getSymbolicIcon(icon) ?? getIconByAppName(icon) ?? "application-x-executable-symbolic"), css: "font-size: 20px; margin-right: 6px;" } as Widget.IconProps), new Widget.Box({ From f81217195c15426af57a3eeae27c978eef48e545 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Fri, 4 Jul 2025 22:11:59 +0300 Subject: [PATCH 29/64] Update Media.ts --- ags/widget/bar/Media.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ags/widget/bar/Media.ts b/ags/widget/bar/Media.ts index 1c4ee4bb..33b7cfae 100644 --- a/ags/widget/bar/Media.ts +++ b/ags/widget/bar/Media.ts @@ -89,7 +89,9 @@ export function Media(): Gtk.Widget { children: bind(AstalMpris.get_default(), "players").as((players: Array) => players[0] ? [ new Widget.Icon({ - icon: getSymbolicIcon(players[0].get_entry()) ?? "folder-music-symbolic" + icon: getSymbolicIcon(players[0].get_entry()) ?? + getSymbolicIcon(players[0].get_bus_name().split('.').filter(str => !str.toLowerCase().includes('instance')).join('.')) ?? + "folder-music-symbolic" } as Widget.IconProps), new Widget.Label({ className: "title", From 93c33a95ff439c13797e6ba5fd6cb34bb0020358 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 5 Jul 2025 12:21:15 +0300 Subject: [PATCH 30/64] Update app.ts --- ags/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ags/app.ts b/ags/app.ts index 76cc7d50..72717bda 100644 --- a/ags/app.ts +++ b/ags/app.ts @@ -7,7 +7,7 @@ import { Wireplumber } from "./scripts/volume"; import { handleArguments } from "./scripts/arg-handler"; import { Time, timeout } from "astal/time"; -import { OSDModes, setOSDMode, updateLayout } from "./window/OSD"; +import { OSDModes, setOSDMode, variableHandler } from "./window/OSD"; import { Runner } from "./runner/Runner"; import { PluginApps } from "./runner/plugins/apps"; From 4fed944d22330b5c6b284f8057f46bbb24658666 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sun, 6 Jul 2025 14:20:08 +0300 Subject: [PATCH 31/64] Create player.ts --- ags/scripts/player.ts | 103 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 ags/scripts/player.ts diff --git a/ags/scripts/player.ts b/ags/scripts/player.ts new file mode 100644 index 00000000..2b3b8baf --- /dev/null +++ b/ags/scripts/player.ts @@ -0,0 +1,103 @@ +import { GObject, register, property, signal, Binding, bind } from "astal"; +import AstalMpris from "gi://AstalMpris"; + +export { AstalPlayers }; + +@register({ GTypeName: "AstalPlayers" }) +class AstalPlayers extends GObject.Object { + private static astalMpris: AstalMpris.Mpris = AstalMpris.Mpris.get_default(); + private static inst: AstalPlayers; + + #players: AstalMpris.Player[] = []; + #activePlayer: AstalMpris.Player | null = null; + #playerConnections: Map = new Map(); + + @property(AstalMpris.Player) + get activePlayer() { + return this.#activePlayer; + } + + constructor() { + super(); + + AstalPlayers.astalMpris.connect("player-added", (_, player) => this._addPlayer(player)); + AstalPlayers.astalMpris.connect("player-closed", (_, player) => this._removePlayer(player)); + + this.#players = AstalPlayers.astalMpris.get_players(); + this.#players.forEach(player => this._addPlayerSignals(player)); + + this._updateActivePlayer(); + } + + private _addPlayer(player: AstalMpris.Player) { + if (this.#players.includes(player)) return; + + this.#players.push(player); + this._addPlayerSignals(player); + this._updateActivePlayer(); + } + + private _addPlayerSignals(player: AstalMpris.Player) { + const handlerId = player.connect("notify::playback-status", () => { + this._updateActivePlayer(); + }); + this.#playerConnections.set(player, handlerId); + } + + private _removePlayer(player: AstalMpris.Player) { + this.#players = this.#players.filter(p => p !== player); + + if (this.#playerConnections.has(player)) { + player.disconnect(this.#playerConnections.get(player)!); + this.#playerConnections.delete(player); + } + + this._updateActivePlayer(); + } + + private _updateActivePlayer() { + const playingPlayer = this.#players.find(p => p.playback_status === AstalMpris.PlaybackStatus.PLAYING); + + let newActivePlayer; + + if (playingPlayer) { + newActivePlayer = playingPlayer; + } else if (this.#activePlayer && this.#players.includes(this.#activePlayer)) { + newActivePlayer = this.#activePlayer; + } else { + newActivePlayer = null; + } + + if (this.#activePlayer !== newActivePlayer) { + this.#activePlayer = newActivePlayer; + this.notify("active-player"); + } + } + + public static getDefault(): AstalPlayers { + if (!AstalPlayers.inst) + AstalPlayers.inst = new AstalPlayers(); + + return AstalPlayers.inst; + } + + /** + * This function handles album art/cover of playing media. If a file is provided + * by the player, it adds the "file://" uri as a prefix, so you can use it in css. + * + * @param player the player you want to pull album art from + * @returns Binding to player.artUrl containing the album art uri, or an undefined binding if none was found. + */ + public getAlbumArt(player: AstalMpris.Player): Binding { + return bind(player, "artUrl").as((artUrl: string) => { + + if (!artUrl) + return undefined; + + if (artUrl.startsWith("/")) + return "file://" + artUrl; + + return artUrl; + }); + } +} From 327ab4a4b4a1d444732d059c6821d36271b7726d Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sun, 6 Jul 2025 14:22:03 +0300 Subject: [PATCH 32/64] Update Media.ts Now widget will show active player in real time, even if you switch player --- ags/widget/bar/Media.ts | 48 +++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/ags/widget/bar/Media.ts b/ags/widget/bar/Media.ts index 33b7cfae..73665b72 100644 --- a/ags/widget/bar/Media.ts +++ b/ags/widget/bar/Media.ts @@ -1,11 +1,13 @@ import { bind, exec } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import AstalMpris from "gi://AstalMpris"; -import { getSymbolicIcon } from "../../scripts/apps"; +import { getAppIcon, getIconByAppName, getSymbolicIcon } from "../../scripts/apps"; import { Separator, SeparatorProps } from "../Separator"; import { Windows } from "../../windows"; import { Clipboard } from "../../scripts/clipboard"; +import { AstalPlayers } from "../../scripts/player"; + export function Media(): Gtk.Widget { const connections: Array = []; @@ -18,8 +20,8 @@ export function Media(): Gtk.Widget { className: "media-controls button-row", expand: false, homogeneous: false, - children: bind(AstalMpris.get_default(), "players").as((players: Array) => - players[0] ? [ + children: bind(AstalPlayers.getDefault(), "activePlayer").as((activePlayer: AstalMpris.Player) => + activePlayer ? [ new Widget.Button({ className: "link", image: new Widget.Icon({ @@ -27,12 +29,11 @@ export function Media(): Gtk.Widget { } as Widget.IconProps), tooltipText: "Copy link to Clipboard", // AstalMpris.Player.metadata works only sometimes, so I'm not using it - visible: bind(players[0], "metadata").as(Boolean), + visible: bind(activePlayer, "metadata").as(Boolean), onClick: async () => { const link = exec(`playerctl --player=${ - players[0].busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") + activePlayer.busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") } metadata xesam:url`); - link && Clipboard.getDefault().copyAsync(link); } } as Widget.ButtonProps), @@ -42,23 +43,23 @@ export function Media(): Gtk.Widget { icon: "media-skip-backward-symbolic" } as Widget.IconProps), tooltipText: "Previous", - onClick: () => players[0].canGoPrevious && players[0].previous() + onClick: () => activePlayer.canGoPrevious && activePlayer.previous() } as Widget.ButtonProps), new Widget.Button({ className: "play-pause", - tooltipText: bind(players[0], "playback_status").as((status) => + tooltipText: bind(activePlayer, "playback_status").as((status) => status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play"), image: new Widget.Icon({ - icon: bind(players[0], "playbackStatus").as((status: AstalMpris.PlaybackStatus) => + icon: bind(activePlayer, "playbackStatus").as((status: AstalMpris.PlaybackStatus) => status === AstalMpris.PlaybackStatus.PLAYING ? "media-playback-pause-symbolic" : "media-playback-start-symbolic") } as Widget.IconProps), - onClick: () => players[0].playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? - players[0].play() - : players[0].pause() + onClick: () => activePlayer.playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? + activePlayer.play() + : activePlayer.pause() } as Widget.ButtonProps), new Widget.Button({ className: "next", @@ -66,7 +67,7 @@ export function Media(): Gtk.Widget { icon: "media-skip-forward-symbolic" } as Widget.IconProps), tooltipText: "Next", - onClick: () => players[0].canGoNext && players[0].next() + onClick: () => activePlayer.canGoNext && activePlayer.next() } as Widget.ButtonProps) ] : new Widget.Label({ label: "Don't Stop The Music!" @@ -77,8 +78,8 @@ export function Media(): Gtk.Widget { const mediaWidget = new Widget.EventBox({ className: "media-eventbox", - visible: bind(AstalMpris.get_default(), "players").as((players: Array) => - players[0] && players[0].get_available()), + visible: bind(AstalPlayers.getDefault(), "activePlayer").as((activePlayer: AstalMpris.Player) => + activePlayer && activePlayer.get_available()), onDestroy: (_) => connections.map(id => _.disconnect(id)), onClick: () => Windows.toggle("center-window"), child: new Widget.Box({ @@ -86,30 +87,31 @@ export function Media(): Gtk.Widget { children: [ new Widget.Box({ spacing: 4, - children: bind(AstalMpris.get_default(), "players").as((players: Array) => - players[0] ? [ + children: bind(AstalPlayers.getDefault(), "activePlayer").as((activePlayer: AstalMpris.Player) => + activePlayer ? [ new Widget.Icon({ - icon: getSymbolicIcon(players[0].get_entry()) ?? - getSymbolicIcon(players[0].get_bus_name().split('.').filter(str => !str.toLowerCase().includes('instance')).join('.')) ?? + icon: getSymbolicIcon(activePlayer.get_entry()) ?? + getSymbolicIcon(activePlayer.get_bus_name().split('.').filter(str => !str.toLowerCase().includes('instance')).join('.')) ?? "folder-music-symbolic" } as Widget.IconProps), new Widget.Label({ className: "title", - label: bind(players[0], "title").as((title: string) => title || "No Title"), + label: bind(activePlayer, "title").as((title: string) => title || "No Title"), maxWidthChars: 20, truncate: true } as Widget.LabelProps), Separator({ - visible: bind(players[0], "artist").as(artist => artist ? true : false), + visible: bind(activePlayer, "artist").as(artist => artist ? true : false), orientation: Gtk.Orientation.HORIZONTAL, size: 1, margin: 5, + //cssColor: `rgb(180, 180, 180)`, alpha: .3 } as SeparatorProps), new Widget.Label({ className: "artist", - visible: bind(players[0], "artist").as(artist => artist ? true : false), - label: bind(players[0], "artist").as((artist: string) => artist), + visible: bind(activePlayer, "artist").as(artist => artist ? true : false), + label: bind(activePlayer, "artist").as((artist: string) => artist), maxWidthChars: 18, truncate: true, } as Widget.LabelProps) From 93affe28d643b938633b66f8e675de50b990a66a Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sun, 6 Jul 2025 14:22:34 +0300 Subject: [PATCH 33/64] Update BigMedia.ts --- ags/widget/center-window/BigMedia.ts | 83 ++++++++++++++-------------- 1 file changed, 42 insertions(+), 41 deletions(-) diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts index 9bade179..d5e4ed87 100644 --- a/ags/widget/center-window/BigMedia.ts +++ b/ags/widget/center-window/BigMedia.ts @@ -1,7 +1,7 @@ import { AstalIO, bind, Binding, exec, timeout } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import AstalMpris from "gi://AstalMpris"; -import { Players } from "../../scripts/player"; +import { AstalPlayers } from "../../scripts/player"; export function BigMedia(): Gtk.Widget { let dragTimer: (AstalIO.Time|undefined); @@ -11,10 +11,10 @@ export function BigMedia(): Gtk.Widget { orientation: Gtk.Orientation.VERTICAL, homogeneous: false, width_request: 250, - visible: bind(AstalMpris.get_default(), "players").as((players: Array) => - players[0] ? true : false), - children: bind(AstalMpris.get_default(), "players").as((players: Array) => - players[0] && [ + visible: bind(AstalPlayers.getDefault(), "activePlayer").as((player: AstalMpris.Player) => + player ? true : false), + children: bind(AstalPlayers.getDefault(), "activePlayer").as((player: AstalMpris.Player) => + player && [ new Widget.Box({ halign: Gtk.Align.CENTER, child: new Widget.Box({ @@ -22,8 +22,8 @@ export function BigMedia(): Gtk.Widget { hexpand: false, orientation: Gtk.Orientation.VERTICAL, marginTop: 6, - visible: Players.getDefault().getAlbumArt(players[0]).as(Boolean), - css: Players.getDefault().getAlbumArt(players[0]).as((artUrl: string|undefined) => + visible: AstalPlayers.getDefault().getAlbumArt(player).as(Boolean), + css: AstalPlayers.getDefault().getAlbumArt(player).as((artUrl: string|undefined) => artUrl ? `.image { background-image: url('${artUrl}'); }` : undefined), width_request: 132, height_request: 128 @@ -37,15 +37,15 @@ export function BigMedia(): Gtk.Widget { children: [ new Widget.Label({ className: "title", - tooltipText: bind(players[0], "title").as((title: string) => !title ? "No Title" : title), - label: bind(players[0], "title").as((title: string) => !title ? "No Title" : title), + tooltipText: bind(player, "title").as((title: string) => !title ? "No Title" : title), + label: bind(player, "title").as((title: string) => !title ? "No Title" : title), truncate: true, maxWidthChars: 25, } as Widget.LabelProps), new Widget.Label({ className: "artist", - tooltipText: bind(players[0], "artist").as((artist: string) => !artist ? (players[0].get_identity() ?? "No Artist") : artist), - label: bind(players[0], "artist").as((artist: string) => !artist ? (players[0].get_identity() ?? "No Artist") : artist), + tooltipText: bind(player, "artist").as((artist: string) => !artist ? (player.get_identity() ?? "No Artist") : artist), + label: bind(player, "artist").as((artist: string) => !artist ? (player.get_identity() ?? "No Artist") : artist), maxWidthChars: 28, truncate: true, } as Widget.LabelProps) @@ -54,24 +54,24 @@ export function BigMedia(): Gtk.Widget { new Widget.Box({ className: "progress", hexpand: true, - visible: bind(players[0], "canSeek"), + visible: bind(player, "canSeek"), children: [ new Widget.Slider({ min: 0, hexpand: true, - max: bind(players[0], "length").as((length: number) => - (length > 129600000) ? Math.floor(players[0].get_position()) + max: bind(player, "length").as((length: number) => + (length > 129600000) ? Math.floor(player.get_position()) : Math.floor(length)), // for streams and players whitch dont have a length - value: bind(players[0], "position").as((position: number) => + value: bind(player, "position").as((position: number) => Math.floor(position)), onDragged: (slider: Widget.Slider) => { if(dragTimer === undefined) dragTimer = timeout(600, () => - players[0].set_position(Math.round(slider.value))); + player.set_position(Math.round(slider.value))); else { dragTimer.cancel(); dragTimer = timeout(600, () => - players[0].set_position(Math.round(slider.value))); + player.set_position(Math.round(slider.value))); } } }) @@ -86,11 +86,11 @@ export function BigMedia(): Gtk.Widget { className: "elapsed", valign: Gtk.Align.START, halign: Gtk.Align.START, - label: bind(players[0], "position").as((pos: number) => { + label: bind(player, "position").as((pos: number) => { const sec: number = Math.floor(pos % 60); const min = Math.floor((pos % 3600) / 60); const hours: number = Math.floor(pos / 3600); - return pos > 0 && players[0].length > 0 ? + return pos > 0 && player.length > 0 ? `${hours > 0 ? `${hours}:` : ''}${min < 10 && hours > 0 ? `0${min}` : `${min}`}:${sec < 10 ? `0${sec}` : `${sec}`}` : `0:00`; }) @@ -104,29 +104,29 @@ export function BigMedia(): Gtk.Widget { icon: "edit-paste-symbolic" } as Widget.IconProps), tooltipText: "Copy link to Clipboard", - visible: bind(players[0], "metadata").as(Boolean), + visible: bind(player, "metadata").as(Boolean), onClick: async () => { const link = exec(`playerctl --player=${ - players[0].busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") + player.busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") } metadata xesam:url`); link && Clipboard.getDefault().copyAsync(link); } } as Widget.ButtonProps), new Widget.Button({ className: "shuffle", - visible: bind(players[0], "shuffleStatus").as((shuffleStatus) => + visible: bind(player, "shuffleStatus").as((shuffleStatus) => shuffleStatus !== AstalMpris.Shuffle.UNSUPPORTED), image: new Widget.Icon({ - icon: bind(players[0], "shuffleStatus").as((shuffleStatus) => + icon: bind(player, "shuffleStatus").as((shuffleStatus) => shuffleStatus === AstalMpris.Shuffle.ON ? "media-playlist-shuffle-symbolic" : "media-playlist-consecutive-symbolic") } as Widget.IconProps), - tooltipText: bind(players[0], "shuffleStatus").as((shuffleStatus) => + tooltipText: bind(player, "shuffleStatus").as((shuffleStatus) => shuffleStatus === AstalMpris.Shuffle.ON ? "Shuffle" : "No shuffle"), - onClick: () => players[0].shuffle() + onClick: () => player.shuffle() } as Widget.ButtonProps), new Widget.Button({ className: "previous", @@ -134,21 +134,21 @@ export function BigMedia(): Gtk.Widget { icon: "media-skip-backward-symbolic" } as Widget.IconProps), tooltipText: "Previous", - onClick: () => players[0].canGoPrevious && players[0].previous() + onClick: () => player.canGoPrevious && player.previous() } as Widget.ButtonProps), new Widget.Button({ className: "pause", - tooltipText: bind(players[0], "playback_status").as((status) => + tooltipText: bind(player, "playback_status").as((status) => status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play"), image: new Widget.Icon({ - icon: bind(players[0], "playbackStatus").as((status) => + icon: bind(player, "playbackStatus").as((status) => status === AstalMpris.PlaybackStatus.PLAYING ? "media-playback-pause-symbolic" : "media-playback-start-symbolic"), } as Widget.IconProps), - onClick: () => players[0].playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? - players[0].play() - : players[0].pause() + onClick: () => player.playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? + player.play() + : player.pause() } as Widget.ButtonProps), new Widget.Button({ className: "next", @@ -156,14 +156,14 @@ export function BigMedia(): Gtk.Widget { icon: "media-skip-forward-symbolic" } as Widget.IconProps), tooltipText: "Next", - onClick: () => players[0].canGoNext && players[0].next() + onClick: () => player.canGoNext && player.next() } as Widget.ButtonProps), new Widget.Button({ className: "repeat", - visible: bind(players[0], "loopStatus").as((loopStatus) => + visible: bind(player, "loopStatus").as((loopStatus) => loopStatus !== AstalMpris.Loop.UNSUPPORTED), image: new Widget.Icon({ - icon: bind(players[0], "loopStatus").as((loopStatus) => { + icon: bind(player, "loopStatus").as((loopStatus) => { switch(loopStatus) { case AstalMpris.Loop.TRACK: return "media-playlist-repeat-song-symbolic"; @@ -175,7 +175,7 @@ export function BigMedia(): Gtk.Widget { return "loop-arrow-symbolic"; }) } as Widget.IconProps), - tooltipText: bind(players[0], "loopStatus").as((loopStatus) => { + tooltipText: bind(player, "loopStatus").as((loopStatus) => { switch(loopStatus) { case AstalMpris.Loop.TRACK: return "Loop song"; @@ -186,7 +186,7 @@ export function BigMedia(): Gtk.Widget { return "No loop"; }), - onClick: () => players[0].loop() + onClick: () => player.loop() } as Widget.ButtonProps) ] } as Widget.BoxProps), @@ -194,16 +194,17 @@ export function BigMedia(): Gtk.Widget { className: "length", valign: Gtk.Align.START, halign: Gtk.Align.END, - label: bind(players[0], "length").as((len/* bananananananana */: number) => { + label: bind(player, "length").as((len/* bananananananana */: number) => { const maxLen: number = 9223372036854; const sec: number = Math.floor(len % 60); const min = Math.floor((len % 3600) / 60); - const hour: number = Math.floor(len / 3600); + const hours: number = Math.floor(len / 3600); - return (len > 0 && hour < maxLen / 10000000) ? - `${hour > 0 ? `${hour}:` : ''}${min < 10 && hour > 0 ? `0${min}` : `${min}`}:${sec < 10 ? `0${sec}` : `${sec}`}` - : (len <= 0 ? `0:00` : "Live"); + //console.log("Len:", len, "\nLen in hours:", hours); + return (len > 0 && hours < maxLen / 10000000) ? // && Number.isFinite(len) <-- this shit doesn't work! + `${hours > 0 ? `${hours}:` : ''}${min < 10 && hours > 0 ? `0${min}` : `${min}`}:${sec < 10 ? `0${sec}` : `${sec}`}` + : ( len <= 0 ? `0:00` : "Live"); }) } as Widget.LabelProps) }) From 96b30ab27c1103bec64ed7ca406cb921e5753b53 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sun, 6 Jul 2025 14:51:13 +0300 Subject: [PATCH 34/64] Update BigMedia.ts --- ags/widget/center-window/BigMedia.ts | 72 ++++++++++++++-------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts index d5e4ed87..52348e87 100644 --- a/ags/widget/center-window/BigMedia.ts +++ b/ags/widget/center-window/BigMedia.ts @@ -11,10 +11,10 @@ export function BigMedia(): Gtk.Widget { orientation: Gtk.Orientation.VERTICAL, homogeneous: false, width_request: 250, - visible: bind(AstalPlayers.getDefault(), "activePlayer").as((player: AstalMpris.Player) => - player ? true : false), - children: bind(AstalPlayers.getDefault(), "activePlayer").as((player: AstalMpris.Player) => - player && [ + visible: bind(AstalPlayers.getDefault(), "activePlayer").as((actviePlayer: AstalMpris.Player) => + actviePlayer ? true : false), + children: bind(AstalPlayers.getDefault(), "activePlayer").as((actviePlayer: AstalMpris.Player) => + actviePlayer && [ new Widget.Box({ halign: Gtk.Align.CENTER, child: new Widget.Box({ @@ -22,8 +22,8 @@ export function BigMedia(): Gtk.Widget { hexpand: false, orientation: Gtk.Orientation.VERTICAL, marginTop: 6, - visible: AstalPlayers.getDefault().getAlbumArt(player).as(Boolean), - css: AstalPlayers.getDefault().getAlbumArt(player).as((artUrl: string|undefined) => + visible: AstalPlayers.getDefault().getAlbumArt(actviePlayer).as(Boolean), + css: AstalPlayers.getDefault().getAlbumArt(actviePlayer).as((artUrl: string|undefined) => artUrl ? `.image { background-image: url('${artUrl}'); }` : undefined), width_request: 132, height_request: 128 @@ -37,15 +37,15 @@ export function BigMedia(): Gtk.Widget { children: [ new Widget.Label({ className: "title", - tooltipText: bind(player, "title").as((title: string) => !title ? "No Title" : title), - label: bind(player, "title").as((title: string) => !title ? "No Title" : title), + tooltipText: bind(actviePlayer, "title").as((title: string) => !title ? "No Title" : title), + label: bind(actviePlayer, "title").as((title: string) => !title ? "No Title" : title), truncate: true, maxWidthChars: 25, } as Widget.LabelProps), new Widget.Label({ className: "artist", - tooltipText: bind(player, "artist").as((artist: string) => !artist ? (player.get_identity() ?? "No Artist") : artist), - label: bind(player, "artist").as((artist: string) => !artist ? (player.get_identity() ?? "No Artist") : artist), + tooltipText: bind(actviePlayer, "artist").as((artist: string) => !artist ? (actviePlayer.get_identity() ?? "No Artist") : artist), + label: bind(actviePlayer, "artist").as((artist: string) => !artist ? (actviePlayer.get_identity() ?? "No Artist") : artist), maxWidthChars: 28, truncate: true, } as Widget.LabelProps) @@ -54,24 +54,24 @@ export function BigMedia(): Gtk.Widget { new Widget.Box({ className: "progress", hexpand: true, - visible: bind(player, "canSeek"), + visible: bind(actviePlayer, "canSeek"), children: [ new Widget.Slider({ min: 0, hexpand: true, - max: bind(player, "length").as((length: number) => - (length > 129600000) ? Math.floor(player.get_position()) + max: bind(actviePlayer, "length").as((length: number) => + (length > 129600000) ? Math.floor(actviePlayer.get_position()) : Math.floor(length)), // for streams and players whitch dont have a length - value: bind(player, "position").as((position: number) => + value: bind(actviePlayer, "position").as((position: number) => Math.floor(position)), onDragged: (slider: Widget.Slider) => { if(dragTimer === undefined) dragTimer = timeout(600, () => - player.set_position(Math.round(slider.value))); + actviePlayer.set_position(Math.round(slider.value))); else { dragTimer.cancel(); dragTimer = timeout(600, () => - player.set_position(Math.round(slider.value))); + actviePlayer.set_position(Math.round(slider.value))); } } }) @@ -86,11 +86,11 @@ export function BigMedia(): Gtk.Widget { className: "elapsed", valign: Gtk.Align.START, halign: Gtk.Align.START, - label: bind(player, "position").as((pos: number) => { + label: bind(actviePlayer, "position").as((pos: number) => { const sec: number = Math.floor(pos % 60); const min = Math.floor((pos % 3600) / 60); const hours: number = Math.floor(pos / 3600); - return pos > 0 && player.length > 0 ? + return pos > 0 && actviePlayer.length > 0 ? `${hours > 0 ? `${hours}:` : ''}${min < 10 && hours > 0 ? `0${min}` : `${min}`}:${sec < 10 ? `0${sec}` : `${sec}`}` : `0:00`; }) @@ -104,29 +104,29 @@ export function BigMedia(): Gtk.Widget { icon: "edit-paste-symbolic" } as Widget.IconProps), tooltipText: "Copy link to Clipboard", - visible: bind(player, "metadata").as(Boolean), + visible: bind(actviePlayer, "metadata").as(Boolean), onClick: async () => { const link = exec(`playerctl --player=${ - player.busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") + actviePlayer.busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") } metadata xesam:url`); link && Clipboard.getDefault().copyAsync(link); } } as Widget.ButtonProps), new Widget.Button({ className: "shuffle", - visible: bind(player, "shuffleStatus").as((shuffleStatus) => + visible: bind(actviePlayer, "shuffleStatus").as((shuffleStatus) => shuffleStatus !== AstalMpris.Shuffle.UNSUPPORTED), image: new Widget.Icon({ - icon: bind(player, "shuffleStatus").as((shuffleStatus) => + icon: bind(actviePlayer, "shuffleStatus").as((shuffleStatus) => shuffleStatus === AstalMpris.Shuffle.ON ? "media-playlist-shuffle-symbolic" : "media-playlist-consecutive-symbolic") } as Widget.IconProps), - tooltipText: bind(player, "shuffleStatus").as((shuffleStatus) => + tooltipText: bind(actviePlayer, "shuffleStatus").as((shuffleStatus) => shuffleStatus === AstalMpris.Shuffle.ON ? "Shuffle" : "No shuffle"), - onClick: () => player.shuffle() + onClick: () => actviePlayer.shuffle() } as Widget.ButtonProps), new Widget.Button({ className: "previous", @@ -134,21 +134,21 @@ export function BigMedia(): Gtk.Widget { icon: "media-skip-backward-symbolic" } as Widget.IconProps), tooltipText: "Previous", - onClick: () => player.canGoPrevious && player.previous() + onClick: () => actviePlayer.canGoPrevious && actviePlayer.previous() } as Widget.ButtonProps), new Widget.Button({ className: "pause", - tooltipText: bind(player, "playback_status").as((status) => + tooltipText: bind(actviePlayer, "playback_status").as((status) => status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play"), image: new Widget.Icon({ - icon: bind(player, "playbackStatus").as((status) => + icon: bind(actviePlayer, "playbackStatus").as((status) => status === AstalMpris.PlaybackStatus.PLAYING ? "media-playback-pause-symbolic" : "media-playback-start-symbolic"), } as Widget.IconProps), - onClick: () => player.playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? - player.play() - : player.pause() + onClick: () => actviePlayer.playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? + actviePlayer.play() + : actviePlayer.pause() } as Widget.ButtonProps), new Widget.Button({ className: "next", @@ -156,14 +156,14 @@ export function BigMedia(): Gtk.Widget { icon: "media-skip-forward-symbolic" } as Widget.IconProps), tooltipText: "Next", - onClick: () => player.canGoNext && player.next() + onClick: () => actviePlayer.canGoNext && actviePlayer.next() } as Widget.ButtonProps), new Widget.Button({ className: "repeat", - visible: bind(player, "loopStatus").as((loopStatus) => + visible: bind(actviePlayer, "loopStatus").as((loopStatus) => loopStatus !== AstalMpris.Loop.UNSUPPORTED), image: new Widget.Icon({ - icon: bind(player, "loopStatus").as((loopStatus) => { + icon: bind(actviePlayer, "loopStatus").as((loopStatus) => { switch(loopStatus) { case AstalMpris.Loop.TRACK: return "media-playlist-repeat-song-symbolic"; @@ -175,7 +175,7 @@ export function BigMedia(): Gtk.Widget { return "loop-arrow-symbolic"; }) } as Widget.IconProps), - tooltipText: bind(player, "loopStatus").as((loopStatus) => { + tooltipText: bind(actviePlayer, "loopStatus").as((loopStatus) => { switch(loopStatus) { case AstalMpris.Loop.TRACK: return "Loop song"; @@ -186,7 +186,7 @@ export function BigMedia(): Gtk.Widget { return "No loop"; }), - onClick: () => player.loop() + onClick: () => actviePlayer.loop() } as Widget.ButtonProps) ] } as Widget.BoxProps), @@ -194,7 +194,7 @@ export function BigMedia(): Gtk.Widget { className: "length", valign: Gtk.Align.START, halign: Gtk.Align.END, - label: bind(player, "length").as((len/* bananananananana */: number) => { + label: bind(actviePlayer, "length").as((len/* bananananananana */: number) => { const maxLen: number = 9223372036854; const sec: number = Math.floor(len % 60); From f8fe9c85609401e95ddc4323af1fb1129ce9ba8a Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sun, 13 Jul 2025 16:10:50 +0300 Subject: [PATCH 35/64] Using coverArt() instead of artUrl() Accessing memory will be faster than accessing through a url --- ags/widget/center-window/BigMedia.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts index 52348e87..7e7713ef 100644 --- a/ags/widget/center-window/BigMedia.ts +++ b/ags/widget/center-window/BigMedia.ts @@ -22,8 +22,8 @@ export function BigMedia(): Gtk.Widget { hexpand: false, orientation: Gtk.Orientation.VERTICAL, marginTop: 6, - visible: AstalPlayers.getDefault().getAlbumArt(actviePlayer).as(Boolean), - css: AstalPlayers.getDefault().getAlbumArt(actviePlayer).as((artUrl: string|undefined) => + visible: bind(actviePlayer, "coverArt").as(Boolean), + css: bind(actviePlayer, "coverArt").as((artUrl: string|undefined) => artUrl ? `.image { background-image: url('${artUrl}'); }` : undefined), width_request: 132, height_request: 128 From 5131e82ddfe028311946f2f1db85b24160f12daf Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:31:13 +0300 Subject: [PATCH 36/64] Update app.ts Slightly removed the duplicate elements in the variable --- ags/app.ts | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/ags/app.ts b/ags/app.ts index 72717bda..58d3e0e8 100644 --- a/ags/app.ts +++ b/ags/app.ts @@ -1,5 +1,6 @@ import AstalNotifd from "gi://AstalNotifd"; import AstalHyprland from "gi://AstalHyprland"; +import AstalMpris from "gi://AstalMpris"; import { App, Astal } from "astal/gtk3" import { Wireplumber } from "./scripts/volume"; @@ -23,10 +24,11 @@ import { Stylesheet } from "./scripts/stylesheet"; import { Clipboard } from "./scripts/clipboard"; import { PluginClipboard } from "./runner/plugins/clipboard"; import { Config } from "./scripts/config"; +import { AstalPlayers } from "./scripts/player"; - -import { Players } from "./scripts/player"; - +const hyprland = AstalHyprland.get_default(); +const audio = Wireplumber.getDefault(); +const player = AstalPlayers.getDefault(); let osdTimer: (Time|undefined); let connections = new Map | number)>(); @@ -66,24 +68,37 @@ App.start({ // Init clipboard module Clipboard.getDefault(); - connections.set(AstalHyprland.get_default(), [ - AstalHyprland.get_default().connect("keyboard-layout", (_, Keyboard, layout) => { + //OSD Layout + connections.set(hyprland, [ + hyprland.connect("keyboard-layout", (_, Keyboard, layout) => { variableHandler(OSDModes.LAYOUT, layout); triggerOSD(OSDModes.LAYOUT); }) ]); - - connections.set(Wireplumber.getDefault(), [ - Wireplumber.getDefault().getDefaultSink().connect("notify::volume", () => - triggerOSD(OSDModes.SINK)), - Wireplumber.getDefault().getDefaultSink().connect("notify::mute", () => - triggerOSD(OSDModes.SINK)), - Wireplumber.getDefault().getDefaultSource().connect("notify::volume", () => - triggerOSD(OSDModes.SOURCE)), - Wireplumber.getDefault().getDefaultSource().connect("notify::mute", () => - triggerOSD(OSDModes.SOURCE)), + + const audioHandler = () => triggerOSD(OSDModes.SINK); + const sinkHandler = () => triggerOSD(OSDModes.SOURCE); + //OSD Wireplumber + connections.set(audio, [ + audio.getDefaultSink().connect("notify::volume", audioHandler), + audio.getDefaultSink().connect("notify::mute", audioHandler), + audio.getDefaultSource().connect("notify::volume", sinkHandler), + audio.getDefaultSource().connect("notify::mute", sinkHandler), ]); + //OSD Player + /*connections.set(player, [ + AstalPlayers.getDefault().activePlayer.connect("notify::title", () => + triggerOSD(OSDModes.PLAYER) + ), + player.activePlayer.connect("notify::playback-status", (s) => { + if (s.playbackStatus === AstalMpris.PlaybackStatus.PLAYING && + hyprland.get_focused_client().get_fullscreen() === AstalHyprland.Fullscreen.FULLSCREEN && + hyprland.get_focused_client().get_class().toLowerCase() !== player.activePlayer.get_enrty().toLowerCase()) + triggerOSD(OSDModes.PLAYER); + }) + ])*/ + connections.set(Notifications.getDefault(), [ Notifications.getDefault().connect("notification-added", (_, _notif: AstalNotifd.Notification) => { Windows.open("floating-notifications"); From d321312728e3929151dd3d8fa6e30e0fa3db554f Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:32:17 +0300 Subject: [PATCH 37/64] Update CenterWindow.ts Just a fix for what I wanted to do, but I kept forgetting :) --- ags/window/CenterWindow.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ags/window/CenterWindow.ts b/ags/window/CenterWindow.ts index ccccc96b..b0e3d353 100644 --- a/ags/window/CenterWindow.ts +++ b/ags/window/CenterWindow.ts @@ -5,7 +5,7 @@ import { getDateTime } from "../scripts/time"; import { Separator, SeparatorProps } from "../widget/Separator"; import { PopupWindow, PopupWindowProps } from "../widget/PopupWindow"; import { BigMedia } from "../widget/center-window/BigMedia"; -import AstalMpris from "gi://AstalMpris"; +import { AstalPlayers } from "../scripts/player"; export const CenterWindow = (mon: number) => PopupWindow({ namespace: "center-window", @@ -60,7 +60,7 @@ export const CenterWindow = (mon: number) => PopupWindow({ margin: 5, spacing: 8, alpha: .3, - visible: bind(AstalMpris.get_default(), "players").as(players => players.length > 0), + visible: bind(AstalPlayers.getDefault(), "activePlayer").as(players => players ? true : false), } as SeparatorProps), BigMedia() ] From 04da046c7bbee827f4c643cd33d9fec058a3cbb2 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Wed, 16 Jul 2025 15:36:05 +0300 Subject: [PATCH 38/64] Update player.ts Now it monitors the changes correctly --- ags/scripts/player.ts | 59 ++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/ags/scripts/player.ts b/ags/scripts/player.ts index 2b3b8baf..718976a3 100644 --- a/ags/scripts/player.ts +++ b/ags/scripts/player.ts @@ -1,4 +1,4 @@ -import { GObject, register, property, signal, Binding, bind } from "astal"; +import { GObject, register, property } from "astal"; import AstalMpris from "gi://AstalMpris"; export { AstalPlayers }; @@ -10,7 +10,7 @@ class AstalPlayers extends GObject.Object { #players: AstalMpris.Player[] = []; #activePlayer: AstalMpris.Player | null = null; - #playerConnections: Map = new Map(); + #playerConnections: Map = new Map(); @property(AstalMpris.Player) get activePlayer() { @@ -38,26 +38,39 @@ class AstalPlayers extends GObject.Object { } private _addPlayerSignals(player: AstalMpris.Player) { - const handlerId = player.connect("notify::playback-status", () => { - this._updateActivePlayer(); - }); - this.#playerConnections.set(player, handlerId); + const handler = () => this._onPlayerStateChanged(player); + + const ids = [ + player.connect("notify::playback-status", handler), + player.connect("notify::metadata", handler) + ]; + + this.#playerConnections.set(player, ids); } private _removePlayer(player: AstalMpris.Player) { this.#players = this.#players.filter(p => p !== player); if (this.#playerConnections.has(player)) { - player.disconnect(this.#playerConnections.get(player)!); + const ids = this.#playerConnections.get(player)!; + ids.forEach(id => player.disconnect(id)); this.#playerConnections.delete(player); } this._updateActivePlayer(); } - private _updateActivePlayer() { - const playingPlayer = this.#players.find(p => p.playback_status === AstalMpris.PlaybackStatus.PLAYING); + private _onPlayerStateChanged(player: AstalMpris.Player) { + const wasActivePlayer = this.#activePlayer; + this._updateActivePlayer(); + + if (this.#activePlayer === wasActivePlayer && this.#activePlayer === player) { + this.notify("active-player"); + } + } + private _updateActivePlayer() { + const playingPlayer = this.#players.find(p => p.playbackStatus === AstalMpris.PlaybackStatus.PLAYING); let newActivePlayer; if (playingPlayer) { @@ -75,29 +88,17 @@ class AstalPlayers extends GObject.Object { } public static getDefault(): AstalPlayers { - if (!AstalPlayers.inst) + if (!AstalPlayers.inst) { AstalPlayers.inst = new AstalPlayers(); - + } return AstalPlayers.inst; } + + public connect(signal: string, callback: (...args: any[]) => void): number { + return super.connect(signal, callback); + } - /** - * This function handles album art/cover of playing media. If a file is provided - * by the player, it adds the "file://" uri as a prefix, so you can use it in css. - * - * @param player the player you want to pull album art from - * @returns Binding to player.artUrl containing the album art uri, or an undefined binding if none was found. - */ - public getAlbumArt(player: AstalMpris.Player): Binding { - return bind(player, "artUrl").as((artUrl: string) => { - - if (!artUrl) - return undefined; - - if (artUrl.startsWith("/")) - return "file://" + artUrl; - - return artUrl; - }); + public disconnect(id: number): void { + super.disconnect(id); } } From 1b7bf0c8194d064c72a50e9faed0085ce3498816 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Mon, 21 Jul 2025 00:53:19 +0300 Subject: [PATCH 39/64] Fixed this#[emitter] is null when LastClient in a state of abstraction --- ags/widget/bar/Workspaces.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ags/widget/bar/Workspaces.ts b/ags/widget/bar/Workspaces.ts index 217ed60d..6341dcbd 100644 --- a/ags/widget/bar/Workspaces.ts +++ b/ags/widget/bar/Workspaces.ts @@ -45,9 +45,9 @@ export function Workspaces(): Gtk.Widget { bind(AstalHyprland.get_default(), "focusedWorkspace") ], (lastClient, focusedWorkspace) => focusedWorkspace?.id === workspace.id ? false : Boolean(lastClient))(), - icon: bind(lastClient, "initialClass").as((initialClass) => + icon: lastClient ? bind(lastClient, "initialClass").as((initialClass) => getSymbolicIcon(initialClass) ?? getAppIcon(initialClass) ?? - "application-x-executable-symbolic") + "application-x-executable-symbolic") : undefined } as Widget.IconProps) ) } as Widget.BoxProps), From 8ae87d6d8b3a3d4a2249a5225c92d578dae8dd9f Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 22 Jul 2025 17:43:25 +0300 Subject: [PATCH 40/64] Update BigMedia.ts Some improvements and fixes --- ags/widget/center-window/BigMedia.ts | 41 ++++++++-------------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts index 7e7713ef..0d36da9a 100644 --- a/ags/widget/center-window/BigMedia.ts +++ b/ags/widget/center-window/BigMedia.ts @@ -1,11 +1,11 @@ -import { AstalIO, bind, Binding, exec, timeout } from "astal"; +import { AstalIO, bind, Binding, exec, timeout, GLib } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import AstalMpris from "gi://AstalMpris"; import { AstalPlayers } from "../../scripts/player"; +import { Stylesheet } from "../../scripts/stylesheet"; +import { progressBar } from "./Slider"; export function BigMedia(): Gtk.Widget { - let dragTimer: (AstalIO.Time|undefined); - return new Widget.Box({ className: "big-media", orientation: Gtk.Orientation.VERTICAL, @@ -56,25 +56,7 @@ export function BigMedia(): Gtk.Widget { hexpand: true, visible: bind(actviePlayer, "canSeek"), children: [ - new Widget.Slider({ - min: 0, - hexpand: true, - max: bind(actviePlayer, "length").as((length: number) => - (length > 129600000) ? Math.floor(actviePlayer.get_position()) - : Math.floor(length)), // for streams and players whitch dont have a length - value: bind(actviePlayer, "position").as((position: number) => - Math.floor(position)), - onDragged: (slider: Widget.Slider) => { - if(dragTimer === undefined) - dragTimer = timeout(600, () => - actviePlayer.set_position(Math.round(slider.value))); - else { - dragTimer.cancel(); - dragTimer = timeout(600, () => - actviePlayer.set_position(Math.round(slider.value))); - } - } - }) + progressBar(actviePlayer) ] }), new Widget.CenterBox({ @@ -105,7 +87,7 @@ export function BigMedia(): Gtk.Widget { } as Widget.IconProps), tooltipText: "Copy link to Clipboard", visible: bind(actviePlayer, "metadata").as(Boolean), - onClick: async () => { + onClickRelease: async () => { const link = exec(`playerctl --player=${ actviePlayer.busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") } metadata xesam:url`); @@ -126,7 +108,7 @@ export function BigMedia(): Gtk.Widget { shuffleStatus === AstalMpris.Shuffle.ON ? "Shuffle" : "No shuffle"), - onClick: () => actviePlayer.shuffle() + onClickRelease: () => actviePlayer.shuffle() } as Widget.ButtonProps), new Widget.Button({ className: "previous", @@ -134,7 +116,7 @@ export function BigMedia(): Gtk.Widget { icon: "media-skip-backward-symbolic" } as Widget.IconProps), tooltipText: "Previous", - onClick: () => actviePlayer.canGoPrevious && actviePlayer.previous() + onClickRelease: () => actviePlayer.canGoPrevious && actviePlayer.previous() } as Widget.ButtonProps), new Widget.Button({ className: "pause", @@ -146,7 +128,7 @@ export function BigMedia(): Gtk.Widget { "media-playback-pause-symbolic" : "media-playback-start-symbolic"), } as Widget.IconProps), - onClick: () => actviePlayer.playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? + onClickRelease: () => actviePlayer.playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? actviePlayer.play() : actviePlayer.pause() } as Widget.ButtonProps), @@ -156,7 +138,7 @@ export function BigMedia(): Gtk.Widget { icon: "media-skip-forward-symbolic" } as Widget.IconProps), tooltipText: "Next", - onClick: () => actviePlayer.canGoNext && actviePlayer.next() + onClickRelease: () => actviePlayer.canGoNext && actviePlayer.next() } as Widget.ButtonProps), new Widget.Button({ className: "repeat", @@ -186,7 +168,7 @@ export function BigMedia(): Gtk.Widget { return "No loop"; }), - onClick: () => actviePlayer.loop() + onClickRelease: () => actviePlayer.loop() } as Widget.ButtonProps) ] } as Widget.BoxProps), @@ -195,14 +177,13 @@ export function BigMedia(): Gtk.Widget { valign: Gtk.Align.START, halign: Gtk.Align.END, label: bind(actviePlayer, "length").as((len/* bananananananana */: number) => { - const maxLen: number = 9223372036854; const sec: number = Math.floor(len % 60); const min = Math.floor((len % 3600) / 60); const hours: number = Math.floor(len / 3600); //console.log("Len:", len, "\nLen in hours:", hours); - return (len > 0 && hours < maxLen / 10000000) ? // && Number.isFinite(len) <-- this shit doesn't work! + return (len > 0 && len < GLib.MAXINT64 / 10000000) ? `${hours > 0 ? `${hours}:` : ''}${min < 10 && hours > 0 ? `0${min}` : `${min}`}:${sec < 10 ? `0${sec}` : `${sec}`}` : ( len <= 0 ? `0:00` : "Live"); }) From c1807e442ad85bf8f5d7c9a7e574902cd6380dae Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 22 Jul 2025 17:44:04 +0300 Subject: [PATCH 41/64] Add Slider for BigMedia --- ags/widget/center-window/Slider.ts | 156 +++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 ags/widget/center-window/Slider.ts diff --git a/ags/widget/center-window/Slider.ts b/ags/widget/center-window/Slider.ts new file mode 100644 index 00000000..301d1bf5 --- /dev/null +++ b/ags/widget/center-window/Slider.ts @@ -0,0 +1,156 @@ +import { bind, timeout, Time, GLib } from 'astal'; +import { Gtk, Gdk, Widget } from 'astal/gtk3'; +import { AstalPlayers } from '../../scripts/player'; +import AstalMpris from "gi://AstalMpris"; +import { Wallpaper } from '../../scripts/wallpaper'; + +let dragTimer: (Time|null) = null; + +let isDragging = false; +let dragProgress: number | null = null; + +let pulseAnimationId: number | null = null; +let pulseValue = 0; + +function drawRoundedRectangle(cr, x, y, width, height, radius) { + radius = Math.min(radius, height / 2, width / 2); + + cr.moveTo(x + radius, y); + cr.arc(x + width - radius, y + radius, radius, 1.5 * Math.PI, 2 * Math.PI); // Top right + cr.arc(x + width - radius, y + height - radius, radius, 0, 0.5 * Math.PI); // Bottom right + cr.arc(x + radius, y + height - radius, radius, 0.5 * Math.PI, Math.PI); // Bottom left + cr.arc(x + radius, y + radius, radius, Math.PI, 1.5 * Math.PI); // Top left + cr.closePath(); +} + +export function progressBar(player: AstalMpris.Player): Gtk.Widget { + return new Widget.DrawingArea({ + className: "progress-drawing-area", + hexpand: true, + heightRequest: 30, + + setup: (self) => { + self.add_events( + Gdk.EventMask.BUTTON_PRESS_MASK | + Gdk.EventMask.BUTTON_RELEASE_MASK | + Gdk.EventMask.POINTER_MOTION_MASK + ); + + const updateDragPosition = (x: number) => { + const width = self.get_allocated_width(); + const length = player.get_length() + console.log('Width', width, 'x', x, 'Len', length); + if (width === 0) return; + + const newProgress = Math.max(0, Math.min(x / width, 1)); + + console.log('New Progress', newProgress); + + if (dragProgress !== newProgress) { + dragProgress = newProgress; + //self.queue_draw(); + } + }; + + self.connect('button-press-event', (_, event) => { + if (player.length > GLib.MAXINT64 / 10000000) return; + const [success, x] = event.get_coords(); + + isDragging = true; + updateDragPosition(x); + }); + + self.connect('motion-notify-event', (_, event) => { + if (isDragging) { + const [, x] = event.get_coords(); + updateDragPosition(x); + } + }); + + self.connect('button-release-event', () => { + if (isDragging && dragProgress !== null && player.get_length() > 0) { + const finalPosition = dragProgress * player.get_length(); + player.set_position(finalPosition) + timeout(70, () => isDragging = false); + console.log('Final Position', finalPosition, '\nLen', player.get_length(), '\nPosition', player.get_position()); + }; + }); + + self.connect('draw', (self) => { + if (pulseAnimationId !== null) return; + + if (player.playbackStatus === AstalMpris.PlaybackStatus.PLAYING) { + pulseAnimationId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, Math.round(1000 / Wallpaper.getDefault().getRefreshRate()), () => { + pulseValue = Math.sin((Date.now() / 1000) * Math.PI); + self.queue_draw(); + return GLib.SOURCE_CONTINUE; + } + ); + } + }); + + // self.connect('draw', bind(self, (self, cr) => { + // console.log("Draw", self, typeof self); + // })) + + self.connect("destroy", () => { + if (pulseAnimationId !== null) { + GLib.source_remove(pulseAnimationId); + pulseAnimationId = null; + } + }); + }, + + onDraw: (self, cr) => { + const styleContext = self.get_style_context(); + + const width = self.get_allocated_width(); + const height = self.get_allocated_height(); + const position = player.get_position(); + const length = player.get_length(); + + const playerProgress = length > 0 ? position / length : 0; + const displayProgress = (isDragging && dragProgress !== null) ? dragProgress : + (length > (GLib.MAXINT64 / 10000000) ? length : playerProgress); + + const barHeight = 6; + const baseHandleRadius = 7; + const handlePulseAmount = 1.5; + const animatedHandleRadius = baseHandleRadius + (pulseValue > 0 ? pulseValue * handlePulseAmount : 0); + const centerY = height / 2; + const barRadius = barHeight / 2; + const rgba = new Gdk.RGBA(); + + const bg = styleContext.get_property('background-color', Gtk.StateFlags.NORMAL); + const fg = styleContext.get_property('color', Gtk.StateFlags.NORMAL); + + const handleX = Math.max(animatedHandleRadius, Math.min(width * displayProgress, width - animatedHandleRadius)); + + // Background + rgba.parse('rgba(100, 100, 100, 0.4)'); + cr.setSourceRGBA(rgba.red, rgba.green, rgba.blue, rgba.alpha); + drawRoundedRectangle(cr, 0, centerY - barHeight / 2, width, barHeight, barRadius); + cr.fill(); + + // Progress + // Рисуем прогресс, только если он больше нуля + cr.save(); // Сохраняем состояние контекста рисования + + // Устанавливаем прямоугольную область отсечения по ширине прогресса + cr.rectangle(0, 0, width * displayProgress, height); + cr.clip(); + + cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); + drawRoundedRectangle(cr, 0, centerY - barHeight / 2, width, barHeight, barRadius); + cr.fill(); + + cr.restore(); // Восстанавливаем контекст, убирая маску отсечения + + // Handle + //rgba.parse('#ffffff'); + cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); + cr.arc(handleX, centerY, animatedHandleRadius, 0, 2 * Math.PI); + cr.fill(); + }, + }); +} From 0146c6c31396682d4c4ecb7e0d067db26de0a2ee Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 22 Jul 2025 17:45:06 +0300 Subject: [PATCH 42/64] Update player.ts --- ags/scripts/player.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/ags/scripts/player.ts b/ags/scripts/player.ts index 718976a3..8e1d47ef 100644 --- a/ags/scripts/player.ts +++ b/ags/scripts/player.ts @@ -20,8 +20,8 @@ class AstalPlayers extends GObject.Object { constructor() { super(); - AstalPlayers.astalMpris.connect("player-added", (_, player) => this._addPlayer(player)); - AstalPlayers.astalMpris.connect("player-closed", (_, player) => this._removePlayer(player)); + AstalPlayers.astalMpris.connect('player-added', (_, player) => this._addPlayer(player)); + AstalPlayers.astalMpris.connect('player-closed', (_, player) => this._removePlayer(player)); this.#players = AstalPlayers.astalMpris.get_players(); this.#players.forEach(player => this._addPlayerSignals(player)); @@ -41,8 +41,14 @@ class AstalPlayers extends GObject.Object { const handler = () => this._onPlayerStateChanged(player); const ids = [ - player.connect("notify::playback-status", handler), - player.connect("notify::metadata", handler) + player.connect('notify::playback-status', handler), + player.connect('notify::cover-art', handler), + player.connect('notify::identity', handler), + player.connect('notify::track-id', handler), + // player.connect('notify::title', handler), // Em... This is a stupid idea. + // player.connect('notify::artist', handler), + //player.connect('notify::metadata', handler), // infinity call from Clapper + //player.connect('notify::position', handler) // DO NOT DO THAT! ]; this.#playerConnections.set(player, ids); @@ -65,7 +71,7 @@ class AstalPlayers extends GObject.Object { this._updateActivePlayer(); if (this.#activePlayer === wasActivePlayer && this.#activePlayer === player) { - this.notify("active-player"); + this.notify('active-player'); } } @@ -83,7 +89,7 @@ class AstalPlayers extends GObject.Object { if (this.#activePlayer !== newActivePlayer) { this.#activePlayer = newActivePlayer; - this.notify("active-player"); + this.notify('active-player'); } } @@ -93,7 +99,7 @@ class AstalPlayers extends GObject.Object { } return AstalPlayers.inst; } - + public connect(signal: string, callback: (...args: any[]) => void): number { return super.connect(signal, callback); } From 57130696e858a73196e095aa56095e0f671fde15 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 22 Jul 2025 18:02:13 +0300 Subject: [PATCH 43/64] Delete comments --- ags/widget/center-window/Slider.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ags/widget/center-window/Slider.ts b/ags/widget/center-window/Slider.ts index 301d1bf5..77b35148 100644 --- a/ags/widget/center-window/Slider.ts +++ b/ags/widget/center-window/Slider.ts @@ -133,10 +133,8 @@ export function progressBar(player: AstalMpris.Player): Gtk.Widget { cr.fill(); // Progress - // Рисуем прогресс, только если он больше нуля - cr.save(); // Сохраняем состояние контекста рисования + cr.save(); - // Устанавливаем прямоугольную область отсечения по ширине прогресса cr.rectangle(0, 0, width * displayProgress, height); cr.clip(); @@ -144,7 +142,7 @@ export function progressBar(player: AstalMpris.Player): Gtk.Widget { drawRoundedRectangle(cr, 0, centerY - barHeight / 2, width, barHeight, barRadius); cr.fill(); - cr.restore(); // Восстанавливаем контекст, убирая маску отсечения + cr.restore(); // Handle //rgba.parse('#ffffff'); From d98bba591a9488734b212f5141b88b25d990d88a Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 22 Jul 2025 18:06:46 +0300 Subject: [PATCH 44/64] Update Slider.ts Add comment --- ags/widget/center-window/Slider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ags/widget/center-window/Slider.ts b/ags/widget/center-window/Slider.ts index 77b35148..09623f0f 100644 --- a/ags/widget/center-window/Slider.ts +++ b/ags/widget/center-window/Slider.ts @@ -80,7 +80,7 @@ export function progressBar(player: AstalMpris.Player): Gtk.Widget { if (pulseAnimationId !== null) return; if (player.playbackStatus === AstalMpris.PlaybackStatus.PLAYING) { - pulseAnimationId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, Math.round(1000 / Wallpaper.getDefault().getRefreshRate()), () => { + pulseAnimationId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 16, () => { // 16 is milliseconds per frame (~60fps). If you what get more fps, then use this formula: Math.round(1000 / refreshRate) pulseValue = Math.sin((Date.now() / 1000) * Math.PI); self.queue_draw(); return GLib.SOURCE_CONTINUE; From 230670ebea65d37d8906ff38a1582485259154c4 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Tue, 22 Jul 2025 19:04:09 +0300 Subject: [PATCH 45/64] Cleaning up code --- ags/widget/center-window/Slider.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ags/widget/center-window/Slider.ts b/ags/widget/center-window/Slider.ts index 09623f0f..dfe0f9b4 100644 --- a/ags/widget/center-window/Slider.ts +++ b/ags/widget/center-window/Slider.ts @@ -39,16 +39,12 @@ export function progressBar(player: AstalMpris.Player): Gtk.Widget { const updateDragPosition = (x: number) => { const width = self.get_allocated_width(); const length = player.get_length() - console.log('Width', width, 'x', x, 'Len', length); if (width === 0) return; const newProgress = Math.max(0, Math.min(x / width, 1)); - - console.log('New Progress', newProgress); if (dragProgress !== newProgress) { dragProgress = newProgress; - //self.queue_draw(); } }; @@ -72,7 +68,6 @@ export function progressBar(player: AstalMpris.Player): Gtk.Widget { const finalPosition = dragProgress * player.get_length(); player.set_position(finalPosition) timeout(70, () => isDragging = false); - console.log('Final Position', finalPosition, '\nLen', player.get_length(), '\nPosition', player.get_position()); }; }); From cb5cf8ee4d307ca904a7c3e37e411576a3f1963d Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Wed, 23 Jul 2025 20:17:05 +0300 Subject: [PATCH 46/64] Update Slider.ts --- ags/widget/center-window/Slider.ts | 113 +++++++++++++++-------------- 1 file changed, 57 insertions(+), 56 deletions(-) diff --git a/ags/widget/center-window/Slider.ts b/ags/widget/center-window/Slider.ts index dfe0f9b4..5864272e 100644 --- a/ags/widget/center-window/Slider.ts +++ b/ags/widget/center-window/Slider.ts @@ -1,17 +1,19 @@ -import { bind, timeout, Time, GLib } from 'astal'; +import { timeout, GLib } from 'astal'; import { Gtk, Gdk, Widget } from 'astal/gtk3'; -import { AstalPlayers } from '../../scripts/player'; import AstalMpris from "gi://AstalMpris"; import { Wallpaper } from '../../scripts/wallpaper'; - -let dragTimer: (Time|null) = null; - -let isDragging = false; -let dragProgress: number | null = null; let pulseAnimationId: number | null = null; let pulseValue = 0; +export interface SliderOptions { + getValue(): number; + getMaxValue(): number; + setValue(value: number): void; + realtimeChangeValue(): boolean; + getPlaybackStatus?(): AstalMpris.PlaybackStatus; +} + function drawRoundedRectangle(cr, x, y, width, height, radius) { radius = Math.min(radius, height / 2, width / 2); @@ -23,7 +25,10 @@ function drawRoundedRectangle(cr, x, y, width, height, radius) { cr.closePath(); } -export function progressBar(player: AstalMpris.Player): Gtk.Widget { +export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { + let isDragging = false; + let dragProgress: number | null = null; + return new Widget.DrawingArea({ className: "progress-drawing-area", hexpand: true, @@ -38,21 +43,15 @@ export function progressBar(player: AstalMpris.Player): Gtk.Widget { const updateDragPosition = (x: number) => { const width = self.get_allocated_width(); - const length = player.get_length() if (width === 0) return; - - const newProgress = Math.max(0, Math.min(x / width, 1)); - - if (dragProgress !== newProgress) { - dragProgress = newProgress; - } + dragProgress = Math.max(0, Math.min(x / width, 1)); + console.log('DragProgress', dragProgress); }; - - self.connect('button-press-event', (_, event) => { - if (player.length > GLib.MAXINT64 / 10000000) return; - const [success, x] = event.get_coords(); + self.connect('button-press-event', (_, event) => { + if (model.getMaxValue() > GLib.MAXINT64 / 10000000) return; isDragging = true; + const [, x] = event.get_coords(); updateDragPosition(x); }); @@ -60,34 +59,39 @@ export function progressBar(player: AstalMpris.Player): Gtk.Widget { if (isDragging) { const [, x] = event.get_coords(); updateDragPosition(x); + + if (model.realtimeChangeValue() && dragProgress !== null) model.setValue(dragProgress * model.getMaxValue()); } }); self.connect('button-release-event', () => { - if (isDragging && dragProgress !== null && player.get_length() > 0) { - const finalPosition = dragProgress * player.get_length(); - player.set_position(finalPosition) - timeout(70, () => isDragging = false); - }; + if (isDragging && dragProgress !== null) { + const maxValue = model.getMaxValue(); + if (maxValue > 0) { + console.log('Set Value', dragProgress * maxValue); + model.setValue(dragProgress * maxValue); + } + + isDragging = model.getPlaybackStatus ? timeout(40, () => { isDragging = false }) : false; + } }); - + self.connect('draw', (self) => { - if (pulseAnimationId !== null) return; + self.queue_draw(); - if (player.playbackStatus === AstalMpris.PlaybackStatus.PLAYING) { - pulseAnimationId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 16, () => { // 16 is milliseconds per frame (~60fps). If you what get more fps, then use this formula: Math.round(1000 / refreshRate) + const playbackStatus = model.getPlaybackStatus ? model.getPlaybackStatus() : null; + + if (playbackStatus === AstalMpris.PlaybackStatus.PLAYING) { + if (pulseAnimationId === null) { + const fpm = Math.round(1000 / Wallpaper.getDefault().getRefreshRate()); + pulseAnimationId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, fpm, () => { pulseValue = Math.sin((Date.now() / 1000) * Math.PI); - self.queue_draw(); return GLib.SOURCE_CONTINUE; - } - ); + }); + } } }); - // self.connect('draw', bind(self, (self, cr) => { - // console.log("Draw", self, typeof self); - // })) - self.connect("destroy", () => { if (pulseAnimationId !== null) { GLib.source_remove(pulseAnimationId); @@ -95,18 +99,21 @@ export function progressBar(player: AstalMpris.Player): Gtk.Widget { } }); }, - + onDraw: (self, cr) => { const styleContext = self.get_style_context(); - const width = self.get_allocated_width(); const height = self.get_allocated_height(); - const position = player.get_position(); - const length = player.get_length(); - - const playerProgress = length > 0 ? position / length : 0; - const displayProgress = (isDragging && dragProgress !== null) ? dragProgress : - (length > (GLib.MAXINT64 / 10000000) ? length : playerProgress); + + const position = model.getValue(); + const length = model.getMaxValue(); + + const currentProgress = length > 0 ? position / length : 0; + const displayProgress = (isDragging && dragProgress !== null) + ? dragProgress + : (model.getMaxValue() > (GLib.MAXINT64 / 10000000) + ? model.getMaxValue() + : currentProgress); const barHeight = 6; const baseHandleRadius = 7; @@ -114,36 +121,30 @@ export function progressBar(player: AstalMpris.Player): Gtk.Widget { const animatedHandleRadius = baseHandleRadius + (pulseValue > 0 ? pulseValue * handlePulseAmount : 0); const centerY = height / 2; const barRadius = barHeight / 2; - const rgba = new Gdk.RGBA(); - const bg = styleContext.get_property('background-color', Gtk.StateFlags.NORMAL); const fg = styleContext.get_property('color', Gtk.StateFlags.NORMAL); - const handleX = Math.max(animatedHandleRadius, Math.min(width * displayProgress, width - animatedHandleRadius)); - // Background - rgba.parse('rgba(100, 100, 100, 0.4)'); - cr.setSourceRGBA(rgba.red, rgba.green, rgba.blue, rgba.alpha); + // 1. Background + cr.setSourceRGBA(fg.red, fg.green, fg.blue, 0.3); drawRoundedRectangle(cr, 0, centerY - barHeight / 2, width, barHeight, barRadius); cr.fill(); - // Progress - cr.save(); - + // 2. Progress + cr.save(); cr.rectangle(0, 0, width * displayProgress, height); - cr.clip(); + cr.clip(); cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); drawRoundedRectangle(cr, 0, centerY - barHeight / 2, width, barHeight, barRadius); cr.fill(); cr.restore(); - // Handle - //rgba.parse('#ffffff'); + // 3. Dot-Handle cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); cr.arc(handleX, centerY, animatedHandleRadius, 0, 2 * Math.PI); cr.fill(); - }, + } }); } From d0a35a0147e8f0ec28a2236917cb04051ea1ff44 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Wed, 23 Jul 2025 20:18:24 +0300 Subject: [PATCH 47/64] Update BigMedia.ts --- ags/widget/center-window/BigMedia.ts | 93 ++++++++++++++++------------ 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts index 0d36da9a..24c0f3d0 100644 --- a/ags/widget/center-window/BigMedia.ts +++ b/ags/widget/center-window/BigMedia.ts @@ -3,7 +3,23 @@ import { Gtk, Widget } from "astal/gtk3"; import AstalMpris from "gi://AstalMpris"; import { AstalPlayers } from "../../scripts/player"; import { Stylesheet } from "../../scripts/stylesheet"; -import { progressBar } from "./Slider"; +import { createUnifiedSlider } from "./Slider"; + +function formatTime(seconds: number): string { + if (isNaN(seconds) || seconds < 0) return "0:00"; + const totalSeconds = Math.floor(seconds); + const sec = totalSeconds % 60; + const min = Math.floor((totalSeconds % 3600) / 60); + const hours = Math.floor(totalSeconds / 3600); + + const minStr = (min < 10 && hours > 0) ? `0${min}` : `${min}`; + const secStr = (sec < 10) ? `0${sec}` : `${sec}`; + + if (hours > 0) { + return `${hours}:${minStr}:${secStr}`; + } + return `${min}:${secStr}`; +} export function BigMedia(): Gtk.Widget { return new Widget.Box({ @@ -11,10 +27,10 @@ export function BigMedia(): Gtk.Widget { orientation: Gtk.Orientation.VERTICAL, homogeneous: false, width_request: 250, - visible: bind(AstalPlayers.getDefault(), "activePlayer").as((actviePlayer: AstalMpris.Player) => - actviePlayer ? true : false), - children: bind(AstalPlayers.getDefault(), "activePlayer").as((actviePlayer: AstalMpris.Player) => - actviePlayer && [ + visible: bind(AstalPlayers.getDefault(), "activePlayer").as((activePlayer: AstalMpris.Player) => + activePlayer ? true : false), + children: bind(AstalPlayers.getDefault(), "activePlayer").as((activePlayer: AstalMpris.Player) => + activePlayer && [ new Widget.Box({ halign: Gtk.Align.CENTER, child: new Widget.Box({ @@ -22,8 +38,8 @@ export function BigMedia(): Gtk.Widget { hexpand: false, orientation: Gtk.Orientation.VERTICAL, marginTop: 6, - visible: bind(actviePlayer, "coverArt").as(Boolean), - css: bind(actviePlayer, "coverArt").as((artUrl: string|undefined) => + visible: bind(activePlayer, "coverArt").as(Boolean), + css: bind(activePlayer, "coverArt").as((artUrl: string|undefined) => artUrl ? `.image { background-image: url('${artUrl}'); }` : undefined), width_request: 132, height_request: 128 @@ -37,15 +53,15 @@ export function BigMedia(): Gtk.Widget { children: [ new Widget.Label({ className: "title", - tooltipText: bind(actviePlayer, "title").as((title: string) => !title ? "No Title" : title), - label: bind(actviePlayer, "title").as((title: string) => !title ? "No Title" : title), + tooltipText: bind(activePlayer, "title").as((title: string) => !title ? "No Title" : title), + label: bind(activePlayer, "title").as((title: string) => !title ? "No Title" : title), truncate: true, maxWidthChars: 25, } as Widget.LabelProps), new Widget.Label({ className: "artist", - tooltipText: bind(actviePlayer, "artist").as((artist: string) => !artist ? (actviePlayer.get_identity() ?? "No Artist") : artist), - label: bind(actviePlayer, "artist").as((artist: string) => !artist ? (actviePlayer.get_identity() ?? "No Artist") : artist), + tooltipText: bind(activePlayer, "artist").as((artist: string) => !artist ? (activePlayer.get_identity() ?? "No Artist") : artist), + label: bind(activePlayer, "artist").as((artist: string) => !artist ? (activePlayer.get_identity() ?? "No Artist") : artist), maxWidthChars: 28, truncate: true, } as Widget.LabelProps) @@ -54,9 +70,15 @@ export function BigMedia(): Gtk.Widget { new Widget.Box({ className: "progress", hexpand: true, - visible: bind(actviePlayer, "canSeek"), + visible: bind(activePlayer, "canSeek"), children: [ - progressBar(actviePlayer) + createUnifiedSlider({ + getValue: () => activePlayer.get_position(), + getMaxValue: () => activePlayer.get_length(), + setValue: (value) => activePlayer.set_position(value), + getPlaybackStatus: () => activePlayer.playback_status, + realtimeChangeValue: () => false + }) ] }), new Widget.CenterBox({ @@ -68,13 +90,8 @@ export function BigMedia(): Gtk.Widget { className: "elapsed", valign: Gtk.Align.START, halign: Gtk.Align.START, - label: bind(actviePlayer, "position").as((pos: number) => { - const sec: number = Math.floor(pos % 60); - const min = Math.floor((pos % 3600) / 60); - const hours: number = Math.floor(pos / 3600); - return pos > 0 && actviePlayer.length > 0 ? - `${hours > 0 ? `${hours}:` : ''}${min < 10 && hours > 0 ? `0${min}` : `${min}`}:${sec < 10 ? `0${sec}` : `${sec}`}` - : `0:00`; + label: bind(activePlayer, "position").as((pos: number) => { + return formatTime(pos > 0 && activePlayer.length > 0 ? pos : 0); }) } as Widget.LabelProps), centerWidget: new Widget.Box({ @@ -86,29 +103,29 @@ export function BigMedia(): Gtk.Widget { icon: "edit-paste-symbolic" } as Widget.IconProps), tooltipText: "Copy link to Clipboard", - visible: bind(actviePlayer, "metadata").as(Boolean), + visible: bind(activePlayer, "metadata").as(Boolean), onClickRelease: async () => { const link = exec(`playerctl --player=${ - actviePlayer.busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") + activePlayer.busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") } metadata xesam:url`); link && Clipboard.getDefault().copyAsync(link); } } as Widget.ButtonProps), new Widget.Button({ className: "shuffle", - visible: bind(actviePlayer, "shuffleStatus").as((shuffleStatus) => + visible: bind(activePlayer, "shuffleStatus").as((shuffleStatus) => shuffleStatus !== AstalMpris.Shuffle.UNSUPPORTED), image: new Widget.Icon({ - icon: bind(actviePlayer, "shuffleStatus").as((shuffleStatus) => + icon: bind(activePlayer, "shuffleStatus").as((shuffleStatus) => shuffleStatus === AstalMpris.Shuffle.ON ? "media-playlist-shuffle-symbolic" : "media-playlist-consecutive-symbolic") } as Widget.IconProps), - tooltipText: bind(actviePlayer, "shuffleStatus").as((shuffleStatus) => + tooltipText: bind(activePlayer, "shuffleStatus").as((shuffleStatus) => shuffleStatus === AstalMpris.Shuffle.ON ? "Shuffle" : "No shuffle"), - onClickRelease: () => actviePlayer.shuffle() + onClickRelease: () => activePlayer.shuffle() } as Widget.ButtonProps), new Widget.Button({ className: "previous", @@ -116,21 +133,21 @@ export function BigMedia(): Gtk.Widget { icon: "media-skip-backward-symbolic" } as Widget.IconProps), tooltipText: "Previous", - onClickRelease: () => actviePlayer.canGoPrevious && actviePlayer.previous() + onClickRelease: () => activePlayer.canGoPrevious && activePlayer.previous() } as Widget.ButtonProps), new Widget.Button({ className: "pause", - tooltipText: bind(actviePlayer, "playback_status").as((status) => + tooltipText: bind(activePlayer, "playback_status").as((status) => status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play"), image: new Widget.Icon({ - icon: bind(actviePlayer, "playbackStatus").as((status) => + icon: bind(activePlayer, "playbackStatus").as((status) => status === AstalMpris.PlaybackStatus.PLAYING ? "media-playback-pause-symbolic" : "media-playback-start-symbolic"), } as Widget.IconProps), - onClickRelease: () => actviePlayer.playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? - actviePlayer.play() - : actviePlayer.pause() + onClickRelease: () => activePlayer.playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? + activePlayer.play() + : activePlayer.pause() } as Widget.ButtonProps), new Widget.Button({ className: "next", @@ -138,14 +155,14 @@ export function BigMedia(): Gtk.Widget { icon: "media-skip-forward-symbolic" } as Widget.IconProps), tooltipText: "Next", - onClickRelease: () => actviePlayer.canGoNext && actviePlayer.next() + onClickRelease: () => activePlayer.canGoNext && activePlayer.next() } as Widget.ButtonProps), new Widget.Button({ className: "repeat", - visible: bind(actviePlayer, "loopStatus").as((loopStatus) => + visible: bind(activePlayer, "loopStatus").as((loopStatus) => loopStatus !== AstalMpris.Loop.UNSUPPORTED), image: new Widget.Icon({ - icon: bind(actviePlayer, "loopStatus").as((loopStatus) => { + icon: bind(activePlayer, "loopStatus").as((loopStatus) => { switch(loopStatus) { case AstalMpris.Loop.TRACK: return "media-playlist-repeat-song-symbolic"; @@ -157,7 +174,7 @@ export function BigMedia(): Gtk.Widget { return "loop-arrow-symbolic"; }) } as Widget.IconProps), - tooltipText: bind(actviePlayer, "loopStatus").as((loopStatus) => { + tooltipText: bind(activePlayer, "loopStatus").as((loopStatus) => { switch(loopStatus) { case AstalMpris.Loop.TRACK: return "Loop song"; @@ -168,7 +185,7 @@ export function BigMedia(): Gtk.Widget { return "No loop"; }), - onClickRelease: () => actviePlayer.loop() + onClickRelease: () => activePlayer.loop() } as Widget.ButtonProps) ] } as Widget.BoxProps), @@ -176,7 +193,7 @@ export function BigMedia(): Gtk.Widget { className: "length", valign: Gtk.Align.START, halign: Gtk.Align.END, - label: bind(actviePlayer, "length").as((len/* bananananananana */: number) => { + label: bind(activePlayer, "length").as((len/* bananananananana */: number) => { const sec: number = Math.floor(len % 60); const min = Math.floor((len % 3600) / 60); From 8564bf74f47a0cdf1d8930743736ad3b1f098c27 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Wed, 23 Jul 2025 20:20:51 +0300 Subject: [PATCH 48/64] Update OSD.ts --- ags/window/OSD.ts | 113 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 85 insertions(+), 28 deletions(-) diff --git a/ags/window/OSD.ts b/ags/window/OSD.ts index f6e0dc21..3ed5195a 100644 --- a/ags/window/OSD.ts +++ b/ags/window/OSD.ts @@ -3,12 +3,14 @@ import { Astal, Gtk, Widget } from "astal/gtk3"; import { Wireplumber } from "../scripts/volume"; import AstalWp from "gi://AstalWp"; import AstalHyprland from "gi://AstalHyprland"; +import { AstalPlayers } from "../scripts/player"; +import { getSymbolicIcon } from "../scripts/apps"; export enum OSDModes { SINK, SOURCE, LAYOUT, - BRIGHTNESS + //BRIGHTNESS, //CAPSLOCK, //NUMLOCK, //PLAYER @@ -16,14 +18,24 @@ export enum OSDModes { let osdMode: (Variable|null); const layoutVar = new Variable(""); +let WireplumberObject: object|null; -//It can be used to get values from connected elements and further process them. export function variableHandler(mode: OSDModes, value: any) { - console.log(`Got value ${value}`); switch (mode) { case OSDModes.LAYOUT: + if (layoutVar === value) return; layoutVar.set(value.substring(0, 2).toUpperCase()); break; + + case OSDModes.SINK: + WireplumberObject = value; + console.log(`Sink mute status: ${typeof value}`); + break; + + case OSDModes.SOURCE: + WireplumberObject = value; + console.log(`Source mute status: ${typeof value}`); + break; } } @@ -36,7 +48,7 @@ export function setOSDMode(newMode: OSDModes): void { function createOSD( props: Widget.BoxProps, iconName: string | Binding, - labelOSD?: string | Binding + labelOSD?: string | Binding, ) { return new Widget.Box({ ...props, @@ -49,8 +61,43 @@ function createOSD( } as Widget.IconProps), new Widget.Label({ className: "action", + visible: labelOSD ? true : false, label: labelOSD, - } as Widget.LabelProps) + } as Widget.LabelProps), + ] + } as Widget.BoxProps) +} + +function createOSDSublabel( + props: Widget.BoxProps, + iconName: string | Binding, + labelOSD?: string | Binding, + sublabelOSD?: string | Binding +) { + return new Widget.Box({ + ...props, + className: `osd ${props.className || ''}`, + expand: true, + children: [ + new Widget.Icon({ + className: "icon", + icon: iconName, + } as Widget.IconProps), + new Widget.Box({ + vertical: true, + children: [ + new Widget.Label({ + className: "action", + visible: labelOSD ? true : false, + label: labelOSD, + } as Widget.LabelProps), + new Widget.Label({ + className: "sublabel", + visible: sublabelOSD ? true : false, + label: sublabelOSD, + } as Widget.LabelProps) + ] + }) ] } as Widget.BoxProps) } @@ -144,6 +191,29 @@ function OSDSource() { ) } +//need to do more work here +function OSDPlayer() { + const player = AstalPlayers.getDefault().activePlayer; + return createOSDSublabel( + { name: "player" }, + bind(AstalPlayers.getDefault(), "activePlayer").as(activePlayer => getSymbolicIcon(activePlayer.get_entry()) || + getSymbolicIcon(activePlayer.get_bus_name().split('.').filter(str => !str.toLowerCase().includes('instance')).join('.')) || + "folder-music-symbolic"), + bind(player, "title").as(title => title ? title : ""), + bind(player, "artist").as(artist => artist ? artist : (player.get_identity() || "")) + ) + /*return createOSDLevelBar( + { name: "player" }, + player, + bind(AstalPlayers.getDefault(), "activePlayer").as(activePlayer => getSymbolicIcon(activePlayer.get_entry()) ?? + getSymbolicIcon(activePlayer.get_bus_name().split('.').filter(str => !str.toLowerCase().includes('instance')).join('.')) ?? + "folder-music-symbolic"), + bind(player, "title"), + bind(player, "position"), + bind(player, "length") + )*/ +} + function OSDLayout() { const hyprland = AstalHyprland.get_default(); return createOSD( @@ -154,7 +224,7 @@ function OSDLayout() { } export const OSD = (mon: number) => { - osdMode = new Variable(OSDModes.SINK); + osdMode = new Variable(); return new Widget.Window({ namespace: "osd", @@ -169,28 +239,15 @@ export const OSD = (mon: number) => { onDestroy: () => { osdMode?.drop(); osdMode = null; - //currenntLayout = null; + WireplumberObject = null; }, - child: new Widget.Stack({ - hhomogeneous: false, - vhomogeneous: false, - visibleChildName: bind(osdMode, "value").as((mode: OSDModes) => { - switch (mode) { - case OSDModes.SINK: return "sink"; - case OSDModes.SOURCE: return "source"; - case OSDModes.LAYOUT: return "layout"; - //default: return "sink"; - } - }), - onDestroy: () => { - osdMode = null; - //currenntLayout = null; - }, - children: [ - OSDSink(), - OSDSource(), - OSDLayout() - ] - } as Widget.BoxProps) + children: bind(osdMode, "value").as((mode: OSDModes) => { + switch (mode) { + case OSDModes.SINK: return OSDSink(); + case OSDModes.SOURCE: return OSDSource(); + case OSDModes.LAYOUT: return OSDLayout(); + case OSDModes.PLAYER: return OSDPlayer(); + } + }), } as Widget.WindowProps); } From 8a7e1771bd22f3349cf8331711f85f1b16be05e8 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Wed, 23 Jul 2025 20:23:34 +0300 Subject: [PATCH 49/64] Update Slider.ts --- ags/widget/center-window/Slider.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ags/widget/center-window/Slider.ts b/ags/widget/center-window/Slider.ts index 5864272e..1ec381aa 100644 --- a/ags/widget/center-window/Slider.ts +++ b/ags/widget/center-window/Slider.ts @@ -45,7 +45,6 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { const width = self.get_allocated_width(); if (width === 0) return; dragProgress = Math.max(0, Math.min(x / width, 1)); - console.log('DragProgress', dragProgress); }; self.connect('button-press-event', (_, event) => { @@ -68,7 +67,6 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { if (isDragging && dragProgress !== null) { const maxValue = model.getMaxValue(); if (maxValue > 0) { - console.log('Set Value', dragProgress * maxValue); model.setValue(dragProgress * maxValue); } @@ -124,7 +122,7 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { const fg = styleContext.get_property('color', Gtk.StateFlags.NORMAL); const handleX = Math.max(animatedHandleRadius, Math.min(width * displayProgress, width - animatedHandleRadius)); - + // 1. Background cr.setSourceRGBA(fg.red, fg.green, fg.blue, 0.3); drawRoundedRectangle(cr, 0, centerY - barHeight / 2, width, barHeight, barRadius); From 07ef64518d9936ff6202b7f425461d6629a3f668 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Wed, 23 Jul 2025 21:10:45 +0300 Subject: [PATCH 50/64] Update Slider.ts --- ags/widget/center-window/Slider.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ags/widget/center-window/Slider.ts b/ags/widget/center-window/Slider.ts index 1ec381aa..35397194 100644 --- a/ags/widget/center-window/Slider.ts +++ b/ags/widget/center-window/Slider.ts @@ -113,22 +113,26 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { ? model.getMaxValue() : currentProgress); + // Bar const barHeight = 6; + + // Dot const baseHandleRadius = 7; const handlePulseAmount = 1.5; const animatedHandleRadius = baseHandleRadius + (pulseValue > 0 ? pulseValue * handlePulseAmount : 0); const centerY = height / 2; const barRadius = barHeight / 2; + const handleX = Math.max(animatedHandleRadius, Math.min(width * displayProgress, width - animatedHandleRadius)); + // Color const fg = styleContext.get_property('color', Gtk.StateFlags.NORMAL); - const handleX = Math.max(animatedHandleRadius, Math.min(width * displayProgress, width - animatedHandleRadius)); - // 1. Background + // Background cr.setSourceRGBA(fg.red, fg.green, fg.blue, 0.3); drawRoundedRectangle(cr, 0, centerY - barHeight / 2, width, barHeight, barRadius); cr.fill(); - // 2. Progress + // Progress cr.save(); cr.rectangle(0, 0, width * displayProgress, height); @@ -139,7 +143,7 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { cr.restore(); - // 3. Dot-Handle + // Dot-Handle cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); cr.arc(handleX, centerY, animatedHandleRadius, 0, 2 * Math.PI); cr.fill(); From bb95de715b690cfdf97ac492c98b0af9d0121583 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:44:55 +0300 Subject: [PATCH 51/64] Update Slider.ts Unification of the function for different requirements --- ags/widget/center-window/Slider.ts | 48 ++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/ags/widget/center-window/Slider.ts b/ags/widget/center-window/Slider.ts index 35397194..d55563ed 100644 --- a/ags/widget/center-window/Slider.ts +++ b/ags/widget/center-window/Slider.ts @@ -11,17 +11,17 @@ export interface SliderOptions { getMaxValue(): number; setValue(value: number): void; realtimeChangeValue(): boolean; + //getColor(): string; getPlaybackStatus?(): AstalMpris.PlaybackStatus; } function drawRoundedRectangle(cr, x, y, width, height, radius) { radius = Math.min(radius, height / 2, width / 2); - cr.moveTo(x + radius, y); - cr.arc(x + width - radius, y + radius, radius, 1.5 * Math.PI, 2 * Math.PI); // Top right - cr.arc(x + width - radius, y + height - radius, radius, 0, 0.5 * Math.PI); // Bottom right - cr.arc(x + radius, y + height - radius, radius, 0.5 * Math.PI, Math.PI); // Bottom left - cr.arc(x + radius, y + radius, radius, Math.PI, 1.5 * Math.PI); // Top left + cr.arc(x + width - radius, y + radius, radius, 1.5 * Math.PI, 2 * Math.PI); + cr.arc(x + width - radius, y + height - radius, radius, 0, 0.5 * Math.PI); + cr.arc(x + radius, y + height - radius, radius, 0.5 * Math.PI, Math.PI); + cr.arc(x + radius, y + radius, radius, Math.PI, 1.5 * Math.PI); cr.closePath(); } @@ -81,7 +81,7 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { if (playbackStatus === AstalMpris.PlaybackStatus.PLAYING) { if (pulseAnimationId === null) { - const fpm = Math.round(1000 / Wallpaper.getDefault().getRefreshRate()); + const fpm = 16; // 16 is Miliseconds Per Frame (~60). If you want set more than 60 fps, then use this formula: Math.round(1000 / Wallpaper.getDefault().getRefreshRate()) pulseAnimationId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, fpm, () => { pulseValue = Math.sin((Date.now() / 1000) * Math.PI); return GLib.SOURCE_CONTINUE; @@ -112,21 +112,32 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { : (model.getMaxValue() > (GLib.MAXINT64 / 10000000) ? model.getMaxValue() : currentProgress); - + // Bar - const barHeight = 6; + const barHeight = 10; + const centerY = height / 2; + const barRadius = barHeight / 2; - // Dot + // Base parameters const baseHandleRadius = 7; + + // Dot const handlePulseAmount = 1.5; const animatedHandleRadius = baseHandleRadius + (pulseValue > 0 ? pulseValue * handlePulseAmount : 0); - const centerY = height / 2; - const barRadius = barHeight / 2; - const handleX = Math.max(animatedHandleRadius, Math.min(width * displayProgress, width - animatedHandleRadius)); + const dotHandleX = Math.max(animatedHandleRadius, Math.min(width * displayProgress, width - animatedHandleRadius)); + + // Strip/Rectangle + const rectangleHandleHeight = 20; + const rectangleHandleWidth = 10; + const rectangleHandleX = Math.max(0, Math.min(width * displayProgress - (rectangleHandleWidth / 2), width - rectangleHandleWidth)); // Color const fg = styleContext.get_property('color', Gtk.StateFlags.NORMAL); - + + //const color = new Gdk.RGBA(); + + //color.parse(model.getColor()); + // Background cr.setSourceRGBA(fg.red, fg.green, fg.blue, 0.3); drawRoundedRectangle(cr, 0, centerY - barHeight / 2, width, barHeight, barRadius); @@ -137,15 +148,22 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { cr.rectangle(0, 0, width * displayProgress, height); cr.clip(); + cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); + drawRoundedRectangle(cr, 0, centerY - barHeight / 2, width, barHeight, barRadius); cr.fill(); cr.restore(); - // Dot-Handle + // Handle cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); - cr.arc(handleX, centerY, animatedHandleRadius, 0, 2 * Math.PI); + + // Dot + //cr.arc(dotHandleX, centerY, animatedHandleRadius, 0, 2 * Math.PI); + + // Strip/Rectangle + drawRoundedRectangle(cr, rectangleHandleX, centerY - rectangleHandleHeight / 2, rectangleHandleWidth, rectangleHandleHeight, barRadius); cr.fill(); } }); From 41e7e40f39281c4af2a0e6eb0db250be5f2a7ae6 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Fri, 25 Jul 2025 23:35:12 +0300 Subject: [PATCH 52/64] Fixed keymode settings for PopupWindow Sometimes EXCLUSIVE setting is blocking interaction with bar --- ags/widget/PopupWindow.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/ags/widget/PopupWindow.ts b/ags/widget/PopupWindow.ts index fe66b05c..bde0f28d 100644 --- a/ags/widget/PopupWindow.ts +++ b/ags/widget/PopupWindow.ts @@ -1,6 +1,9 @@ -import { Binding } from "astal"; +import { Binding, bind } from "astal"; import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; import { BackgroundWindow } from "./BackgroundWindow"; +import AstalHyprland from "gi://AstalHyprland"; + +const hyprland = AstalHyprland.get_default(); type PopupWindowSpecificProps = { onDestroy?: (self: Widget.Window) => void; @@ -50,13 +53,17 @@ export function PopupWindow(props: PopupWindowProps): Widget.Window { // @ts-ignore ignore the `onClickedOutside()` method because astal thinks it's a signal winProps[key as keyof typeof winProps] = props[key as keyof typeof props]; } - + return new Widget.Window({ ...winProps, namespace: props?.namespace ?? "popup-window", className: `popup-window ${(props.namespace instanceof Binding ? props.namespace.get() : props.namespace) || ""}`, - keymode: Astal.Keymode.EXCLUSIVE, + keymode: bind(hyprland, 'focusedWorkspace').as(fw => + fw.last_client.fullscreen === AstalHyprland.Fullscreen.FULLSCREEN && fw.last_client.workspace === fw + ? Astal.Keymode.EXCLUSIVE + : Astal.Keymode.ON_DEMAND + ), anchor: TOP | LEFT | RIGHT | BOTTOM, exclusivity: props.exclusivity ?? Astal.Exclusivity.NORMAL, halign: undefined, From 9337e3268adb14449ab25b4ef7df3f947d9b2b29 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 26 Jul 2025 15:03:06 +0300 Subject: [PATCH 53/64] Overall optimization & new sliders Add slider like Material 3 Expressive --- ags/widget/assets/Slider.ts | 243 ++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 ags/widget/assets/Slider.ts diff --git a/ags/widget/assets/Slider.ts b/ags/widget/assets/Slider.ts new file mode 100644 index 00000000..355c840a --- /dev/null +++ b/ags/widget/assets/Slider.ts @@ -0,0 +1,243 @@ +import { timeout, GLib } from 'astal'; +import { Gtk, Gdk, Widget } from 'astal/gtk3'; +import AstalMpris from "gi://AstalMpris"; +import { Wallpaper } from '../scripts/wallpaper'; + +let pauseProgress = 1; // 0 = full wave, 1 = full straight + +export enum typeSliders { + MATERIAL_EXPRESSIVE, + CHUNKY_MODERN +} + +export interface SliderOptions { + getValue(): number; + getMaxValue(): number; + setValue(value: number): void; + realtimeChangeValue(): boolean; + getColor(): string | (() => string | null); + typeSlider(): typeSliders; + getPlaybackStatus?(): AstalMpris.PlaybackStatus; +} + +function drawRoundedRectangle(cr, x, y, width, height, radius) { + radius = Math.min(radius, height / 2, width / 2); + cr.moveTo(x + radius, y); + cr.arc(x + width - radius, y + radius, radius, 1.5 * Math.PI, 2 * Math.PI); + cr.arc(x + width - radius, y + height - radius, radius, 0, 0.5 * Math.PI); + cr.arc(x + radius, y + height - radius, radius, 0.5 * Math.PI, Math.PI); + cr.arc(x + radius, y + radius, radius, Math.PI, 1.5 * Math.PI); + cr.closePath(); +} + +export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { + let isDragging = false; + let dragProgress: number | null = null; + + const isPlayingStream = model.getMaxValue() < GLib.MAXINT64 / 10000000 ? false : true; + + return new Widget.EventBox({ + children: [ + new Widget.DrawingArea({ + className: "slider-drawing-area", + hexpand: true, + heightRequest: 30, + + setup: (self) => { + self.add_events( + Gdk.EventMask.BUTTON_PRESS_MASK | + Gdk.EventMask.BUTTON_RELEASE_MASK | + Gdk.EventMask.POINTER_MOTION_MASK + ); + + const updateDragPosition = (x: number) => { + const width = self.get_allocated_width(); + if (width === 0) return; + dragProgress = Math.max(0, Math.min(x / width, 1)); + }; + + self.connect('button-press-event', (_, event) => { + if (isPlayingStream) return; + isDragging = true; + const [, x] = event.get_coords(); + updateDragPosition(x); + }); + + self.connect('motion-notify-event', (_, event) => { + if (isDragging) { + const [, x] = event.get_coords(); + updateDragPosition(x); + + if (model.realtimeChangeValue() && dragProgress !== null) model.setValue(dragProgress * model.getMaxValue()); + } + }); + + self.connect('button-release-event', () => { + if (isDragging && dragProgress !== null) { + const maxValue = model.getMaxValue(); + if (maxValue > 0) { + model.setValue(dragProgress * maxValue); + } + + isDragging = model.getPlaybackStatus ? timeout(30, () => { isDragging = false }) : false; + } + }); + + + let drawLoopId: number | null = null; + + self.connect('realize', () => { + if (drawLoopId === null) { + const fpm = Math.round(1000 / Wallpaper.getDefault().getRefreshRate()); + drawLoopId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, fpm, () => { + if (self.get_window()?.is_visible()) { + self.queue_draw(); + } + return GLib.SOURCE_CONTINUE; + }); + } + }); + + self.connect('draw', (self) => { + if (model.typeSlider() === typeSliders.MATERIAL_EXPRESSIVE) { + const playbackStatus = model.getPlaybackStatus ? model.getPlaybackStatus() : null; + + if (playbackStatus === AstalMpris.PlaybackStatus.PLAYING) { + if (pauseProgress > 0) { + pauseProgress = Math.max(0, pauseProgress - 0.03); + } + } else { + if (pauseProgress < 1) { + pauseProgress = Math.min(1, pauseProgress + 0.03); + } + } + } + }); + + self.connect("destroy", () => { + if (drawLoopId !== null) { + GLib.source_remove(drawLoopId); + drawLoopId = null; + } + }); + }, + + onDraw: (self, cr) => { + const styleContext = self.get_style_context(); + const width = self.get_allocated_width(); + const height = self.get_allocated_height(); + + const position = model.getValue(); + const length = model.getMaxValue(); + + const currentProgress = length > 0 ? position / length : 0; + const displayProgress = (isDragging && dragProgress !== null) + ? dragProgress + : (!isPlayingStream + ? currentProgress + : 1 //Max + ); + + const styleType = model.typeSlider(); + const fg = styleContext.get_property('color', Gtk.StateFlags.NORMAL); + const rawColor = model.getColor; + const colorStr = typeof rawColor === 'function' ? rawColor() : rawColor; + const color = new Gdk.RGBA(); + if (colorStr) color.parse(colorStr); + + switch (styleType) { + case typeSliders.CHUNKY_MODERN: { + + const barHeight = 15; + const centerY = height / 2; + const barRadius = Math.max(5, barHeight / 2); + + const progressX = width * displayProgress; + + const rectangleHandleWidth = 10; + const rectangleHandleHeight = Math.max(barHeight + rectangleHandleWidth, Math.min(height - barHeight, barHeight + (barHeight / 2))); + const rectangleHandleX = Math.max(0, Math.min(progressX - (rectangleHandleWidth / 2), width - rectangleHandleWidth)); + + const handleOffset = 5; + const activeEndX = rectangleHandleX - handleOffset; + const inactiveStartX = rectangleHandleX + rectangleHandleWidth + handleOffset; + + if (displayProgress > 0 && activeEndX > 0) { + colorStr ? cr.setSourceRGBA(color.red, color.green, color.blue, color.alpha) : cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); + drawRoundedRectangle(cr, 0, centerY - barHeight / 2, Math.max(0, activeEndX), barHeight, barRadius); + cr.fill(); + } + + if (displayProgress < 1 && inactiveStartX < width) { + cr.setSourceRGBA(fg.red, fg.green, fg.blue, 0.3); + const inactiveWidth = width - inactiveStartX; + if (inactiveWidth > 0) { + drawRoundedRectangle(cr, inactiveStartX, centerY - barHeight / 2, inactiveWidth, barHeight, barRadius); + cr.fill(); + } + } + + cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); + drawRoundedRectangle(cr, rectangleHandleX, centerY - rectangleHandleHeight / 2, rectangleHandleWidth, rectangleHandleHeight, barRadius); + cr.fill(); + break; + } + case typeSliders.MATERIAL_EXPRESSIVE: { + const lineThickness = 6; + + const baseWaveAmp = 2; + const waveAmp = baseWaveAmp * (1 - pauseProgress); + const waveFreq = 0.15; + const time = Date.now() * 0.003; + + const rectangleHandleWidth = 5; + const rectangleHandleHeight = 20; + const rectangleHandleX = Math.max(0, Math.min(width * displayProgress - (rectangleHandleWidth / 2), width - rectangleHandleWidth)); + + const centerY = height / 2; + const progressX = width * displayProgress; + const startX = lineThickness / 2; + const activeEndX = Math.max(0, progressX - rectangleHandleWidth * 2); + + const radius = 10; + + cr.setLineWidth(lineThickness); + cr.setLineCap(1); // ROUND caps + + // Active part (wave or line) + if (activeEndX > startX) { + const src = colorStr ? color : fg; + cr.setSourceRGBA(src.red, src.green, src.blue, src.alpha); + + cr.newPath(); + cr.moveTo(startX, centerY + Math.sin(startX * waveFreq + time) * waveAmp); + for (let x = startX; x < activeEndX; x++) { + const y = centerY + Math.sin(x * waveFreq + time) * waveAmp; + cr.lineTo(x, y); + } + cr.stroke(); + } + + // Unactive part + const inactiveStartX = Math.max(lineThickness * 2, Math.min(width, progressX + rectangleHandleWidth * 2)); + if (inactiveStartX < width - startX) { + cr.setSourceRGBA(fg.red, fg.green, fg.blue, 0.3); + cr.newPath(); + cr.moveTo(inactiveStartX, centerY); + cr.lineTo(width - startX, centerY); + cr.stroke(); + } + + if (!isPlayingStream) { + cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); + drawRoundedRectangle(cr, rectangleHandleX, centerY - rectangleHandleHeight / 2, rectangleHandleWidth, rectangleHandleHeight, radius); + cr.fill(); + } + break; + } + } + } + } as Widget.DrawingAreaProps) + ] + } as Widget.EventBoxProps) +} From 7fc4dff88b90efe571a347dba7ac9a6945982024 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 26 Jul 2025 15:03:47 +0300 Subject: [PATCH 54/64] Delete ags/widget/center-window/Slider.ts --- ags/widget/center-window/Slider.ts | 170 ----------------------------- 1 file changed, 170 deletions(-) delete mode 100644 ags/widget/center-window/Slider.ts diff --git a/ags/widget/center-window/Slider.ts b/ags/widget/center-window/Slider.ts deleted file mode 100644 index d55563ed..00000000 --- a/ags/widget/center-window/Slider.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { timeout, GLib } from 'astal'; -import { Gtk, Gdk, Widget } from 'astal/gtk3'; -import AstalMpris from "gi://AstalMpris"; -import { Wallpaper } from '../../scripts/wallpaper'; - -let pulseAnimationId: number | null = null; -let pulseValue = 0; - -export interface SliderOptions { - getValue(): number; - getMaxValue(): number; - setValue(value: number): void; - realtimeChangeValue(): boolean; - //getColor(): string; - getPlaybackStatus?(): AstalMpris.PlaybackStatus; -} - -function drawRoundedRectangle(cr, x, y, width, height, radius) { - radius = Math.min(radius, height / 2, width / 2); - cr.moveTo(x + radius, y); - cr.arc(x + width - radius, y + radius, radius, 1.5 * Math.PI, 2 * Math.PI); - cr.arc(x + width - radius, y + height - radius, radius, 0, 0.5 * Math.PI); - cr.arc(x + radius, y + height - radius, radius, 0.5 * Math.PI, Math.PI); - cr.arc(x + radius, y + radius, radius, Math.PI, 1.5 * Math.PI); - cr.closePath(); -} - -export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { - let isDragging = false; - let dragProgress: number | null = null; - - return new Widget.DrawingArea({ - className: "progress-drawing-area", - hexpand: true, - heightRequest: 30, - - setup: (self) => { - self.add_events( - Gdk.EventMask.BUTTON_PRESS_MASK | - Gdk.EventMask.BUTTON_RELEASE_MASK | - Gdk.EventMask.POINTER_MOTION_MASK - ); - - const updateDragPosition = (x: number) => { - const width = self.get_allocated_width(); - if (width === 0) return; - dragProgress = Math.max(0, Math.min(x / width, 1)); - }; - - self.connect('button-press-event', (_, event) => { - if (model.getMaxValue() > GLib.MAXINT64 / 10000000) return; - isDragging = true; - const [, x] = event.get_coords(); - updateDragPosition(x); - }); - - self.connect('motion-notify-event', (_, event) => { - if (isDragging) { - const [, x] = event.get_coords(); - updateDragPosition(x); - - if (model.realtimeChangeValue() && dragProgress !== null) model.setValue(dragProgress * model.getMaxValue()); - } - }); - - self.connect('button-release-event', () => { - if (isDragging && dragProgress !== null) { - const maxValue = model.getMaxValue(); - if (maxValue > 0) { - model.setValue(dragProgress * maxValue); - } - - isDragging = model.getPlaybackStatus ? timeout(40, () => { isDragging = false }) : false; - } - }); - - self.connect('draw', (self) => { - self.queue_draw(); - - const playbackStatus = model.getPlaybackStatus ? model.getPlaybackStatus() : null; - - if (playbackStatus === AstalMpris.PlaybackStatus.PLAYING) { - if (pulseAnimationId === null) { - const fpm = 16; // 16 is Miliseconds Per Frame (~60). If you want set more than 60 fps, then use this formula: Math.round(1000 / Wallpaper.getDefault().getRefreshRate()) - pulseAnimationId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, fpm, () => { - pulseValue = Math.sin((Date.now() / 1000) * Math.PI); - return GLib.SOURCE_CONTINUE; - }); - } - } - }); - - self.connect("destroy", () => { - if (pulseAnimationId !== null) { - GLib.source_remove(pulseAnimationId); - pulseAnimationId = null; - } - }); - }, - - onDraw: (self, cr) => { - const styleContext = self.get_style_context(); - const width = self.get_allocated_width(); - const height = self.get_allocated_height(); - - const position = model.getValue(); - const length = model.getMaxValue(); - - const currentProgress = length > 0 ? position / length : 0; - const displayProgress = (isDragging && dragProgress !== null) - ? dragProgress - : (model.getMaxValue() > (GLib.MAXINT64 / 10000000) - ? model.getMaxValue() - : currentProgress); - - // Bar - const barHeight = 10; - const centerY = height / 2; - const barRadius = barHeight / 2; - - // Base parameters - const baseHandleRadius = 7; - - // Dot - const handlePulseAmount = 1.5; - const animatedHandleRadius = baseHandleRadius + (pulseValue > 0 ? pulseValue * handlePulseAmount : 0); - const dotHandleX = Math.max(animatedHandleRadius, Math.min(width * displayProgress, width - animatedHandleRadius)); - - // Strip/Rectangle - const rectangleHandleHeight = 20; - const rectangleHandleWidth = 10; - const rectangleHandleX = Math.max(0, Math.min(width * displayProgress - (rectangleHandleWidth / 2), width - rectangleHandleWidth)); - - // Color - const fg = styleContext.get_property('color', Gtk.StateFlags.NORMAL); - - //const color = new Gdk.RGBA(); - - //color.parse(model.getColor()); - - // Background - cr.setSourceRGBA(fg.red, fg.green, fg.blue, 0.3); - drawRoundedRectangle(cr, 0, centerY - barHeight / 2, width, barHeight, barRadius); - cr.fill(); - - // Progress - cr.save(); - cr.rectangle(0, 0, width * displayProgress, height); - - cr.clip(); - - cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); - - drawRoundedRectangle(cr, 0, centerY - barHeight / 2, width, barHeight, barRadius); - cr.fill(); - - cr.restore(); - - // Handle - cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); - - // Dot - //cr.arc(dotHandleX, centerY, animatedHandleRadius, 0, 2 * Math.PI); - - // Strip/Rectangle - drawRoundedRectangle(cr, rectangleHandleX, centerY - rectangleHandleHeight / 2, rectangleHandleWidth, rectangleHandleHeight, barRadius); - cr.fill(); - } - }); -} From 44cdd7ebac2e444b340e9df2b3898df9053d9cc1 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 26 Jul 2025 15:05:16 +0300 Subject: [PATCH 55/64] Update PopupWindow.ts --- ags/widget/PopupWindow.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/ags/widget/PopupWindow.ts b/ags/widget/PopupWindow.ts index bde0f28d..5f594e0f 100644 --- a/ags/widget/PopupWindow.ts +++ b/ags/widget/PopupWindow.ts @@ -2,6 +2,7 @@ import { Binding, bind } from "astal"; import { Astal, Gdk, Gtk, Widget } from "astal/gtk3"; import { BackgroundWindow } from "./BackgroundWindow"; import AstalHyprland from "gi://AstalHyprland"; +import { Windows } from "../windows"; const hyprland = AstalHyprland.get_default(); @@ -53,17 +54,24 @@ export function PopupWindow(props: PopupWindowProps): Widget.Window { // @ts-ignore ignore the `onClickedOutside()` method because astal thinks it's a signal winProps[key as keyof typeof winProps] = props[key as keyof typeof props]; } + + let isFullscreen: boolean; return new Widget.Window({ ...winProps, namespace: props?.namespace ?? "popup-window", className: `popup-window ${(props.namespace instanceof Binding ? props.namespace.get() : props.namespace) || ""}`, - keymode: bind(hyprland, 'focusedWorkspace').as(fw => - fw.last_client.fullscreen === AstalHyprland.Fullscreen.FULLSCREEN && fw.last_client.workspace === fw + keymode: bind(hyprland, 'focusedWorkspace').as(fw => { + + if (Windows.isVisible('logout-menu')) return Astal.Keymode.EXCLUSIVE; + + if (fw.get_last_client() === null) return Astal.Keymode.ON_DEMAND; + + return fw.get_last_client().get_fullscreen() === AstalHyprland.Fullscreen.FULLSCREEN && fw.get_last_client().get_workspace() === fw ? Astal.Keymode.EXCLUSIVE : Astal.Keymode.ON_DEMAND - ), + }), anchor: TOP | LEFT | RIGHT | BOTTOM, exclusivity: props.exclusivity ?? Astal.Exclusivity.NORMAL, halign: undefined, From 8465f772d29278d4e6de90185cae72c3971ee48f Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sat, 26 Jul 2025 18:59:39 +0300 Subject: [PATCH 56/64] Update Slider.ts --- ags/widget/assets/Slider.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ags/widget/assets/Slider.ts b/ags/widget/assets/Slider.ts index 355c840a..24a59b26 100644 --- a/ags/widget/assets/Slider.ts +++ b/ags/widget/assets/Slider.ts @@ -88,8 +88,9 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { self.connect('realize', () => { if (drawLoopId === null) { - const fpm = Math.round(1000 / Wallpaper.getDefault().getRefreshRate()); - drawLoopId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, fpm, () => { + // 16 ms is about 60 fps, so if you want get more fps, use this formula: Math.round(1000 / RefreshRate) + const FrameToMilliseconds = 16; // 60 fps + drawLoopId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, FrameToMilliseconds, () => { if (self.get_window()?.is_visible()) { self.queue_draw(); } From 9cdcbaf4f013737d3acf4b849c1ede55a7bdb2b9 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sun, 27 Jul 2025 16:36:16 +0300 Subject: [PATCH 57/64] Rework Slider --- ags/widget/assets/Slider.ts | 155 ++++++++++++++++++++++++++---------- 1 file changed, 115 insertions(+), 40 deletions(-) diff --git a/ags/widget/assets/Slider.ts b/ags/widget/assets/Slider.ts index 24a59b26..cddd1637 100644 --- a/ags/widget/assets/Slider.ts +++ b/ags/widget/assets/Slider.ts @@ -1,13 +1,12 @@ import { timeout, GLib } from 'astal'; import { Gtk, Gdk, Widget } from 'astal/gtk3'; import AstalMpris from "gi://AstalMpris"; -import { Wallpaper } from '../scripts/wallpaper'; let pauseProgress = 1; // 0 = full wave, 1 = full straight export enum typeSliders { - MATERIAL_EXPRESSIVE, - CHUNKY_MODERN + MATERIAL_EXPRESSIVE_WAVE, //For players + MATERIAL_EXPRESSIVE_SLIDER } export interface SliderOptions { @@ -20,21 +19,57 @@ export interface SliderOptions { getPlaybackStatus?(): AstalMpris.PlaybackStatus; } -function drawRoundedRectangle(cr, x, y, width, height, radius) { +function drawRoundedRectangleCustom(cr, x, y, width, height, radius, corners = { + topLeft: true, + topRight: true, + bottomRight: true, + bottomLeft: true +}) { radius = Math.min(radius, height / 2, width / 2); - cr.moveTo(x + radius, y); - cr.arc(x + width - radius, y + radius, radius, 1.5 * Math.PI, 2 * Math.PI); - cr.arc(x + width - radius, y + height - radius, radius, 0, 0.5 * Math.PI); - cr.arc(x + radius, y + height - radius, radius, 0.5 * Math.PI, Math.PI); - cr.arc(x + radius, y + radius, radius, Math.PI, 1.5 * Math.PI); + + cr.moveTo(x + (corners.topLeft ? radius : 0), y); + + // Top rigth + if (corners.topRight) { + cr.arc(x + width - radius, y + radius, radius, 1.5 * Math.PI, 2 * Math.PI); + } else { + cr.lineTo(x + width, y); + cr.lineTo(x + width, y + radius); + } + + // Bottom rigth + if (corners.bottomRight) { + cr.arc(x + width - radius, y + height - radius, radius, 0, 0.5 * Math.PI); + } else { + cr.lineTo(x + width, y + height); + cr.lineTo(x + width - radius, y + height); + } + + // Bottom left + if (corners.bottomLeft) { + cr.arc(x + radius, y + height - radius, radius, 0.5 * Math.PI, Math.PI); + } else { + cr.lineTo(x, y + height); + cr.lineTo(x, y + height - radius); + } + + // Top left + if (corners.topLeft) { + cr.arc(x + radius, y + radius, radius, Math.PI, 1.5 * Math.PI); + } else { + cr.lineTo(x, y); + cr.lineTo(x + radius, y); + } + cr.closePath(); } + export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { let isDragging = false; let dragProgress: number | null = null; - const isPlayingStream = model.getMaxValue() < GLib.MAXINT64 / 10000000 ? false : true; + const isStreamPlaying = model.getMaxValue() >= GLib.MAXINT64 / 10000000; return new Widget.EventBox({ children: [ @@ -57,7 +92,7 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { }; self.connect('button-press-event', (_, event) => { - if (isPlayingStream) return; + if (isStreamPlaying) return; isDragging = true; const [, x] = event.get_coords(); updateDragPosition(x); @@ -79,7 +114,7 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { model.setValue(dragProgress * maxValue); } - isDragging = model.getPlaybackStatus ? timeout(30, () => { isDragging = false }) : false; + isDragging = model.getPlaybackStatus ? timeout(40, () => { isDragging = false }) : false; } }); @@ -89,7 +124,7 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { self.connect('realize', () => { if (drawLoopId === null) { // 16 ms is about 60 fps, so if you want get more fps, use this formula: Math.round(1000 / RefreshRate) - const FrameToMilliseconds = 16; // 60 fps + const FrameToMilliseconds = 16; // ~60 fps drawLoopId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, FrameToMilliseconds, () => { if (self.get_window()?.is_visible()) { self.queue_draw(); @@ -100,7 +135,7 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { }); self.connect('draw', (self) => { - if (model.typeSlider() === typeSliders.MATERIAL_EXPRESSIVE) { + if (model.typeSlider() === typeSliders.MATERIAL_EXPRESSIVE_WAVE) { const playbackStatus = model.getPlaybackStatus ? model.getPlaybackStatus() : null; if (playbackStatus === AstalMpris.PlaybackStatus.PLAYING) { @@ -134,7 +169,7 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { const currentProgress = length > 0 ? position / length : 0; const displayProgress = (isDragging && dragProgress !== null) ? dragProgress - : (!isPlayingStream + : (!isStreamPlaying ? currentProgress : 1 //Max ); @@ -147,46 +182,86 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { if (colorStr) color.parse(colorStr); switch (styleType) { - case typeSliders.CHUNKY_MODERN: { - + case typeSliders.MATERIAL_EXPRESSIVE_SLIDER: { const barHeight = 15; - const centerY = height / 2; const barRadius = Math.max(5, barHeight / 2); - + const centerY = height / 2; const progressX = width * displayProgress; - - const rectangleHandleWidth = 10; - const rectangleHandleHeight = Math.max(barHeight + rectangleHandleWidth, Math.min(height - barHeight, barHeight + (barHeight / 2))); - const rectangleHandleX = Math.max(0, Math.min(progressX - (rectangleHandleWidth / 2), width - rectangleHandleWidth)); - + + const handleWidth = 5; + const handleHeight = Math.max(barHeight + handleWidth, Math.min(height - barHeight, barHeight + barHeight / 2)); + const handleX = Math.max(0, Math.min(progressX - handleWidth / 2, width - handleWidth)); + const handleOffset = 5; - const activeEndX = rectangleHandleX - handleOffset; - const inactiveStartX = rectangleHandleX + rectangleHandleWidth + handleOffset; - + const activeEndX = handleX - handleOffset; + const inactiveStartX = handleX + handleWidth + handleOffset; + + // Active line if (displayProgress > 0 && activeEndX > 0) { - colorStr ? cr.setSourceRGBA(color.red, color.green, color.blue, color.alpha) : cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); - drawRoundedRectangle(cr, 0, centerY - barHeight / 2, Math.max(0, activeEndX), barHeight, barRadius); + const colorToUse = colorStr ? color : fg; + cr.setSourceRGBA(colorToUse.red, colorToUse.green, colorToUse.blue, colorToUse.alpha); + drawRoundedRectangleCustom( + cr, + 0, centerY - barHeight / 2, + Math.max(0, activeEndX), barHeight, barRadius, + { topLeft: true, topRight: false, bottomRight: false, bottomLeft: true } + ); cr.fill(); } - - if (displayProgress < 1 && inactiveStartX < width) { - cr.setSourceRGBA(fg.red, fg.green, fg.blue, 0.3); + + // Unactive line + const hasInactive = displayProgress < 1 && inactiveStartX < width; + if (hasInactive) { const inactiveWidth = width - inactiveStartX; if (inactiveWidth > 0) { - drawRoundedRectangle(cr, inactiveStartX, centerY - barHeight / 2, inactiveWidth, barHeight, barRadius); + cr.setSourceRGBA(fg.red, fg.green, fg.blue, 0.3); + drawRoundedRectangleCustom( + cr, + inactiveStartX, centerY - barHeight / 2, + inactiveWidth, barHeight, barRadius, + { topLeft: false, topRight: true, bottomRight: true, bottomLeft: false } + ); cr.fill(); } } - + + // Handle cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); - drawRoundedRectangle(cr, rectangleHandleX, centerY - rectangleHandleHeight / 2, rectangleHandleWidth, rectangleHandleHeight, barRadius); + drawRoundedRectangleCustom( + cr, + handleX, centerY - handleHeight / 2, + handleWidth, handleHeight, barRadius + ); cr.fill(); + + // Max Value Dot + if (hasInactive) { + const dotRadius = barHeight / 6; + const dotX = width - barHeight / 2; + const colorToUse = colorStr ? color : fg; + + cr.save(); + drawRoundedRectangleCustom( + cr, + inactiveStartX, centerY - barHeight / 2, + width - inactiveStartX, barHeight, barRadius, + { topLeft: false, topRight: true, bottomRight: true, bottomLeft: false } + ); + cr.clip(); + + cr.setSourceRGBA(colorToUse.red, colorToUse.green, colorToUse.blue, colorToUse.alpha); + cr.newPath(); + cr.arc(dotX, centerY, dotRadius, 0, 2 * Math.PI); + cr.fill(); + cr.restore(); + } + break; } - case typeSliders.MATERIAL_EXPRESSIVE: { + case typeSliders.MATERIAL_EXPRESSIVE_WAVE: { const lineThickness = 6; - const baseWaveAmp = 2; + const baseWaveAmp = 3; const waveAmp = baseWaveAmp * (1 - pauseProgress); const waveFreq = 0.15; const time = Date.now() * 0.003; @@ -198,7 +273,7 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { const centerY = height / 2; const progressX = width * displayProgress; const startX = lineThickness / 2; - const activeEndX = Math.max(0, progressX - rectangleHandleWidth * 2); + const activeEndX = Math.max(startX, progressX - rectangleHandleWidth * 2); const radius = 10; @@ -229,9 +304,9 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { cr.stroke(); } - if (!isPlayingStream) { + if (!isStreamPlaying) { cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); - drawRoundedRectangle(cr, rectangleHandleX, centerY - rectangleHandleHeight / 2, rectangleHandleWidth, rectangleHandleHeight, radius); + drawRoundedRectangleCustom(cr, rectangleHandleX, centerY - rectangleHandleHeight / 2, rectangleHandleWidth, rectangleHandleHeight, radius); cr.fill(); } break; From deedfe550fa2aa7a5ecee5f179110d3090a2a1ed Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Mon, 28 Jul 2025 00:32:52 +0300 Subject: [PATCH 58/64] Polishing the appearance of sliders... --- ags/widget/assets/Slider.ts | 138 +++++++++++++++++++++++------------- 1 file changed, 88 insertions(+), 50 deletions(-) diff --git a/ags/widget/assets/Slider.ts b/ags/widget/assets/Slider.ts index cddd1637..06b0d07c 100644 --- a/ags/widget/assets/Slider.ts +++ b/ags/widget/assets/Slider.ts @@ -1,6 +1,7 @@ import { timeout, GLib } from 'astal'; import { Gtk, Gdk, Widget } from 'astal/gtk3'; import AstalMpris from "gi://AstalMpris"; +import { Wallpaper } from '../../scripts/wallpaper'; let pauseProgress = 1; // 0 = full wave, 1 = full straight @@ -9,63 +10,87 @@ export enum typeSliders { MATERIAL_EXPRESSIVE_SLIDER } -export interface SliderOptions { - getValue(): number; - getMaxValue(): number; - setValue(value: number): void; - realtimeChangeValue(): boolean; - getColor(): string | (() => string | null); - typeSlider(): typeSliders; - getPlaybackStatus?(): AstalMpris.PlaybackStatus; -} - -function drawRoundedRectangleCustom(cr, x, y, width, height, radius, corners = { - topLeft: true, - topRight: true, - bottomRight: true, - bottomLeft: true +function drawRoundedRectangleCustom(cr, x, y, width, height, options: { + topLeft?: boolean | number | 'small'; + topRight?: boolean | number | 'small'; + bottomRight?: boolean | number | 'small'; + bottomLeft?: boolean | number | 'small'; + minRadius?: number; }) { - radius = Math.min(radius, height / 2, width / 2); + const { + topLeft = 0, + topRight = 0, + bottomRight = 0, + bottomLeft = 0, + minRadius = 2 + } = options; + + const calculateRadiusPercent = (height: number): number => { + const percent = -1.5 * height + 72.5; + return Math.max(30, Math.min(percent, 100)); + }; + + let tl, tr, br, bl; + + const dynamicMaxRadiusPercent = calculateRadiusPercent(height) / 100; + const maxRadius = height * dynamicMaxRadiusPercent; + + tl = topLeft === true ? maxRadius : (topLeft === 'small' ? minRadius : (topLeft || 0)); + tr = topRight === true ? maxRadius : (topRight === 'small' ? minRadius : (topRight || 0)); + br = bottomRight === true ? maxRadius : (bottomRight === 'small' ? minRadius : (bottomRight || 0)); + bl = bottomLeft === true ? maxRadius : (bottomLeft === 'small' ? minRadius : (bottomLeft || 0)); + + const maxR = Math.min(width / 2, height / 2); + tl = Math.min(tl, maxR); + tr = Math.min(tr, maxR); + br = Math.min(br, maxR); + bl = Math.min(bl, maxR); - cr.moveTo(x + (corners.topLeft ? radius : 0), y); + cr.moveTo(x + tl, y); // Top rigth - if (corners.topRight) { - cr.arc(x + width - radius, y + radius, radius, 1.5 * Math.PI, 2 * Math.PI); + if (tr > 0) { + cr.arc(x + width - tr, y + tr, tr, 1.5 * Math.PI, 2 * Math.PI); } else { cr.lineTo(x + width, y); - cr.lineTo(x + width, y + radius); } // Bottom rigth - if (corners.bottomRight) { - cr.arc(x + width - radius, y + height - radius, radius, 0, 0.5 * Math.PI); + if (br > 0) { + cr.arc(x + width - br, y + height - br, br, 0, 0.5 * Math.PI); } else { cr.lineTo(x + width, y + height); - cr.lineTo(x + width - radius, y + height); } // Bottom left - if (corners.bottomLeft) { - cr.arc(x + radius, y + height - radius, radius, 0.5 * Math.PI, Math.PI); + if (bl > 0) { + cr.arc(x + bl, y + height - bl, bl, 0.5 * Math.PI, Math.PI); } else { cr.lineTo(x, y + height); - cr.lineTo(x, y + height - radius); } // Top left - if (corners.topLeft) { - cr.arc(x + radius, y + radius, radius, Math.PI, 1.5 * Math.PI); + if (tl > 0) { + cr.arc(x + tl, y + tl, tl, Math.PI, 1.5 * Math.PI); } else { cr.lineTo(x, y); - cr.lineTo(x + radius, y); + cr.lineTo(x + tl, y); } cr.closePath(); } - -export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { +export function createUnifiedSlider(model: { + getValue(): number; + getMaxValue(): number; + setValue(value: number): void; + realtimeChangeValue(): boolean; + getColor(): string | (() => string | null); + typeSlider(): typeSliders; + getPlaybackStatus?(): AstalMpris.PlaybackStatus; + setSliderHeight?(): number; + extraSetup?(): () => { }; +}): Gtk.Widget { let isDragging = false; let dragProgress: number | null = null; @@ -76,7 +101,9 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { new Widget.DrawingArea({ className: "slider-drawing-area", hexpand: true, - heightRequest: 30, + heightRequest: model.setSliderHeight + ? (model.setSliderHeight() < 15) ? model.setSliderHeight() * 2 : model.setSliderHeight() * 1.5 + : 25, setup: (self) => { self.add_events( @@ -124,8 +151,8 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { self.connect('realize', () => { if (drawLoopId === null) { // 16 ms is about 60 fps, so if you want get more fps, use this formula: Math.round(1000 / RefreshRate) - const FrameToMilliseconds = 16; // ~60 fps - drawLoopId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, FrameToMilliseconds, () => { + const FrameInMilliseconds = 16; + drawLoopId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, FrameInMilliseconds, () => { if (self.get_window()?.is_visible()) { self.queue_draw(); } @@ -183,28 +210,28 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { switch (styleType) { case typeSliders.MATERIAL_EXPRESSIVE_SLIDER: { - const barHeight = 15; - const barRadius = Math.max(5, barHeight / 2); + const barHeight = model.setSliderHeight ? model.setSliderHeight() : 15; const centerY = height / 2; const progressX = width * displayProgress; const handleWidth = 5; - const handleHeight = Math.max(barHeight + handleWidth, Math.min(height - barHeight, barHeight + barHeight / 2)); + const handleHeight = Math.max(barHeight + handleWidth * 2, Math.min(height - barHeight, barHeight + barHeight / 2)); const handleX = Math.max(0, Math.min(progressX - handleWidth / 2, width - handleWidth)); const handleOffset = 5; const activeEndX = handleX - handleOffset; const inactiveStartX = handleX + handleWidth + handleOffset; + const colorToUse = colorStr ? color : fg; + // Active line if (displayProgress > 0 && activeEndX > 0) { - const colorToUse = colorStr ? color : fg; cr.setSourceRGBA(colorToUse.red, colorToUse.green, colorToUse.blue, colorToUse.alpha); drawRoundedRectangleCustom( cr, 0, centerY - barHeight / 2, - Math.max(0, activeEndX), barHeight, barRadius, - { topLeft: true, topRight: false, bottomRight: false, bottomLeft: true } + Math.max(0, activeEndX), barHeight, + { topLeft: true, topRight: 'small', bottomRight: 'small', bottomLeft: true } ); cr.fill(); } @@ -218,8 +245,8 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { drawRoundedRectangleCustom( cr, inactiveStartX, centerY - barHeight / 2, - inactiveWidth, barHeight, barRadius, - { topLeft: false, topRight: true, bottomRight: true, bottomLeft: false } + inactiveWidth, barHeight, + { topLeft: 'small', topRight: true, bottomRight: true, bottomLeft: 'small' } ); cr.fill(); } @@ -227,24 +254,28 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { // Handle cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); + //cr.setSourceRGBA(colorToUse.red, colorToUse.green, colorToUse.blue, colorToUse.alpha); drawRoundedRectangleCustom( cr, handleX, centerY - handleHeight / 2, - handleWidth, handleHeight, barRadius + handleWidth, handleHeight, + { topLeft: true, topRight: true, bottomRight: true, bottomLeft: true } ); cr.fill(); // Max Value Dot if (hasInactive) { - const dotRadius = barHeight / 6; - const dotX = width - barHeight / 2; - const colorToUse = colorStr ? color : fg; + //const dotRadius = barHeight / 6; + const dotRadius = 2.5; + //const dotX = width - barHeight / 2; + const dotX = width - dotRadius * 3; + //const colorToUse = colorStr ? color : fg; cr.save(); drawRoundedRectangleCustom( cr, inactiveStartX, centerY - barHeight / 2, - width - inactiveStartX, barHeight, barRadius, + width - inactiveStartX, barHeight, { topLeft: false, topRight: true, bottomRight: true, bottomLeft: false } ); cr.clip(); @@ -280,7 +311,7 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { cr.setLineWidth(lineThickness); cr.setLineCap(1); // ROUND caps - // Active part (wave or line) + // Active line (wave or line) if (activeEndX > startX) { const src = colorStr ? color : fg; cr.setSourceRGBA(src.red, src.green, src.blue, src.alpha); @@ -294,7 +325,7 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { cr.stroke(); } - // Unactive part + // Unactive line const inactiveStartX = Math.max(lineThickness * 2, Math.min(width, progressX + rectangleHandleWidth * 2)); if (inactiveStartX < width - startX) { cr.setSourceRGBA(fg.red, fg.green, fg.blue, 0.3); @@ -306,7 +337,14 @@ export function createUnifiedSlider(model: SliderOptions): Gtk.Widget { if (!isStreamPlaying) { cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); - drawRoundedRectangleCustom(cr, rectangleHandleX, centerY - rectangleHandleHeight / 2, rectangleHandleWidth, rectangleHandleHeight, radius); + drawRoundedRectangleCustom( + cr, + rectangleHandleX, + centerY - rectangleHandleHeight / 2, + rectangleHandleWidth, + rectangleHandleHeight, + { topLeft: true, topRight: true, bottomRight: true, bottomLeft: true } + ); cr.fill(); } break; From 5c93506ced6c83a526c55d165e48ec7680f2f1b8 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:02:30 +0300 Subject: [PATCH 59/64] Just polishing the code, nothing special --- ags/widget/assets/Slider.ts | 530 ++++++++++++++++++++---------------- 1 file changed, 290 insertions(+), 240 deletions(-) diff --git a/ags/widget/assets/Slider.ts b/ags/widget/assets/Slider.ts index 06b0d07c..56c107ed 100644 --- a/ags/widget/assets/Slider.ts +++ b/ags/widget/assets/Slider.ts @@ -80,13 +80,17 @@ function drawRoundedRectangleCustom(cr, x, y, width, height, options: { cr.closePath(); } -export function createUnifiedSlider(model: { +export function createSlider(model: { + // TO DO: + // ADD ICON IN SLIDER, + // ADD STEPS getValue(): number; getMaxValue(): number; setValue(value: number): void; - realtimeChangeValue(): boolean; getColor(): string | (() => string | null); typeSlider(): typeSliders; + iconName?(): string | (() => string | null); + realtimeChangeValue?(): boolean; getPlaybackStatus?(): AstalMpris.PlaybackStatus; setSliderHeight?(): number; extraSetup?(): () => { }; @@ -94,264 +98,310 @@ export function createUnifiedSlider(model: { let isDragging = false; let dragProgress: number | null = null; + const InRealTime = model.realtimeChangeValue ? model.realtimeChangeValue() : true; + const isStreamPlaying = model.getMaxValue() >= GLib.MAXINT64 / 10000000; - return new Widget.EventBox({ - children: [ - new Widget.DrawingArea({ - className: "slider-drawing-area", - hexpand: true, - heightRequest: model.setSliderHeight - ? (model.setSliderHeight() < 15) ? model.setSliderHeight() * 2 : model.setSliderHeight() * 1.5 - : 25, - - setup: (self) => { - self.add_events( - Gdk.EventMask.BUTTON_PRESS_MASK | - Gdk.EventMask.BUTTON_RELEASE_MASK | - Gdk.EventMask.POINTER_MOTION_MASK - ); + const iconSize = model.setSliderHeight ? model.setSliderHeight() - 4 : 11; + const iconNameRaw = model.iconName; + const iconName = typeof iconNameRaw === 'function' ? iconNameRaw() : iconNameRaw; + + const iconImage = iconName + ? new Gtk.Image({ + icon_name: iconName, + pixel_size: iconSize, + halign: Gtk.Align.START, + valign: Gtk.Align.CENTER, + margin_start: 14, + }) + : null; + + const drawingArea = new Widget.DrawingArea({ + className: "slider-drawing-area", + hexpand: true, + heightRequest: model.setSliderHeight + ? (model.setSliderHeight() < 15) ? model.setSliderHeight() * 2 : model.setSliderHeight() * 1.5 + : 25, + + setup: (self) => { + self.add_events( + Gdk.EventMask.BUTTON_PRESS_MASK | + Gdk.EventMask.BUTTON_RELEASE_MASK | + Gdk.EventMask.POINTER_MOTION_MASK + ); + + const updateDragPosition = (x: number) => { + const width = self.get_allocated_width(); + if (width === 0) return; + dragProgress = Math.max(0, Math.min(x / width, 1)); + }; + + // TO DO: + // ADD ANIMATION OF TRANSITION FROM END POSITION TO START POSITION, + // ADD CLASSIFICATION OF PRESSED BUTTON (PRIMARY & SECONDARY) + self.connect('button-press-event', (_, event) => { + if (isStreamPlaying) return; + isDragging = true; + const [, x] = event.get_coords(); + updateDragPosition(x); + }); + + self.connect('motion-notify-event', (_, event) => { + if (isDragging) { + const [, x] = event.get_coords(); + updateDragPosition(x); + + if (InRealTime && dragProgress !== null) model.setValue(dragProgress * model.getMaxValue()); + } + }); - const updateDragPosition = (x: number) => { - const width = self.get_allocated_width(); - if (width === 0) return; - dragProgress = Math.max(0, Math.min(x / width, 1)); - }; - - self.connect('button-press-event', (_, event) => { - if (isStreamPlaying) return; - isDragging = true; - const [, x] = event.get_coords(); - updateDragPosition(x); - }); + self.connect('button-release-event', () => { + if (isDragging && dragProgress !== null) { + const maxValue = model.getMaxValue(); + if (maxValue > 0) { + model.setValue(dragProgress * maxValue); + } - self.connect('motion-notify-event', (_, event) => { - if (isDragging) { - const [, x] = event.get_coords(); - updateDragPosition(x); + isDragging = model.getPlaybackStatus ? timeout(40, () => { isDragging = false }) : false; + } + }); - if (model.realtimeChangeValue() && dragProgress !== null) model.setValue(dragProgress * model.getMaxValue()); - } - }); - self.connect('button-release-event', () => { - if (isDragging && dragProgress !== null) { - const maxValue = model.getMaxValue(); - if (maxValue > 0) { - model.setValue(dragProgress * maxValue); - } + let drawLoopId: number | null = null; - isDragging = model.getPlaybackStatus ? timeout(40, () => { isDragging = false }) : false; + self.connect('realize', () => { + if (drawLoopId === null) { + // 16 ms is about 60 fps, so if you want get more fps, use this formula: Math.round(1000 / RefreshRate) + const FramesInMilliseconds = Math.round(1000 / Wallpaper.getDefault().getRefreshRate()); + drawLoopId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, FramesInMilliseconds, () => { + if (self.get_window()?.is_visible()) { + self.queue_draw(); } + return GLib.SOURCE_CONTINUE; }); - - - let drawLoopId: number | null = null; - - self.connect('realize', () => { - if (drawLoopId === null) { - // 16 ms is about 60 fps, so if you want get more fps, use this formula: Math.round(1000 / RefreshRate) - const FrameInMilliseconds = 16; - drawLoopId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, FrameInMilliseconds, () => { - if (self.get_window()?.is_visible()) { - self.queue_draw(); - } - return GLib.SOURCE_CONTINUE; - }); + } + }); + + self.connect('draw', (self) => { + if (model.typeSlider() === typeSliders.MATERIAL_EXPRESSIVE_WAVE) { + const playbackStatus = model.getPlaybackStatus ? model.getPlaybackStatus() : null; + if (playbackStatus === AstalMpris.PlaybackStatus.PLAYING && !isDragging) { + if (pauseProgress > 0) { + pauseProgress = Math.max(0, pauseProgress - 0.03); } - }); - - self.connect('draw', (self) => { - if (model.typeSlider() === typeSliders.MATERIAL_EXPRESSIVE_WAVE) { - const playbackStatus = model.getPlaybackStatus ? model.getPlaybackStatus() : null; - - if (playbackStatus === AstalMpris.PlaybackStatus.PLAYING) { - if (pauseProgress > 0) { - pauseProgress = Math.max(0, pauseProgress - 0.03); - } - } else { - if (pauseProgress < 1) { - pauseProgress = Math.min(1, pauseProgress + 0.03); - } - } + } else { + if (pauseProgress < 1) { + pauseProgress = Math.min(1, pauseProgress + 0.03); } - }); + } + } + }); - self.connect("destroy", () => { - if (drawLoopId !== null) { - GLib.source_remove(drawLoopId); - drawLoopId = null; - } - }); - }, - - onDraw: (self, cr) => { - const styleContext = self.get_style_context(); - const width = self.get_allocated_width(); - const height = self.get_allocated_height(); - - const position = model.getValue(); - const length = model.getMaxValue(); - - const currentProgress = length > 0 ? position / length : 0; - const displayProgress = (isDragging && dragProgress !== null) - ? dragProgress - : (!isStreamPlaying - ? currentProgress - : 1 //Max + self.connect("destroy", () => { + if (drawLoopId !== null) { + GLib.source_remove(drawLoopId); + drawLoopId = null; + } + }); + }, + + onDraw: (self, cr) => { + const styleContext = self.get_style_context(); + const width = self.get_allocated_width(); + const height = self.get_allocated_height(); + + const position = model.getValue(); + const length = model.getMaxValue(); + + const currentProgress = length > 0 ? position / length : 0; + const displayProgress = (isDragging && dragProgress !== null) + ? dragProgress + : (!isStreamPlaying + ? currentProgress + : 1); //1 - Max + + const styleType = model.typeSlider(); + const fg = styleContext.get_property('color', Gtk.StateFlags.NORMAL); + const rawColor = model.getColor; + const colorStr = typeof rawColor === 'function' ? rawColor() : rawColor; + const color = new Gdk.RGBA(); + if (colorStr) color.parse(colorStr); + + switch (styleType) { + case typeSliders.MATERIAL_EXPRESSIVE_SLIDER: { + const barHeight = model.setSliderHeight ? model.setSliderHeight() : 15; + const centerY = height / 2; + const progressX = width * displayProgress; + + const handleWidth = 5; + const handleHeight = Math.max(barHeight + handleWidth * 2, Math.min(height - barHeight, barHeight + barHeight / 2)); + const handleX = Math.max(0, Math.min(progressX - handleWidth / 2, width - handleWidth)); + const handleOffset = 5; + const activeEndX = handleX - handleOffset; + const inactiveStartX = handleX + handleWidth + handleOffset; + + const rawIconName = model.iconName; + const iconName = typeof rawIconName === 'function' ? rawIconName() : rawIconName; + const colorToUse = colorStr ? color : fg; + + // Mask + cr.save(); + drawRoundedRectangleCustom( + cr, + 0, centerY - barHeight / 2, + width, barHeight, + { topLeft: true, topRight: true, bottomRight: true, bottomLeft: true } + ); + cr.clip(); + + // Active line + if (displayProgress > 0 && activeEndX > 0) { + cr.setSourceRGBA(colorToUse.red, colorToUse.green, colorToUse.blue, colorToUse.alpha); + drawRoundedRectangleCustom( + cr, + 0, centerY - barHeight / 2, + Math.max(0, activeEndX), barHeight, + { topLeft: true, topRight: 'small', bottomLeft: true, bottomRight: 'small' } ); + cr.fill(); + } - const styleType = model.typeSlider(); - const fg = styleContext.get_property('color', Gtk.StateFlags.NORMAL); - const rawColor = model.getColor; - const colorStr = typeof rawColor === 'function' ? rawColor() : rawColor; - const color = new Gdk.RGBA(); - if (colorStr) color.parse(colorStr); - - switch (styleType) { - case typeSliders.MATERIAL_EXPRESSIVE_SLIDER: { - const barHeight = model.setSliderHeight ? model.setSliderHeight() : 15; - const centerY = height / 2; - const progressX = width * displayProgress; - - const handleWidth = 5; - const handleHeight = Math.max(barHeight + handleWidth * 2, Math.min(height - barHeight, barHeight + barHeight / 2)); - const handleX = Math.max(0, Math.min(progressX - handleWidth / 2, width - handleWidth)); - - const handleOffset = 5; - const activeEndX = handleX - handleOffset; - const inactiveStartX = handleX + handleWidth + handleOffset; - - const colorToUse = colorStr ? color : fg; - - // Active line - if (displayProgress > 0 && activeEndX > 0) { - cr.setSourceRGBA(colorToUse.red, colorToUse.green, colorToUse.blue, colorToUse.alpha); - drawRoundedRectangleCustom( - cr, - 0, centerY - barHeight / 2, - Math.max(0, activeEndX), barHeight, - { topLeft: true, topRight: 'small', bottomRight: 'small', bottomLeft: true } - ); - cr.fill(); - } - - // Unactive line - const hasInactive = displayProgress < 1 && inactiveStartX < width; - if (hasInactive) { - const inactiveWidth = width - inactiveStartX; - if (inactiveWidth > 0) { - cr.setSourceRGBA(fg.red, fg.green, fg.blue, 0.3); - drawRoundedRectangleCustom( - cr, - inactiveStartX, centerY - barHeight / 2, - inactiveWidth, barHeight, - { topLeft: 'small', topRight: true, bottomRight: true, bottomLeft: 'small' } - ); - cr.fill(); - } - } - - // Handle - cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); - //cr.setSourceRGBA(colorToUse.red, colorToUse.green, colorToUse.blue, colorToUse.alpha); + // Unactive line + const hasInactive = displayProgress < 1 && inactiveStartX < width; + if (hasInactive) { + const inactiveWidth = width - inactiveStartX; + if (inactiveWidth > 0) { + cr.setSourceRGBA(fg.red, fg.green, fg.blue, 0.3); drawRoundedRectangleCustom( - cr, - handleX, centerY - handleHeight / 2, - handleWidth, handleHeight, - { topLeft: true, topRight: true, bottomRight: true, bottomLeft: true } + cr, + inactiveStartX, centerY - barHeight / 2, + inactiveWidth, barHeight, + { topLeft: 'small', topRight: true, bottomLeft: 'small', bottomRight: true } ); cr.fill(); - - // Max Value Dot - if (hasInactive) { - //const dotRadius = barHeight / 6; - const dotRadius = 2.5; - //const dotX = width - barHeight / 2; - const dotX = width - dotRadius * 3; - //const colorToUse = colorStr ? color : fg; - - cr.save(); - drawRoundedRectangleCustom( - cr, - inactiveStartX, centerY - barHeight / 2, - width - inactiveStartX, barHeight, - { topLeft: false, topRight: true, bottomRight: true, bottomLeft: false } - ); - cr.clip(); - - cr.setSourceRGBA(colorToUse.red, colorToUse.green, colorToUse.blue, colorToUse.alpha); - cr.newPath(); - cr.arc(dotX, centerY, dotRadius, 0, 2 * Math.PI); - cr.fill(); - cr.restore(); - } - - break; } - case typeSliders.MATERIAL_EXPRESSIVE_WAVE: { - const lineThickness = 6; + } + + // Icon + // Hm... + // if (iconName) { + // const iconSize = barHeight - 4; + // const iconPadding = 14; + // let pixbuf; + // try { + // pixbuf = Gtk.IconTheme.get_default().load_icon(iconName, iconSize, Gtk.IconLookupFlags.FORCE_SIZE); + // if (pixbuf) { + // Gdk.cairo_set_source_pixbuf(cr, pixbuf, iconPadding, centerY - iconSize / 2); + // cr.paint(); + // } + // } catch (e) { + // console.log(`Failed to load icon '${iconName}': ${e}`); + // } + // } + + // Max Value Dot + if (hasInactive) { + //const dotRadius = barHeight / 6; + const dotRadius = 2.5; + //const dotX = width - barHeight / 2; + const dotX = width - dotRadius * 2; + //const colorToUse = colorStr ? color : fg; + + cr.save(); + drawRoundedRectangleCustom( + cr, + inactiveStartX, centerY - barHeight / 2, + width - inactiveStartX, barHeight, + { topLeft: false, topRight: true, bottomRight: true, bottomLeft: false } + ); + cr.clip(); + + cr.setSourceRGBA(colorToUse.red, colorToUse.green, colorToUse.blue, colorToUse.alpha); + cr.copyPath(); + cr.arc(dotX, centerY, dotRadius, 0, 2 * Math.PI); + cr.fill(); + cr.restore(); + } + + // Drop the mask + cr.restore(); + + // Draw on top of all surfaces + cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); + drawRoundedRectangleCustom( + cr, + handleX, centerY - handleHeight / 2, + handleWidth, handleHeight, + { topLeft: true, topRight: true, bottomRight: true, bottomLeft: true } + ); + cr.fill(); + + break; + } + case typeSliders.MATERIAL_EXPRESSIVE_WAVE: { + const lineThickness = 6; - const baseWaveAmp = 3; - const waveAmp = baseWaveAmp * (1 - pauseProgress); - const waveFreq = 0.15; - const time = Date.now() * 0.003; - - const rectangleHandleWidth = 5; - const rectangleHandleHeight = 20; - const rectangleHandleX = Math.max(0, Math.min(width * displayProgress - (rectangleHandleWidth / 2), width - rectangleHandleWidth)); - - const centerY = height / 2; - const progressX = width * displayProgress; - const startX = lineThickness / 2; - const activeEndX = Math.max(startX, progressX - rectangleHandleWidth * 2); - - const radius = 10; - - cr.setLineWidth(lineThickness); - cr.setLineCap(1); // ROUND caps - - // Active line (wave or line) - if (activeEndX > startX) { - const src = colorStr ? color : fg; - cr.setSourceRGBA(src.red, src.green, src.blue, src.alpha); - - cr.newPath(); - cr.moveTo(startX, centerY + Math.sin(startX * waveFreq + time) * waveAmp); - for (let x = startX; x < activeEndX; x++) { - const y = centerY + Math.sin(x * waveFreq + time) * waveAmp; - cr.lineTo(x, y); - } - cr.stroke(); - } - - // Unactive line - const inactiveStartX = Math.max(lineThickness * 2, Math.min(width, progressX + rectangleHandleWidth * 2)); - if (inactiveStartX < width - startX) { - cr.setSourceRGBA(fg.red, fg.green, fg.blue, 0.3); - cr.newPath(); - cr.moveTo(inactiveStartX, centerY); - cr.lineTo(width - startX, centerY); - cr.stroke(); - } - - if (!isStreamPlaying) { - cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); - drawRoundedRectangleCustom( - cr, - rectangleHandleX, - centerY - rectangleHandleHeight / 2, - rectangleHandleWidth, - rectangleHandleHeight, - { topLeft: true, topRight: true, bottomRight: true, bottomLeft: true } - ); - cr.fill(); - } - break; + const baseWaveAmp = 3; + const waveAmp = baseWaveAmp * (1 - pauseProgress); + const waveFreq = 0.15; + const time = Date.now() * 0.003; + + const rectangleHandleWidth = 5; + const rectangleHandleHeight = 20; + const rectangleHandleX = Math.max(0, Math.min(width * displayProgress - (rectangleHandleWidth / 2), width - rectangleHandleWidth)); + + const centerY = height / 2; + const progressX = width * displayProgress; + const startX = lineThickness / 2; + const activeEndX = Math.max(startX, progressX - rectangleHandleWidth * 2); + + const radius = 10; + + cr.setLineWidth(lineThickness); + cr.setLineCap(1); // ROUND caps + + // Active line (wave or line) + if (activeEndX > startX) { + const src = colorStr ? color : fg; + cr.setSourceRGBA(src.red, src.green, src.blue, src.alpha); + + cr.newPath(); + cr.moveTo(startX, centerY + Math.sin(startX * waveFreq + time) * waveAmp); + for (let x = startX; x < activeEndX; x++) { + const y = centerY + Math.sin(x * waveFreq + time) * waveAmp; + cr.lineTo(x, y); } + cr.stroke(); + } + + // Unactive line + const inactiveStartX = Math.max(lineThickness * 2, Math.min(width, progressX + rectangleHandleWidth * 2)); + if (inactiveStartX < width - startX) { + cr.setSourceRGBA(fg.red, fg.green, fg.blue, 0.3); + cr.newPath(); + cr.moveTo(inactiveStartX, centerY); + cr.lineTo(width - startX, centerY); + cr.stroke(); } + + // Handle + if (!isStreamPlaying) { + cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha); + drawRoundedRectangleCustom( + cr, + rectangleHandleX, + centerY - rectangleHandleHeight / 2, + rectangleHandleWidth, + rectangleHandleHeight, + { topLeft: true, topRight: true, bottomRight: true, bottomLeft: true } + ); + cr.fill(); + } + break; } - } as Widget.DrawingAreaProps) - ] - } as Widget.EventBoxProps) + } + } + } as Widget.DrawingAreaProps) + + return drawingArea; } From 2a3c8bf053e995abd61884020a674c296e31a281 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Mon, 28 Jul 2025 18:05:01 +0300 Subject: [PATCH 60/64] Ops.. --- ags/widget/assets/Slider.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ags/widget/assets/Slider.ts b/ags/widget/assets/Slider.ts index 56c107ed..b35cff42 100644 --- a/ags/widget/assets/Slider.ts +++ b/ags/widget/assets/Slider.ts @@ -1,7 +1,6 @@ import { timeout, GLib } from 'astal'; import { Gtk, Gdk, Widget } from 'astal/gtk3'; import AstalMpris from "gi://AstalMpris"; -import { Wallpaper } from '../../scripts/wallpaper'; let pauseProgress = 1; // 0 = full wave, 1 = full straight @@ -171,8 +170,8 @@ export function createSlider(model: { self.connect('realize', () => { if (drawLoopId === null) { - // 16 ms is about 60 fps, so if you want get more fps, use this formula: Math.round(1000 / RefreshRate) - const FramesInMilliseconds = Math.round(1000 / Wallpaper.getDefault().getRefreshRate()); + const fps = 60; + const FramesInMilliseconds = Math.round(1000 / fps); drawLoopId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, FramesInMilliseconds, () => { if (self.get_window()?.is_visible()) { self.queue_draw(); From 43d34b7295b4b168fe61780e927357f1811c8397 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sun, 3 Aug 2025 11:09:29 +0300 Subject: [PATCH 61/64] Updated the widget creation logic Hooks are used instead of connections for stable operation. Now the widget is created only once and changes as needed. --- ags/widget/bar/Media.ts | 158 +++++++++++++++++++--------------------- 1 file changed, 75 insertions(+), 83 deletions(-) diff --git a/ags/widget/bar/Media.ts b/ags/widget/bar/Media.ts index 73665b72..a5115b4f 100644 --- a/ags/widget/bar/Media.ts +++ b/ags/widget/bar/Media.ts @@ -1,7 +1,7 @@ -import { bind, exec } from "astal"; +import { Variable, bind, exec } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import AstalMpris from "gi://AstalMpris"; -import { getAppIcon, getIconByAppName, getSymbolicIcon } from "../../scripts/apps"; +import { getSymbolicIcon } from "../../scripts/apps"; import { Separator, SeparatorProps } from "../Separator"; import { Windows } from "../../windows"; import { Clipboard } from "../../scripts/clipboard"; @@ -9,8 +9,8 @@ import { Clipboard } from "../../scripts/clipboard"; import { AstalPlayers } from "../../scripts/player"; export function Media(): Gtk.Widget { - - const connections: Array = []; + + const players = AstalPlayers.getDefault(); const mediaControlsRevealer: Widget.Revealer = new Widget.Revealer({ transitionType: Gtk.RevealerTransitionType.SLIDE_RIGHT, @@ -20,121 +20,113 @@ export function Media(): Gtk.Widget { className: "media-controls button-row", expand: false, homogeneous: false, - children: bind(AstalPlayers.getDefault(), "activePlayer").as((activePlayer: AstalMpris.Player) => - activePlayer ? [ - new Widget.Button({ - className: "link", - image: new Widget.Icon({ - icon: "edit-paste-symbolic" - } as Widget.IconProps), - tooltipText: "Copy link to Clipboard", - // AstalMpris.Player.metadata works only sometimes, so I'm not using it - visible: bind(activePlayer, "metadata").as(Boolean), - onClick: async () => { - const link = exec(`playerctl --player=${ - activePlayer.busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") - } metadata xesam:url`); - link && Clipboard.getDefault().copyAsync(link); - } - } as Widget.ButtonProps), - new Widget.Button({ - className: "previous", - image: new Widget.Icon({ - icon: "media-skip-backward-symbolic" - } as Widget.IconProps), - tooltipText: "Previous", - onClick: () => activePlayer.canGoPrevious && activePlayer.previous() - } as Widget.ButtonProps), - new Widget.Button({ - className: "play-pause", - tooltipText: bind(activePlayer, "playback_status").as((status) => - status === AstalMpris.PlaybackStatus.PLAYING ? - "Pause" - : "Play"), - image: new Widget.Icon({ - icon: bind(activePlayer, "playbackStatus").as((status: AstalMpris.PlaybackStatus) => - status === AstalMpris.PlaybackStatus.PLAYING ? - "media-playback-pause-symbolic" - : "media-playback-start-symbolic") - } as Widget.IconProps), - onClick: () => activePlayer.playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? - activePlayer.play() - : activePlayer.pause() - } as Widget.ButtonProps), - new Widget.Button({ - className: "next", - image: new Widget.Icon({ - icon: "media-skip-forward-symbolic" - } as Widget.IconProps), - tooltipText: "Next", - onClick: () => activePlayer.canGoNext && activePlayer.next() - } as Widget.ButtonProps) - ] : new Widget.Label({ - label: "Don't Stop The Music!" - } as Widget.LabelProps) - ) + children: [ + // new Widget.Button({ + // className: "link", + // image: new Widget.Icon({ + // icon: "edit-paste-symbolic" + // } as Widget.IconProps), + // tooltipText: "Copy link to Clipboard", + // // AstalMpris.Player.metadata works only sometimes, so I'm not using it + // visible: bind(players, "activePlayer").as((p: AstalMpris.Player) => p.metadata ? true : false), + // onClickRelease: async () => { + // const link = exec(`playerctl --player=${ + // players.activePlayer.busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") + // } metadata xesam:url`); + // link && Clipboard.getDefault().copyAsync(link); + // } + // } as Widget.ButtonProps), + new Widget.Button({ + className: "previous", + visible: bind(players, "activePlayer").as((p: AstalMpris.Player) => p.canGoPrevious ? true : false), + image: new Widget.Icon({ + icon: "media-skip-backward-symbolic" + } as Widget.IconProps), + tooltipText: "Previous", + onClickRelease: () => players.activePlayer.canGoPrevious && players.activePlayer.previous() + } as Widget.ButtonProps), + new Widget.Button({ + className: "play-pause", + tooltipText: bind(players, "activePlayer").as((p: AstalMpris.Player) => + p?.playback_status === AstalMpris.PlaybackStatus.PLAYING + ? "Pause" : "Play"), + image: new Widget.Icon({ + icon: bind(players, "activePlayer").as((p: AstalMpris.Player) => + p?.playbackStatus === AstalMpris.PlaybackStatus.PLAYING ? + "media-playback-pause-symbolic" + : "media-playback-start-symbolic") + } as Widget.IconProps), + onClickRelease: () => players.activePlayer.playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? + players.activePlayer.play() + : players.activePlayer.pause() + } as Widget.ButtonProps), + new Widget.Button({ + className: "next", + visible: bind(players, "activePlayer").as((p: AstalMpris.Player) => p.canGoNext ? true : false), + image: new Widget.Icon({ + icon: "media-skip-forward-symbolic" + } as Widget.IconProps), + tooltipText: "Next", + onClickRelease: () => players.activePlayer.canGoNext && players.activePlayer.next() + } as Widget.ButtonProps) + ] } as Widget.BoxProps) } as Widget.RevealerProps); const mediaWidget = new Widget.EventBox({ className: "media-eventbox", - visible: bind(AstalPlayers.getDefault(), "activePlayer").as((activePlayer: AstalMpris.Player) => + visible: bind(players, "activePlayer").as((activePlayer: AstalMpris.Player) => activePlayer && activePlayer.get_available()), - onDestroy: (_) => connections.map(id => _.disconnect(id)), onClick: () => Windows.toggle("center-window"), child: new Widget.Box({ - className: "media", + className: "media", children: [ new Widget.Box({ spacing: 4, - children: bind(AstalPlayers.getDefault(), "activePlayer").as((activePlayer: AstalMpris.Player) => - activePlayer ? [ + children: [ new Widget.Icon({ - icon: getSymbolicIcon(activePlayer.get_entry()) ?? - getSymbolicIcon(activePlayer.get_bus_name().split('.').filter(str => !str.toLowerCase().includes('instance')).join('.')) ?? - "folder-music-symbolic" + icon: bind(players, "activePlayer").as((p: AstalMpris.Player) => getSymbolicIcon(p.get_entry()) ?? + getSymbolicIcon(p.get_bus_name().split('.').filter(str => !str.toLowerCase().includes('instance')).join('.')) ?? + "folder-music-symbolic") } as Widget.IconProps), new Widget.Label({ className: "title", - label: bind(activePlayer, "title").as((title: string) => title || "No Title"), + label: bind(players, "activePlayer").as((p: AstalMpris.Player) => p?.title ?? "No Title"), maxWidthChars: 20, truncate: true } as Widget.LabelProps), Separator({ - visible: bind(activePlayer, "artist").as(artist => artist ? true : false), + visible: bind(players, "activePlayer").as((p: AstalMpris.Player) => p?.artist ? true : false), orientation: Gtk.Orientation.HORIZONTAL, size: 1, margin: 5, - //cssColor: `rgb(180, 180, 180)`, alpha: .3 } as SeparatorProps), new Widget.Label({ className: "artist", - visible: bind(activePlayer, "artist").as(artist => artist ? true : false), - label: bind(activePlayer, "artist").as((artist: string) => artist), + visible: bind(players, "activePlayer").as((p: AstalMpris.Player) => p?.artist ? true : false), + label: bind(players, "activePlayer").as((p: AstalMpris.Player) => p?.artist ?? "No Artist"), maxWidthChars: 18, - truncate: true, + truncate: true } as Widget.LabelProps) - ] : new Widget.Label({ - label: "Crazy to think this widget haven't disappeared yet!" - } as Widget.LabelProps) - ) + ] } as Widget.BoxProps), mediaControlsRevealer ] } as Widget.BoxProps) } as Widget.EventBoxProps); - connections.push( - mediaWidget.connect("hover", () => { + mediaWidget.hook(mediaWidget, 'hover', () => { + if (!Windows.isVisible("center-window")) { mediaControlsRevealer.set_reveal_child(true); mediaWidget.className = mediaWidget.className + " reveal"; - }), - mediaWidget.connect("hover-lost", (_) => { - mediaControlsRevealer.set_reveal_child(false); - _.className = mediaWidget.className.replaceAll(" reveal", ""); - }) - ); + } + }); + + mediaWidget.hook(mediaWidget, 'hover-lost', (_) => { + mediaControlsRevealer.set_reveal_child(false); + _.className = mediaWidget.className.replaceAll(" reveal", ""); + }); return mediaWidget; } From d931d2bae545ab2102a49f68397045d4ea4b750c Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sun, 3 Aug 2025 11:16:28 +0300 Subject: [PATCH 62/64] Updated the widget creation logic Now the widget is created only once and changes as needed. --- ags/widget/center-window/BigMedia.ts | 346 +++++++++++++-------------- 1 file changed, 169 insertions(+), 177 deletions(-) diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts index 24c0f3d0..2dc4b257 100644 --- a/ags/widget/center-window/BigMedia.ts +++ b/ags/widget/center-window/BigMedia.ts @@ -2,8 +2,9 @@ import { AstalIO, bind, Binding, exec, timeout, GLib } from "astal"; import { Gtk, Widget } from "astal/gtk3"; import AstalMpris from "gi://AstalMpris"; import { AstalPlayers } from "../../scripts/player"; -import { Stylesheet } from "../../scripts/stylesheet"; -import { createUnifiedSlider } from "./Slider"; +import { createSlider, typeSliders } from "../assets/Slider"; +import { Wallpaper } from "../../scripts/wallpaper"; +import { ProgressBar } from "../assets/ProgressBar"; function formatTime(seconds: number): string { if (isNaN(seconds) || seconds < 0) return "0:00"; @@ -22,190 +23,181 @@ function formatTime(seconds: number): string { } export function BigMedia(): Gtk.Widget { + const players = AstalPlayers.getDefault(); + + const playerSlider = createSlider({ + getValue: () => players.activePlayer?.get_position() ?? 0, + getMaxValue: () => players.activePlayer?.get_length() ?? 0, + setValue: (value) => players.activePlayer?.set_position(value), + getPlaybackStatus: () => players.activePlayer?.playback_status, + getColor: () => "#4285F4", + realtimeChangeValue: () => false, + typeSlider: () => typeSliders.MATERIAL_EXPRESSIVE_WAVE + }); + return new Widget.Box({ className: "big-media", orientation: Gtk.Orientation.VERTICAL, homogeneous: false, width_request: 250, - visible: bind(AstalPlayers.getDefault(), "activePlayer").as((activePlayer: AstalMpris.Player) => - activePlayer ? true : false), - children: bind(AstalPlayers.getDefault(), "activePlayer").as((activePlayer: AstalMpris.Player) => - activePlayer && [ - new Widget.Box({ - halign: Gtk.Align.CENTER, - child: new Widget.Box({ - className: "image", - hexpand: false, - orientation: Gtk.Orientation.VERTICAL, - marginTop: 6, - visible: bind(activePlayer, "coverArt").as(Boolean), - css: bind(activePlayer, "coverArt").as((artUrl: string|undefined) => - artUrl ? `.image { background-image: url('${artUrl}'); }` : undefined), - width_request: 132, - height_request: 128 - } as Widget.BoxProps) - } as Widget.BoxProps), - new Widget.Box({ - className: "info", + visible: bind(players, "activePlayer").as(Boolean), + children: [ + new Widget.Box({ + halign: Gtk.Align.CENTER, + child: new Widget.Box({ + className: "image", + hexpand: false, orientation: Gtk.Orientation.VERTICAL, - vexpand: true, - valign: Gtk.Align.CENTER, - children: [ - new Widget.Label({ - className: "title", - tooltipText: bind(activePlayer, "title").as((title: string) => !title ? "No Title" : title), - label: bind(activePlayer, "title").as((title: string) => !title ? "No Title" : title), - truncate: true, - maxWidthChars: 25, - } as Widget.LabelProps), - new Widget.Label({ - className: "artist", - tooltipText: bind(activePlayer, "artist").as((artist: string) => !artist ? (activePlayer.get_identity() ?? "No Artist") : artist), - label: bind(activePlayer, "artist").as((artist: string) => !artist ? (activePlayer.get_identity() ?? "No Artist") : artist), - maxWidthChars: 28, - truncate: true, - } as Widget.LabelProps) - ] - } as Widget.BoxProps), - new Widget.Box({ - className: "progress", - hexpand: true, - visible: bind(activePlayer, "canSeek"), - children: [ - createUnifiedSlider({ - getValue: () => activePlayer.get_position(), - getMaxValue: () => activePlayer.get_length(), - setValue: (value) => activePlayer.set_position(value), - getPlaybackStatus: () => activePlayer.playback_status, - realtimeChangeValue: () => false - }) - ] + marginTop: 6, + visible: bind(players, "activePlayer").as(p => p && p.cover_art), + css: bind(players, "activePlayer").as(p => + p?.cover_art ? `.image { background-image: url('${p.cover_art}'); }` : undefined), + width_request: 132, + height_request: 128 + }) + }), + new Widget.Box({ + className: "info", + orientation: Gtk.Orientation.VERTICAL, + vexpand: true, + valign: Gtk.Align.CENTER, + children: [ + new Widget.Label({ + className: "title", + tooltipText: bind(players, "activePlayer").as(p => p?.title ?? "No Title"), + label: bind(players, "activePlayer").as(p => p?.title ?? "No Title"), + truncate: true, + maxWidthChars: 25 + }), + new Widget.Label({ + className: "artist", + tooltipText: bind(players, "activePlayer").as(p => p?.artist ?? p?.get_identity() ?? "No Artist"), + label: bind(players, "activePlayer").as(p => p?.artist ?? p?.get_identity() ?? "No Artist"), + maxWidthChars: 28, + truncate: true + }) + ] + }), + new Widget.Box({ + className: "progress", + hexpand: true, + visible: bind(players, "activePlayer").as(p => p?.can_seek ?? false), + children: [ playerSlider ] + }), + new Widget.CenterBox({ + className: "bottom", + homogeneous: false, + hexpand: true, + marginBottom: 6, + startWidget: new Widget.Label({ + className: "elapsed", + valign: Gtk.Align.START, + halign: Gtk.Align.START, + label: bind(players.activePlayer, "position").as(p => { + const pos = p ?? 0; + const len = players.activePlayer.length ?? 0; + return formatTime(pos > 0 && len > 0 ? pos : 0); + }) }), - new Widget.CenterBox({ - className: "bottom", - homogeneous: false, - hexpand: true, - marginBottom: 6, - startWidget: new Widget.Label({ - className: "elapsed", - valign: Gtk.Align.START, - halign: Gtk.Align.START, - label: bind(activePlayer, "position").as((pos: number) => { - return formatTime(pos > 0 && activePlayer.length > 0 ? pos : 0); - }) - } as Widget.LabelProps), - centerWidget: new Widget.Box({ - className: "controls button-row", - children: [ - new Widget.Button({ - className: "link", - image: new Widget.Icon({ - icon: "edit-paste-symbolic" - } as Widget.IconProps), - tooltipText: "Copy link to Clipboard", - visible: bind(activePlayer, "metadata").as(Boolean), - onClickRelease: async () => { - const link = exec(`playerctl --player=${ - activePlayer.busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") - } metadata xesam:url`); - link && Clipboard.getDefault().copyAsync(link); - } - } as Widget.ButtonProps), - new Widget.Button({ - className: "shuffle", - visible: bind(activePlayer, "shuffleStatus").as((shuffleStatus) => - shuffleStatus !== AstalMpris.Shuffle.UNSUPPORTED), - image: new Widget.Icon({ - icon: bind(activePlayer, "shuffleStatus").as((shuffleStatus) => - shuffleStatus === AstalMpris.Shuffle.ON ? - "media-playlist-shuffle-symbolic" - : "media-playlist-consecutive-symbolic") - } as Widget.IconProps), - tooltipText: bind(activePlayer, "shuffleStatus").as((shuffleStatus) => - shuffleStatus === AstalMpris.Shuffle.ON ? - "Shuffle" - : "No shuffle"), - onClickRelease: () => activePlayer.shuffle() - } as Widget.ButtonProps), - new Widget.Button({ - className: "previous", - image: new Widget.Icon({ - icon: "media-skip-backward-symbolic" - } as Widget.IconProps), - tooltipText: "Previous", - onClickRelease: () => activePlayer.canGoPrevious && activePlayer.previous() - } as Widget.ButtonProps), - new Widget.Button({ - className: "pause", - tooltipText: bind(activePlayer, "playback_status").as((status) => - status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play"), - image: new Widget.Icon({ - icon: bind(activePlayer, "playbackStatus").as((status) => - status === AstalMpris.PlaybackStatus.PLAYING ? - "media-playback-pause-symbolic" - : "media-playback-start-symbolic"), - } as Widget.IconProps), - onClickRelease: () => activePlayer.playbackStatus === AstalMpris.PlaybackStatus.PAUSED ? - activePlayer.play() - : activePlayer.pause() - } as Widget.ButtonProps), - new Widget.Button({ - className: "next", - image: new Widget.Icon({ - icon: "media-skip-forward-symbolic" - } as Widget.IconProps), - tooltipText: "Next", - onClickRelease: () => activePlayer.canGoNext && activePlayer.next() - } as Widget.ButtonProps), - new Widget.Button({ - className: "repeat", - visible: bind(activePlayer, "loopStatus").as((loopStatus) => - loopStatus !== AstalMpris.Loop.UNSUPPORTED), - image: new Widget.Icon({ - icon: bind(activePlayer, "loopStatus").as((loopStatus) => { - switch(loopStatus) { - case AstalMpris.Loop.TRACK: - return "media-playlist-repeat-song-symbolic"; - - case AstalMpris.Loop.PLAYLIST: - return "media-playlist-repeat-symbolic"; - } - - return "loop-arrow-symbolic"; - }) - } as Widget.IconProps), - tooltipText: bind(activePlayer, "loopStatus").as((loopStatus) => { - switch(loopStatus) { + centerWidget: new Widget.Box({ + className: "controls button-row", + children: [ + // new Widget.Button({ + // className: "link", + // image: new Widget.Icon({ + // icon: "edit-paste-symbolic" + // } as Widget.IconProps), + // tooltipText: "Copy link to Clipboard", + // visible: bind(players, "activePlayer").as((p: AstalMpris.Player) => p.metadata ? true : false), + // onClickRelease: async () => { + // const link = exec(`playerctl --player=${ + // players.activePlayer.busName.replace(/^org\.mpris\.MediaPlayer2\./i, "") + // } metadata xesam:url`); + // link && Clipboard.getDefault().copyAsync(link); + // } + // } as Widget.ButtonProps), + new Widget.Button({ + className: "shuffle", + visible: bind(players, "activePlayer").as((p: AstalMpris.Player) => + p?.shuffleStatus !== AstalMpris.Shuffle.UNSUPPORTED), + image: new Widget.Icon({ + icon: bind(players, "activePlayer").as((p: AstalMpris.Player) => + p?.shuffleStatus === AstalMpris.Shuffle.ON ? + "media-playlist-shuffle-symbolic" + : "media-playlist-consecutive-symbolic") + } as Widget.IconProps), + tooltipText: bind(players, "activePlayer").as((p: AstalMpris.Player) => + p?.shuffleStatus === AstalMpris.Shuffle.ON ? + "Shuffle" + : "No shuffle"), + onClickRelease: () => players.activePlayer.shuffle() + } as Widget.ButtonProps), + new Widget.Button({ + className: "previous", + visible: bind(players, "activePlayer").as((p: AstalMpris.Player) => p.canGoPrevious ? true : false), + image: new Widget.Icon({ icon: "media-skip-backward-symbolic" }), + tooltipText: "Previous", + onClickRelease: () => players.activePlayer.canGoPrevious && players.activePlayer.previous() + }), + new Widget.Button({ + className: "play-pause", + tooltipText: bind(players, "activePlayer").as((p: AstalMpris.Player) => + p.playback_status === AstalMpris.PlaybackStatus.PLAYING + ? "Pause" : "Play"), + image: new Widget.Icon({ + icon: bind(players, "activePlayer").as((p: AstalMpris.Player) => + p.playback_status === AstalMpris.PlaybackStatus.PLAYING + ? "media-playback-pause-symbolic" : "media-playback-start-symbolic"), + }), + onClickRelease: () => players.activePlayer.play_pause() + }), + new Widget.Button({ + className: "next", + image: new Widget.Icon({ icon: "media-skip-forward-symbolic" }), + tooltipText: "Next", + visible: bind(players, "activePlayer").as((p: AstalMpris.Player) => p?.can_go_next ? true : false), + onClickRelease: () => players.activePlayer.can_go_next && players.activePlayer.next() + }), + new Widget.Button({ + className: "repeat", + visible: bind(players, "activePlayer").as((p: AstalMpris.Player) => + p.loopStatus !== AstalMpris.Loop.UNSUPPORTED), + image: new Widget.Icon({ + icon: bind(players, "activePlayer").as((p: AstalMpris.Player) => { + switch(p.loopStatus) { case AstalMpris.Loop.TRACK: - return "Loop song"; - + return "media-playlist-repeat-song-symbolic"; case AstalMpris.Loop.PLAYLIST: - return "Loop playlist"; + return "media-playlist-repeat-symbolic"; } - - return "No loop"; - }), - onClickRelease: () => activePlayer.loop() - } as Widget.ButtonProps) - ] - } as Widget.BoxProps), - endWidget: new Widget.Label({ - className: "length", - valign: Gtk.Align.START, - halign: Gtk.Align.END, - label: bind(activePlayer, "length").as((len/* bananananananana */: number) => { - - const sec: number = Math.floor(len % 60); - const min = Math.floor((len % 3600) / 60); - const hours: number = Math.floor(len / 3600); - - //console.log("Len:", len, "\nLen in hours:", hours); - return (len > 0 && len < GLib.MAXINT64 / 10000000) ? - `${hours > 0 ? `${hours}:` : ''}${min < 10 && hours > 0 ? `0${min}` : `${min}`}:${sec < 10 ? `0${sec}` : `${sec}`}` - : ( len <= 0 ? `0:00` : "Live"); - }) - } as Widget.LabelProps) + return "loop-arrow-symbolic"; + }) + } as Widget.IconProps), + tooltipText: bind(players, "activePlayer").as((p: AstalMpris.Player) => { + switch(p.loopStatus) { + case AstalMpris.Loop.TRACK: + return "Loop song"; + case AstalMpris.Loop.PLAYLIST: + return "Loop playlist"; + } + return "No loop"; + }), + onClickRelease: () => players.activePlayer.loop() + } as Widget.ButtonProps) + ] + }), + endWidget: new Widget.Label({ + className: "length", + valign: Gtk.Align.START, + halign: Gtk.Align.END, + label: bind(players, "activePlayer").as(p => { + const len = p?.length ?? 0; + if (len <= 0) return "0:00"; + if (len > GLib.MAXINT64 / 10000000) return "Live"; + return formatTime(len); + }) }) - ]) - } as Widget.BoxProps); + }) + ] + }); } From f1c9abe7bb7676909fa0cb9d93f2045286ab15e0 Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sun, 3 Aug 2025 11:42:36 +0300 Subject: [PATCH 63/64] Change some connections for property work `Media.ts` and `BigMedia.ts` --- ags/scripts/player.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ags/scripts/player.ts b/ags/scripts/player.ts index 8e1d47ef..fb282a2e 100644 --- a/ags/scripts/player.ts +++ b/ags/scripts/player.ts @@ -42,13 +42,13 @@ class AstalPlayers extends GObject.Object { const ids = [ player.connect('notify::playback-status', handler), - player.connect('notify::cover-art', handler), + //player.connect('notify::cover-art', handler), player.connect('notify::identity', handler), - player.connect('notify::track-id', handler), - // player.connect('notify::title', handler), // Em... This is a stupid idea. + //player.connect('notify::track-id', handler), + // player.connect('notify::title', handler), // player.connect('notify::artist', handler), - //player.connect('notify::metadata', handler), // infinity call from Clapper - //player.connect('notify::position', handler) // DO NOT DO THAT! + player.connect('notify::metadata', handler), + player.connect('notify::position', handler) ]; this.#playerConnections.set(player, ids); From c347e2c48a2dd7a1e979863e30571a0eb20d899e Mon Sep 17 00:00:00 2001 From: Mephisto <38382271+NotMephisto@users.noreply.github.com> Date: Sun, 3 Aug 2025 12:43:09 +0300 Subject: [PATCH 64/64] Fixed Progress Label --- ags/widget/center-window/BigMedia.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ags/widget/center-window/BigMedia.ts b/ags/widget/center-window/BigMedia.ts index 2dc4b257..e68238c8 100644 --- a/ags/widget/center-window/BigMedia.ts +++ b/ags/widget/center-window/BigMedia.ts @@ -93,9 +93,9 @@ export function BigMedia(): Gtk.Widget { className: "elapsed", valign: Gtk.Align.START, halign: Gtk.Align.START, - label: bind(players.activePlayer, "position").as(p => { - const pos = p ?? 0; - const len = players.activePlayer.length ?? 0; + label: bind(players, "activePlayer").as((p: AstalMpris.Player) => { + const pos = p?.position ?? 0; + const len = p?.length ?? 0; return formatTime(pos > 0 && len > 0 ? pos : 0); }) }),