✨ chore: control-center and center-window widgets to gtk4 and ags v3
This commit is contained in:
@@ -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;
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user