chore: control-center and center-window widgets to gtk4 and ags v3

This commit is contained in:
retrozinndev
2025-07-06 19:56:09 -03:00
parent 7b758bd298
commit 9db1d6fc12
37 changed files with 1477 additions and 1867 deletions
-227
View File
@@ -1,227 +0,0 @@
import { AstalIO, bind, Binding, exec, timeout } from "astal";
import { Gtk, Widget } from "astal/gtk3";
import AstalMpris from "gi://AstalMpris";
import { Clipboard } from "../../scripts/clipboard";
export function BigMedia(): Gtk.Widget {
let dragTimer: (AstalIO.Time|undefined);
return new Widget.Box({
className: "big-media",
orientation: Gtk.Orientation.VERTICAL,
homogeneous: false,
width_request: 250,
visible: bind(AstalMpris.get_default(), "players").as((players: Array<AstalMpris.Player>) =>
players[0] ? true : false),
children: bind(AstalMpris.get_default(), "players").as((players: Array<AstalMpris.Player>) =>
players[0] && [
new Widget.Box({
halign: Gtk.Align.CENTER,
child: new Widget.Box({
className: "image",
hexpand: false,
orientation: Gtk.Orientation.VERTICAL,
marginTop: 6,
visible: getAlbumArt(players[0]).as(Boolean),
css: getAlbumArt(players[0]).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",
orientation: Gtk.Orientation.VERTICAL,
vexpand: true,
valign: Gtk.Align.CENTER,
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),
truncate: true,
maxWidthChars: 25,
} 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),
maxWidthChars: 28,
truncate: true,
} as Widget.LabelProps)
]
} as Widget.BoxProps),
new Widget.Box({
className: "progress",
hexpand: true,
visible: bind(players[0], "canSeek"),
children: [
new Widget.Slider({
min: 0,
hexpand: true,
max: bind(players[0], "length").as((length: number) =>
Math.floor(length)),
value: bind(players[0], "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)));
else {
dragTimer.cancel();
dragTimer = timeout(600, () =>
players[0].set_position(Math.round(slider.value)));
}
}
})
]
}),
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[0], "position").as((pos: number) => {
const sec: number = Math.floor(pos % 60);
return pos > 0 && players[0].length > 0 ?
`${Math.floor(pos / 60)}:${sec < 10 ? "0" : ""}${sec}`
: `0:00`;
})
} 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(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",
visible: bind(players[0], "shuffleStatus").as((shuffleStatus) =>
shuffleStatus !== AstalMpris.Shuffle.UNSUPPORTED),
image: new Widget.Icon({
icon: bind(players[0], "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) =>
shuffleStatus === AstalMpris.Shuffle.ON ?
"Shuffle"
: "No shuffle"),
onClick: () => players[0].shuffle()
} as Widget.ButtonProps),
new Widget.Button({
className: "previous",
image: new Widget.Icon({
icon: "media-skip-backward-symbolic"
} as Widget.IconProps),
tooltipText: "Previous",
onClick: () => players[0].canGoPrevious && players[0].previous()
} as Widget.ButtonProps),
new Widget.Button({
className: "pause",
tooltipText: bind(players[0], "playback_status").as((status) =>
status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play"),
image: new Widget.Icon({
icon: bind(players[0], "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()
} as Widget.ButtonProps),
new Widget.Button({
className: "next",
image: new Widget.Icon({
icon: "media-skip-forward-symbolic"
} as Widget.IconProps),
tooltipText: "Next",
onClick: () => players[0].canGoNext && players[0].next()
} as Widget.ButtonProps),
new Widget.Button({
className: "repeat",
visible: bind(players[0], "loopStatus").as((loopStatus) =>
loopStatus !== AstalMpris.Loop.UNSUPPORTED),
image: new Widget.Icon({
icon: bind(players[0], "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(players[0], "loopStatus").as((loopStatus) => {
switch(loopStatus) {
case AstalMpris.Loop.TRACK:
return "Loop song";
case AstalMpris.Loop.PLAYLIST:
return "Loop playlist";
}
return "No loop";
}),
onClick: () => players[0].loop()
} as Widget.ButtonProps)
]
} as Widget.BoxProps),
endWidget: new Widget.Label({
className: "length",
valign: Gtk.Align.START,
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)) ?
`${Math.floor(len / 60)}:${sec < 10 ? "0" : ""}${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<string | undefined> {
return bind(player, "artUrl").as((artUrl: string) => {
if(!artUrl)
return undefined;
if(artUrl.startsWith("/"))
return "file://" + artUrl;
return artUrl;
});
}
+186
View File
@@ -0,0 +1,186 @@
import { timeout } from "ags/time";
import { execAsync } from "ags/process";
import { Astal, Gtk } from "ags/gtk4";
import { Clipboard } from "../../scripts/clipboard";
import { player } from "../bar/Media";
import { createBinding, With } from "ags";
import AstalMpris from "gi://AstalMpris";
import AstalIO from "gi://AstalIO";
import Gio from "gi://Gio?version=2.0";
import Pango from "gi://Pango?version=1.0";
export const BigMedia = () => {
let dragTimer: (AstalIO.Time|undefined);
return <Gtk.Box class={"big-media"} orientation={Gtk.Orientation.VERTICAL} widthRequest={250}
visible={player(pl => pl.available)}>
<With value={player}>
{(player: AstalMpris.Player) => player.available &&
<Gtk.Box halign={Gtk.Align.CENTER} $={(self) => {
const artSub = createBinding(player, "artUrl").subscribe(() => {
const firstChild = self.get_first_child();
const albumArt = getAlbumArt(player);
if(!albumArt) {
if(firstChild instanceof Gtk.Picture)
self.remove(firstChild);
return;
}
if(firstChild instanceof Gtk.Picture) {
firstChild.set_filename(albumArt);
return;
}
self.prepend(
<Gtk.Picture file={Gio.File.new_for_path(albumArt)}
hexpand={false} vexpand={false} marginTop={6}
widthRequest={132} heightRequest={128}
/> as Gtk.Picture
);
});
const destroyId = self.connect("destroy", () => {
self.disconnect(destroyId);
artSub();
});
}}>
<Gtk.Box class={"info"} orientation={Gtk.Orientation.VERTICAL}
valign={Gtk.Align.CENTER} vexpand={true}>
<Gtk.Label class={"title"} tooltipText={
createBinding(player, "title").as(title => title ?? "No Title")
} ellipsize={Pango.EllipsizeMode.END} maxWidthChars={25}
/>
<Gtk.Label class={"artist"} tooltipText={
createBinding(player, "artist").as(artist => artist ?? "No Artist")
} ellipsize={Pango.EllipsizeMode.END} maxWidthChars={28}
/>
</Gtk.Box>
<Gtk.Box class={"progress"} hexpand={true} visible={createBinding(player, "canSeek")}>
<Astal.Slider hexpand={true} max={createBinding(player, "length").as(Math.floor)}
value={createBinding(player, "position").as(Math.floor)}
onChangeValue={(_, type, value) => {
if(type === undefined || type === null)
return;
if(!dragTimer) {
dragTimer = timeout(200, () =>
player.position = Math.floor(value));
return;
}
dragTimer.cancel();
dragTimer = timeout(200, () =>
player.position = Math.floor(value));
}}
/>
</Gtk.Box>
<Gtk.CenterBox class={"bottom"} hexpand={true} marginBottom={6}>
<Gtk.Label class={"elapsed"} valign={Gtk.Align.START} halign={Gtk.Align.START}
label={createBinding(player, "position").as(pos => {
const sec = Math.floor(pos % 60);
return pos > 0 && player.length > 0 ?
`${Math.floor(pos / 60)}:${sec < 10 ? "0" : ""}${sec}`
: "0:00";
})}
/>
<Gtk.Box class={"controls button-row"}>
<Gtk.Button class={"link"} iconName={"edit-paste-symbolic"}
tooltipText={"Copy link to clipboard"}
onClicked={() => {
execAsync(`playerctl --player=${
player.busName.replace(/^org\.mpris\.MediaPlayer2\./i, "")
} metadata xesam:url`).then(link => {
Clipboard.getDefault().copyAsync(link);
}).catch((e: Error) => {
console.error(`Media: couldn't copy media link. Stderr: \n${e.message}\n${e.stack}`);
});
}}
/>
<Gtk.Button class={"shuffle"} visible={createBinding(player, "shuffleStatus").as(status =>
status !== AstalMpris.Shuffle.UNSUPPORTED)} iconName={
createBinding(player, "shuffleStatus").as(status => status === AstalMpris.Shuffle.ON ?
"media-playlist-shuffle-symbolic"
: "media-playlist-consecutive-symbolic")} tooltipText={
createBinding(player, "shuffleStatus").as(status => status === AstalMpris.Shuffle.ON ?
"Shuffle"
: "No shuffle")} onClicked={player.shuffle}
/>
<Gtk.Button class={"previous"} iconName={"media-skip-backward-symbolic"}
tooltipText={"Previous"} onClicked={() => player.canGoPrevious && player.previous()}
/>
<Gtk.Button class={"play-pause"} tooltipText={
createBinding(player, "playbackStatus").as(status =>
status === AstalMpris.PlaybackStatus.PLAYING ? "Pause" : "Play")}
iconName={createBinding(player, "playbackStatus").as(status =>
status === AstalMpris.PlaybackStatus.PLAYING ?
"media-playback-pause-symbolic"
: "media-playback-start-symbolic")} onClicked={player.play_pause}
/>
<Gtk.Button class={"next"} iconName={"media-skip-forward-symbolic"}
tooltipText={"Next"} onClicked={() => player.canGoNext && player.next()}
/>
<Gtk.Button class={"repeat"} iconName={createBinding(player, "loopStatus").as(status => {
if(status === AstalMpris.Loop.TRACK)
return "media-playlist-repeat-song-symbolic";
if(status === AstalMpris.Loop.PLAYLIST)
return "media-playlist-repeat-symbolic";
return "loop-arrow-symbolic";
})} visible={createBinding(player, "loopStatus").as(status =>
status !== AstalMpris.Loop.UNSUPPORTED)}
tooltipText={createBinding(player, "loopStatus").as(status => {
if(status === AstalMpris.Loop.TRACK)
return "Loop song";
if(status === AstalMpris.Loop.PLAYLIST)
return "Loop playlist";
return "No loop";
})} onClicked={player.loop}
/>
</Gtk.Box>
<Gtk.Label class={"length"} valign={Gtk.Align.START} halign={Gtk.Align.END}
label={createBinding(player, "length").as(len => { /* bananananananana */
const sec = Math.floor(len % 60);
return (len > 0 && Number.isFinite(len)) ?
`${Math.floor(len / 60)}:${sec < 10 ? "0" : ""}${sec}`
: "0:00";
})}
/>
</Gtk.CenterBox>
</Gtk.Box>
}
</With>
</Gtk.Box> as Gtk.Box;
}
/**
* 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): string|undefined {
const artUrl = player.artUrl;
if(!artUrl)
return undefined;
if(artUrl.startsWith("/"))
return "file://" + artUrl;
return artUrl;
}
-46
View File
@@ -1,46 +0,0 @@
import { register, Variable } from "astal";
import { Gtk, Widget } from "astal/gtk3";
type CalendarProps = Pick<Widget.BoxProps,
"name"
| "className"
| "css"
| "expand"
| "halign"
| "valign"> & {
showWeekDays?: boolean;
showHeader?: boolean;
fillGrid?: boolean; // I need a better name for this LMAOOO
};
@register({ GTypeName: "Calendar" })
class Calendar extends Gtk.Box {
#showWeekDays = new Variable<boolean>(true);
#showHeader = new Variable<boolean>(true);
#fillGrid = new Variable<boolean>(false);
set fillGrid(newValue: boolean) { this.#fillGrid.set(newValue); }
get fillGrid() { return this.#fillGrid.get(); }
set showHeader(newValue: boolean) { this.#showHeader.set(newValue); }
get showHeader() { return this.#showHeader.get(); }
set showWeekDays(newValue: boolean) { this.#showWeekDays.set(newValue); }
get showWeekDays() { return this.#showWeekDays.get(); }
constructor(props?: CalendarProps) {
super();
this.add(new Widget.Box({
...props,
widthRequest: 128,
heightRequest: 128,
children: [
new Widget.Box({
className: "header",
heightRequest: 24,
hexpand: true,
} as Widget.BoxProps)
]
} as Widget.BoxProps));
}
}