feat(ags): finish migrating v1 to v2
All checks were successful
Discord / discord commits (push) Has been skipped

This commit is contained in:
matt1432 2025-02-28 21:56:54 -05:00
parent 7d64a1fe25
commit 2e15e10fd5
24 changed files with 1196 additions and 1026 deletions

View file

@ -7,6 +7,7 @@
@use '../widgets/clipboard'; @use '../widgets/clipboard';
@use '../widgets/date'; @use '../widgets/date';
@use '../widgets/icon-browser'; @use '../widgets/icon-browser';
@use '../widgets/media-player';
@use '../widgets/misc'; @use '../widgets/misc';
@use '../widgets/network'; @use '../widgets/network';
@use '../widgets/notifs'; @use '../widgets/notifs';

View file

@ -0,0 +1,124 @@
@use '../../style/colors';
.media-player {
margin-top: 9px;
min-width: 100px;
min-height: 100px;
* {
all: unset;
}
.arrow {
transition: -gtk-icon-transform 0.3s ease-in-out;
margin-bottom: 12px;
}
.player {
padding: 10px;
min-width: 400px;
min-height: 200px;
border-radius: 30px;
border-top: 2px solid colors.$accent_color;
border-bottom: 2px solid colors.$accent_color;
transition: background 250ms;
.top {
font-size: 23px;
}
.metadata {
.title {
font-weight: 500;
transition: text 250ms;
}
.artist {
font-weight: 400;
font-size: 15px;
transition: text 250ms;
}
}
.bottom {
font-size: 30px;
}
.pausebutton {
transition: background-color ease .2s, color ease .2s;
font-size: 18px;
}
.playing {
transition: background-color ease .2s, color ease .2s;
border-radius: 15px;
}
.stopped,
.paused {
transition: background-color ease .2s, color ease .2s;
border-radius: 26px;
padding: 4px 6px;
}
button label {
min-width: 35px;
}
}
.player-icon, .position-indicator {
min-width: 18px;
margin: 7px;
}
.position-indicator {
background-color: rgba(255, 255, 255, 0.7);
box-shadow: 0 0 5px 0 rgba(255, 255, 255, 0.3);
border-radius: 100%;
}
.previous,
.next,
.shuffle,
.loop {
border-radius: 100%;
transition: color 200ms;
&:hover {
border-radius: 100%;
background-color: rgba(127, 132, 156, 0.4);
transition: color 200ms;
}
}
.loop {
label {
padding-right: 8px;
}
}
.position-slider {
min-height: 20px;
highlight {
margin: 0;
border-radius: 2em;
}
trough {
margin: 0 8px;
border-radius: 2em;
}
slider {
margin: -8px;
min-height: 20px;
border-radius: 10px;
transition: background-color 0.5s ease-in-out;
}
slider:hover {
transition: background-color 0.5s ease-in-out;
}
}
}

View file

@ -0,0 +1,214 @@
import { timeout } from 'astal';
import { Gtk } from 'astal/gtk3';
import { property, register } from 'astal/gobject';
import { CenterBox, CenterBoxProps, EventBox, Overlay, OverlayProps } from 'astal/gtk3/widget';
import Mpris from 'gi://AstalMpris';
const MAX_OFFSET = 200;
const OFFSCREEN = 500;
const ANIM_DURATION = 500;
const TRANSITION = `transition: margin ${ANIM_DURATION}ms ease,
opacity ${ANIM_DURATION}ms ease;`;
/* Types */
export interface Gesture {
attribute?: object
setup?: (self: PlayerGesture) => void
props?: OverlayProps
}
@register()
export class PlayerBox extends CenterBox {
@property(String)
declare bgStyle: string;
@property(Object)
declare player: Mpris.Player;
constructor(props: Omit<CenterBoxProps, 'setup'> & {
bgStyle?: string
player?: Mpris.Player
setup?: (self: PlayerBox) => void
}) {
super(props as CenterBoxProps);
}
}
@register()
export class PlayerGesture extends Overlay {
private _widget: EventBox;
private _gesture: Gtk.GestureDrag;
players = new Map();
setup = false;
dragging = false;
set overlays(value) {
super.overlays = value;
}
get overlays() {
return super.overlays.filter((overlay) => overlay !== this.child);
}
includesWidget(playerW: PlayerBox) {
return this.overlays.find((w) => w === playerW);
}
showTopOnly() {
this.overlays.forEach((over) => {
over.visible = over === this.overlays.at(-1);
});
}
moveToTop(player: PlayerBox) {
player.visible = true;
this.reorder_overlay(player, -1);
timeout(ANIM_DURATION, () => {
this.showTopOnly();
});
}
dragUpdate(realGesture: Gtk.GestureDrag) {
if (realGesture) {
this.overlays.forEach((over) => {
over.visible = true;
});
}
else {
this.showTopOnly();
}
// Don't allow gesture when only one player
if (this.overlays.length <= 1) {
return;
}
this.dragging = true;
let offset = this._gesture.get_offset()[1];
const playerBox = this.overlays.at(-1) as PlayerBox;
if (!offset) {
return;
}
// Slide right
if (offset >= 0) {
playerBox.css = `
margin-left: ${offset}px;
margin-right: -${offset}px;
${playerBox.bgStyle}
`;
}
// Slide left
else {
offset = Math.abs(offset);
playerBox.css = `
margin-left: -${offset}px;
margin-right: ${offset}px;
${playerBox.bgStyle}
`;
}
}
dragEnd() {
// Don't allow gesture when only one player
if (this.overlays.length <= 1) {
return;
}
this.dragging = false;
const offset = this._gesture.get_offset()[1];
const playerBox = this.overlays.at(-1) as PlayerBox;
// If crosses threshold after letting go, slide away
if (offset && Math.abs(offset) > MAX_OFFSET) {
// Disable inputs during animation
this._widget.sensitive = false;
// Slide away right
if (offset >= 0) {
playerBox.css = `
${TRANSITION}
margin-left: ${OFFSCREEN}px;
margin-right: -${OFFSCREEN}px;
opacity: 0.7; ${playerBox.bgStyle}
`;
}
// Slide away left
else {
playerBox.css = `
${TRANSITION}
margin-left: -${OFFSCREEN}px;
margin-right: ${OFFSCREEN}px;
opacity: 0.7; ${playerBox.bgStyle}
`;
}
timeout(ANIM_DURATION, () => {
// Put the player in the back after anim
this.reorder_overlay(playerBox, 0);
// Recenter player
playerBox.css = playerBox.bgStyle;
this._widget.sensitive = true;
this.showTopOnly();
});
}
else {
// Recenter with transition for animation
playerBox.css = `${TRANSITION} ${playerBox.bgStyle}`;
timeout(ANIM_DURATION, () => {
this.showTopOnly();
});
}
}
constructor({
setup = () => { /**/ },
widget,
...props
}: Omit<OverlayProps, 'setup'> & {
widget: EventBox
setup: (self: PlayerGesture) => void
}) {
super(props);
setup(this);
this._widget = widget;
this._gesture = Gtk.GestureDrag.new(this);
this.hook(this._gesture, 'drag-update', (_, realGesture) => this.dragUpdate(realGesture));
this.hook(this._gesture, 'drag-end', () => this.dragEnd());
}
}
export default ({
setup = () => { /**/ },
...props
}: Gesture) => {
const widget = new EventBox();
// Have empty PlayerBox to define the size of the widget
const emptyPlayer = new PlayerBox({
className: 'player',
});
const content = new PlayerGesture({
...props,
setup,
widget,
child: emptyPlayer,
});
widget.add(content);
return widget;
};

View file

@ -0,0 +1,502 @@
import { bind, execAsync, idle, type Variable } from 'astal';
import { Gdk, Gtk } from 'astal/gtk3';
import { Box, Button, CenterBoxProps, Icon, Label, Slider, Stack } from 'astal/gtk3/widget';
import { kebabify } from 'astal/binding';
import Mpris from 'gi://AstalMpris';
import Separator from '../misc/separator';
import { PlayerBox, PlayerGesture } from './gesture';
const ICON_SIZE = 32;
const icons = {
mpris: {
fallback: 'audio-x-generic-symbolic',
shuffle: {
enabled: '󰒝',
disabled: '󰒞',
},
loop: {
none: '󰑗',
track: '󰑘',
playlist: '󰑖',
},
playing: 'media-playback-pause-symbolic',
paused: 'media-playback-start-symbolic',
stopped: 'media-playback-stop-symbolic',
prev: '󰒮',
next: '󰒭',
},
};
/* Types */
export interface Colors {
imageAccent: string
buttonAccent: string
buttonText: string
hoverAccent: string
}
export const CoverArt = (
player: Mpris.Player,
colors: Variable<Colors>,
props: CenterBoxProps,
) => new PlayerBox({
...props,
vertical: true,
bgStyle: '',
player,
setup: (self: PlayerBox) => {
const setCover = () => {
execAsync(['bash', '-c', `[[ -f "${player.coverArt}" ]] &&
coloryou "${player.coverArt}" | grep -v Warning`])
.then((out) => {
if (!player.coverArt) {
return;
}
colors.set(JSON.parse(out));
self.bgStyle = `
background: radial-gradient(circle,
rgba(0, 0, 0, 0.4) 30%,
${colors.get().imageAccent}),
url("${player.coverArt}");
background-size: cover;
background-position: center;
`;
if (!(self.get_parent() as PlayerGesture).dragging) {
self.css = self.bgStyle;
}
}).catch(() => {
colors.set({
imageAccent: '#6b4fa2',
buttonAccent: '#ecdcff',
buttonText: '#25005a',
hoverAccent: '#d4baff',
});
self.bgStyle = `
background: radial-gradient(circle,
rgba(0, 0, 0, 0.4) 30%,
${(colors as Variable<Colors>).get().imageAccent}),
rgb(0, 0, 0);
background-size: cover;
background-position: center;
`;
self.css = self.bgStyle;
});
};
player.connect('notify::cover-art', () => setCover());
idle(() => setCover());
},
});
export const TitleLabel = (player: Mpris.Player) => new Label({
xalign: 0,
max_width_chars: 40,
truncate: true,
justify: Gtk.Justification.LEFT,
className: 'title',
label: bind(player, 'title'),
});
export const ArtistLabel = (player: Mpris.Player) => new Label({
xalign: 0,
max_width_chars: 40,
truncate: true,
justify: Gtk.Justification.LEFT,
className: 'artist',
label: bind(player, 'artist'),
});
export const PlayerIcon = (player: Mpris.Player, overlay: PlayerGesture) => {
const playerIcon = (
p: Mpris.Player,
widget?: PlayerGesture,
playerBox?: PlayerBox,
) => new Button({
tooltip_text: p.identity || '',
onButtonReleaseEvent: () => {
if (widget && playerBox) {
widget.moveToTop(playerBox);
}
},
child: new Icon({
css: `font-size: ${ICON_SIZE}px;`,
className: widget ? 'position-indicator' : 'player-icon',
setup: (self) => {
idle(() => {
if (p.entry === null) { return; }
self.icon = Icon.lookup_icon(p.entry) ?
p.entry :
icons.mpris.fallback;
});
},
}),
});
return new Box({
setup(self) {
const update = () => {
const grandPa = self.get_parent()?.get_parent();
if (!grandPa) {
return;
}
const thisIndex = overlay.overlays.indexOf(grandPa);
self.children = (overlay.overlays as PlayerBox[])
.map((playerBox, i) => {
self.children.push(Separator({ size: 2 }));
return i === thisIndex ?
playerIcon(player) :
playerIcon(playerBox.player, overlay, playerBox);
})
.reverse();
};
self.hook(Mpris.get_default(), 'player-added', update);
self.hook(Mpris.get_default(), 'player-closed', update);
idle(() => update());
},
});
};
const display = Gdk.Display.get_default();
export const PositionSlider = (
player: Mpris.Player,
colors: Variable<Colors>,
) => new Slider({
className: 'position-slider',
valign: Gtk.Align.CENTER,
hexpand: true,
draw_value: false,
onDragged: ({ value }) => {
player.position = player.length * value;
},
onButtonPressEvent: (self) => {
if (!display) {
return;
}
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'grabbing',
));
},
onButtonReleaseEvent: (self) => {
if (!display) {
return;
}
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
},
onEnterNotifyEvent: (self) => {
if (!display) {
return;
}
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
},
onLeaveNotifyEvent: (self) => {
self.window.set_cursor(null);
},
setup: (self) => {
const update = () => {
if (!self.dragging) {
self.visible = player.length > 0;
if (player.length > 0) {
self.value = player.position / player.length;
}
}
};
const interval = setInterval(() => {
if (player) {
update();
}
else {
interval.destroy();
}
}, 1000);
self.hook(colors, () => {
if (colors.get()) {
const c = colors.get();
self.css = `
highlight { background-color: ${c.buttonAccent}; }
slider { background-color: ${c.buttonAccent}; }
slider:hover { background-color: ${c.hoverAccent}; }
trough { background-color: ${c.buttonText}; }
`;
}
});
player.connect('notify::position', () => update());
},
});
const PlayerButton = ({
player,
colors,
children = [],
onClick,
prop,
}: {
player: Mpris.Player
colors: Variable<Colors>
children: Label[] | Icon[]
onClick: keyof Mpris.Player
prop: string
}) => {
let hovered = false;
const stack = new Stack({ children });
return new Button({
cursor: 'pointer',
child: stack,
// @ts-expect-error FIXME
onButtonReleaseEvent: () => player[onClick](),
onHover: () => {
hovered = true;
if (prop === 'playbackStatus' && colors.get()) {
const c = colors.get();
children.forEach((ch) => {
ch.css = `
background-color: ${c.hoverAccent};
color: ${c.buttonText};
min-height: 40px;
min-width: 36px;
margin-bottom: 1px;
margin-right: 1px;
`;
});
}
},
onHoverLost: () => {
hovered = false;
if (prop === 'playbackStatus' && colors.get()) {
const c = colors.get();
children.forEach((ch) => {
ch.css = `
background-color: ${c.buttonAccent};
color: ${c.buttonText};
min-height: 42px;
min-width: 38px;
`;
});
}
},
setup: (self) => {
// @ts-expect-error FIXME
if (player[prop] && player[prop] !== false) {
// @ts-expect-error FIXME
stack.shown = String(player[prop]);
}
player.connect(`notify::${kebabify(prop)}`, () => {
// @ts-expect-error FIXME
if (player[prop] !== 0) {
// @ts-expect-error FIXME
stack.shown = String(player[prop]);
}
});
self.hook(colors, () => {
if (!Mpris.get_default().get_players().find((p) => player === p)) {
return;
}
if (colors.get()) {
const c = colors.get();
if (prop === 'playbackStatus') {
if (hovered) {
children.forEach((ch) => {
ch.css = `
background-color: ${c.hoverAccent};
color: ${c.buttonText};
min-height: 40px;
min-width: 36px;
margin-bottom: 1px;
margin-right: 1px;
`;
});
}
else {
children.forEach((ch) => {
ch.css = `
background-color: ${c.buttonAccent};
color: ${c.buttonText};
min-height: 42px;
min-width: 38px;
`;
});
}
}
else {
self.css = `
* { color: ${c.buttonAccent}; }
*:hover { color: ${c.hoverAccent}; }
`;
}
}
});
},
});
};
export const ShuffleButton = (
player: Mpris.Player,
colors: Variable<Colors>,
) => PlayerButton({
player,
colors,
children: [
(new Label({
name: Mpris.Shuffle.ON.toString(),
className: 'shuffle enabled',
label: icons.mpris.shuffle.enabled,
})),
(new Label({
name: Mpris.Shuffle.OFF.toString(),
className: 'shuffle disabled',
label: icons.mpris.shuffle.disabled,
})),
],
onClick: 'shuffle',
prop: 'shuffleStatus',
});
export const LoopButton = (
player: Mpris.Player,
colors: Variable<Colors>,
) => PlayerButton({
player,
colors,
children: [
(new Label({
name: Mpris.Loop.NONE.toString(),
className: 'loop none',
label: icons.mpris.loop.none,
})),
(new Label({
name: Mpris.Loop.TRACK.toString(),
className: 'loop track',
label: icons.mpris.loop.track,
})),
(new Label({
name: Mpris.Loop.PLAYLIST.toString(),
className: 'loop playlist',
label: icons.mpris.loop.playlist,
})),
],
onClick: 'loop',
prop: 'loopStatus',
});
export const PlayPauseButton = (
player: Mpris.Player,
colors: Variable<Colors>,
) => PlayerButton({
player,
colors,
children: [
(new Icon({
name: Mpris.PlaybackStatus.PLAYING.toString(),
className: 'pausebutton playing',
icon: icons.mpris.playing,
})),
(new Icon({
name: Mpris.PlaybackStatus.PAUSED.toString(),
className: 'pausebutton paused',
icon: icons.mpris.paused,
})),
(new Icon({
name: Mpris.PlaybackStatus.STOPPED.toString(),
className: 'pausebutton stopped paused',
icon: icons.mpris.stopped,
})),
],
onClick: 'play_pause',
prop: 'playbackStatus',
});
export const PreviousButton = (
player: Mpris.Player,
colors: Variable<Colors>,
) => PlayerButton({
player,
colors,
children: [
(new Label({
name: 'true',
className: 'previous',
label: icons.mpris.prev,
})),
(new Label({
name: 'false',
className: 'previous',
label: icons.mpris.prev,
})),
],
onClick: 'previous',
prop: 'canGoPrev',
});
export const NextButton = (
player: Mpris.Player,
colors: Variable<Colors>,
) => PlayerButton({
player,
colors,
children: [
(new Label({
name: 'true',
className: 'next',
label: icons.mpris.next,
})),
(new Label({
name: 'false',
className: 'next',
label: icons.mpris.next,
})),
],
onClick: 'next',
prop: 'canGoNext',
});

View file

@ -0,0 +1,183 @@
import { Variable } from 'astal';
import { Gtk } from 'astal/gtk3';
import { Box, CenterBox } from 'astal/gtk3/widget';
import Mpris from 'gi://AstalMpris';
import Separator from '../misc/separator';
import * as mpris from './mpris';
import PlayerGesture, {
PlayerGesture as PlayerGestureClass,
PlayerBox as PlayerBoxClass,
} from './gesture';
const FAVE_PLAYER = 'org.mpris.MediaPlayer2.spotify';
const SPACING = 8;
const Top = (
player: Mpris.Player,
overlay: PlayerGestureClass,
) => new Box({
className: 'top',
halign: Gtk.Align.START,
valign: Gtk.Align.START,
children: [
mpris.PlayerIcon(player, overlay),
],
});
const Center = (
player: Mpris.Player,
colors: Variable<mpris.Colors>,
) => new Box({
className: 'center',
children: [
(new CenterBox({
vertical: true,
start_widget: new Box({
className: 'metadata',
vertical: true,
halign: Gtk.Align.START,
valign: Gtk.Align.CENTER,
hexpand: true,
children: [
mpris.TitleLabel(player),
mpris.ArtistLabel(player),
],
}),
})),
(new CenterBox({
vertical: true,
center_widget: mpris.PlayPauseButton(player, colors),
})),
],
});
const Bottom = (
player: Mpris.Player,
colors: Variable<mpris.Colors>,
) => new Box({
className: 'bottom',
children: [
mpris.PreviousButton(player, colors),
Separator({ size: SPACING }),
mpris.PositionSlider(player, colors),
Separator({ size: SPACING }),
mpris.NextButton(player, colors),
Separator({ size: SPACING }),
mpris.ShuffleButton(player, colors),
Separator({ size: SPACING }),
mpris.LoopButton(player, colors),
],
});
const PlayerBox = (
player: Mpris.Player,
colors: Variable<mpris.Colors>,
overlay: PlayerGestureClass,
) => {
const widget = mpris.CoverArt(player, colors, {
className: `player ${player.identity}`,
hexpand: true,
start_widget: Top(player, overlay),
center_widget: Center(player, colors),
end_widget: Bottom(player, colors),
});
widget.visible = false;
return widget;
};
export default () => {
const content = PlayerGesture({
setup: (self) => {
const addPlayer = (player: Mpris.Player) => {
if (!player || self.players.has(player.bus_name)) {
return;
}
// Get the one on top so we can move it up later
const previousFirst = self.overlays.at(-1) as PlayerBoxClass;
// Make the new player
const colorsVar = Variable({
imageAccent: '#6b4fa2',
buttonAccent: '#ecdcff',
buttonText: '#25005a',
hoverAccent: '#d4baff',
});
self.players.set(
player.bus_name,
PlayerBox(player, colorsVar, self),
);
self.add_overlay(self.players.get(player.bus_name));
// Select favorite player at startup
if (!self.setup && self.players.has(FAVE_PLAYER)) {
self.moveToTop(self.players.get(FAVE_PLAYER));
self.setup = true;
}
// Move previousFirst on top again
else {
self.moveToTop(previousFirst);
}
};
const removePlayer = (player: Mpris.Player) => {
if (!player || !self.players.has(player.bus_name)) {
return;
}
const toDelete = self.players.get(player.bus_name);
// Get the one on top so we can move it up later
const previousFirst = self.overlays.at(-1) as PlayerBoxClass;
// Move previousFirst on top again
if (previousFirst !== toDelete) {
self.moveToTop(previousFirst);
}
else {
self.moveToTop(self.players.has(FAVE_PLAYER) ?
self.players.get(FAVE_PLAYER) :
self.overlays[0]);
}
// Remake overlays without deleted one
self.remove(toDelete);
self.players.delete(player.bus_name);
};
const mprisDefault = Mpris.get_default();
self.hook(mprisDefault, 'player-added', (_, player) => addPlayer(player));
self.hook(mprisDefault, 'player-closed', (_, player) => removePlayer(player));
mprisDefault.players.forEach(addPlayer);
},
});
return new Box({
className: 'media-player',
child: content,
});
};

View file

@ -115,6 +115,10 @@ in {
playerctl playerctl
wayfreeze wayfreeze
; ;
inherit
(self.packages.${pkgs.system})
coloryou
;
}) })
++ (optionals cfgDesktop.isTouchscreen (attrValues { ++ (optionals cfgDesktop.isTouchscreen (attrValues {
inherit inherit

View file

@ -1,69 +0,0 @@
// For ./ts/bar/hovers/keyboard.ts
export interface Keyboard {
address: string
name: string
rules: string
model: string
layout: string
variant: string
options: string
active_keymap: string
main: boolean
}
// For ./ts/media-player/gesture.ts
export interface Gesture {
attribute?: object
setup?(self: OverlayGeneric): void
props?: OverlayProps<unknown & Widget, unknown>
}
// For ./ts/media-player/mpris.ts
type PlayerDragProps = unknown & { dragging: boolean };
export type PlayerDrag = AgsCenterBox<
unknown & Widget, unknown & Widget, unknown & Widget, unknown & PlayerDragProps
>;
interface Colors {
imageAccent: string
buttonAccent: string
buttonText: string
hoverAccent: string
}
// For ./ts/media-player
export interface PlayerBoxProps {
bgStyle: string
player: MprisPlayer
}
export type PlayerBox = AgsCenterBox<
unknown & Widget, unknown & Widget, unknown & Widget, PlayerBoxProps
>;
export type PlayerOverlay = AgsOverlay<AgsWidget, {
players: Map
setup: boolean
dragging: boolean
includesWidget(playerW: PlayerBox): PlayerBox
showTopOnly(): void
moveToTop(player: PlayerBox): void
}>;
export interface PlayerButtonType {
player: MprisPlayer
colors: Var<Colors>
children: StackProps['children']
onClick: string
prop: string
}
// For ./ts/quick-settings
import { BluetoothDevice as BTDev } from 'types/service/bluetooth.ts';
export interface APType {
bssid: string
address: string
lastSeen: number
ssid: string
active: boolean
strength: number
iconName: string
}
export type APBox = AgsBox<unknown & Widget, { ap: Var<APType> }>;
export type DeviceBox = AgsBox<unknown & Widget, { dev: BTDev }>;

View file

@ -1,116 +0,0 @@
.arrow {
transition: -gtk-icon-transform 0.3s ease-in-out;
margin-bottom: 12px;
}
.media {
margin-top: 9px;
}
.player {
all: unset;
padding: 10px;
min-width: 400px;
min-height: 200px;
border-radius: 30px;
border-top: 2px solid $contrast-bg;
border-bottom: 2px solid $contrast-bg;
transition: background 250ms;
.top {
font-size: 23px;
}
.metadata {
.title {
font-weight: 500;
transition: text 250ms;
}
.artist {
font-weight: 400;
font-size: 15px;
transition: text 250ms;
}
}
.bottom {
font-size: 30px;
}
.pausebutton {
transition: background-color ease .2s,
color ease .2s;
font-size: 15px;
padding: 4px 4px 4px 7px;
}
.playing {
transition: background-color ease .2s,
color ease .2s;
border-radius: 15px;
}
.stopped,
.paused {
transition: background-color ease .2s,
color ease .2s;
border-radius: 26px;
padding: 4px 4px 4px 10px;
}
button label {
min-width: 35px;
}
}
.position-indicator {
min-width: 18px;
margin: 7px;
background-color: rgba(255, 255, 255, 0.7);
box-shadow: 0 0 5px 0 rgba(255, 255, 255, 0.3);
border-radius: 100%;
}
.previous,
.next,
.shuffle,
.loop {
border-radius: 100%;
transition: color 200ms;
&:hover {
border-radius: 100%;
background-color: rgba(127, 132, 156, 0.4);
transition: color 200ms;
}
}
.loop {
label {
padding-right: 8px;
}
}
.position-slider {
highlight {
margin: 0;
border-radius: 2em;
}
trough {
margin: 0 8px;
border-radius: 2em;
}
slider {
margin: -8px;
min-height: 20px;
border-radius: 10px;
transition: background-color 0.5s ease-in-out;
}
slider:hover {
transition: background-color 0.5s ease-in-out;
}
}

View file

@ -1,167 +0,0 @@
const { timeout } = Utils;
const { Box, EventBox, Overlay } = Widget;
const { Gtk } = imports.gi;
const MAX_OFFSET = 200;
const OFFSCREEN = 500;
const ANIM_DURATION = 500;
const TRANSITION = `transition: margin ${ANIM_DURATION}ms ease,
opacity ${ANIM_DURATION}ms ease;`;
// Types
import {
CenterBoxGeneric,
Gesture,
OverlayGeneric,
PlayerBox,
} from 'global-types';
export default ({
setup = () => { /**/ },
...props
}: Gesture) => {
const widget = EventBox();
const gesture = Gtk.GestureDrag.new(widget);
// Have empty PlayerBox to define the size of the widget
const emptyPlayer = Box({
class_name: 'player',
attribute: { empty: true },
});
const content = Overlay({
...props,
attribute: {
players: new Map(),
setup: false,
dragging: false,
includesWidget: (playerW: OverlayGeneric) => {
return content.overlays.find((w) => w === playerW);
},
showTopOnly: () => content.overlays.forEach((over) => {
over.visible = over === content.overlays.at(-1);
}),
moveToTop: (player: CenterBoxGeneric) => {
player.visible = true;
content.reorder_overlay(player, -1);
timeout(ANIM_DURATION, () => {
content.attribute.showTopOnly();
});
},
},
child: emptyPlayer,
setup: (self) => {
setup(self);
self
.hook(gesture, (_, realGesture) => {
if (realGesture) {
self.overlays.forEach((over) => {
over.visible = true;
});
}
else {
self.attribute.showTopOnly();
}
// Don't allow gesture when only one player
if (self.overlays.length <= 1) {
return;
}
self.attribute.dragging = true;
let offset = gesture.get_offset()[1];
const playerBox = self.overlays.at(-1) as PlayerBox;
if (!offset) {
return;
}
// Slide right
if (offset >= 0) {
playerBox.setCss(`
margin-left: ${offset}px;
margin-right: -${offset}px;
${playerBox.attribute.bgStyle}
`);
}
// Slide left
else {
offset = Math.abs(offset);
playerBox.setCss(`
margin-left: -${offset}px;
margin-right: ${offset}px;
${playerBox.attribute.bgStyle}
`);
}
}, 'drag-update')
.hook(gesture, () => {
// Don't allow gesture when only one player
if (self.overlays.length <= 1) {
return;
}
self.attribute.dragging = false;
const offset = gesture.get_offset()[1];
const playerBox = self.overlays.at(-1) as PlayerBox;
// If crosses threshold after letting go, slide away
if (offset && Math.abs(offset) > MAX_OFFSET) {
// Disable inputs during animation
widget.sensitive = false;
// Slide away right
if (offset >= 0) {
playerBox.setCss(`
${TRANSITION}
margin-left: ${OFFSCREEN}px;
margin-right: -${OFFSCREEN}px;
opacity: 0.7; ${playerBox.attribute.bgStyle}
`);
}
// Slide away left
else {
playerBox.setCss(`
${TRANSITION}
margin-left: -${OFFSCREEN}px;
margin-right: ${OFFSCREEN}px;
opacity: 0.7; ${playerBox.attribute.bgStyle}
`);
}
timeout(ANIM_DURATION, () => {
// Put the player in the back after anim
self.reorder_overlay(playerBox, 0);
// Recenter player
playerBox.setCss(playerBox.attribute.bgStyle);
widget.sensitive = true;
self.attribute.showTopOnly();
});
}
else {
// Recenter with transition for animation
playerBox.setCss(`${TRANSITION}
${playerBox.attribute.bgStyle}`);
}
}, 'drag-end');
},
});
widget.add(content);
return widget;
};

View file

@ -1,473 +0,0 @@
const Mpris = await Service.import('mpris');
const { Button, Icon, Label, Stack, Slider, CenterBox, Box } = Widget;
const { execAsync, lookUpIcon, readFileAsync } = Utils;
import Separator from '../misc/separator.ts';
import CursorBox from '../misc/cursorbox.ts';
const ICON_SIZE = 32;
const icons = {
mpris: {
fallback: 'audio-x-generic-symbolic',
shuffle: {
enabled: '󰒝',
disabled: '󰒞',
},
loop: {
none: '󰑗',
track: '󰑘',
playlist: '󰑖',
},
playing: ' ',
paused: ' ',
stopped: ' ',
prev: '󰒮',
next: '󰒭',
},
};
// Types
import { MprisPlayer } from 'types/service/mpris.ts';
import { Variable as Var } from 'types/variable';
import {
AgsWidget,
CenterBoxPropsGeneric,
Colors,
PlayerBox,
PlayerButtonType,
PlayerDrag,
PlayerOverlay,
} from 'global-types';
export const CoverArt = (
player: MprisPlayer,
colors: Var<Colors>,
props: CenterBoxPropsGeneric,
) => CenterBox({
...props,
vertical: true,
attribute: {
bgStyle: '',
player,
},
setup: (self: PlayerBox) => {
// Give temp cover art
readFileAsync(player.cover_path).catch(() => {
if (!colors.value && !player.track_cover_url) {
colors.setValue({
imageAccent: '#6b4fa2',
buttonAccent: '#ecdcff',
buttonText: '#25005a',
hoverAccent: '#d4baff',
});
self.attribute.bgStyle = `
background: radial-gradient(circle,
rgba(0, 0, 0, 0.4) 30%,
${(colors as Var<Colors>).value.imageAccent}),
rgb(0, 0, 0);
background-size: cover;
background-position: center;
`;
self.setCss(self.attribute.bgStyle);
}
});
self.hook(player, () => {
execAsync(['bash', '-c', `[[ -f "${player.cover_path}" ]] &&
coloryou "${player.cover_path}" | grep -v Warning`])
.then((out) => {
if (!Mpris.players.find((p) => player === p)) {
return;
}
colors.setValue(JSON.parse(out));
self.attribute.bgStyle = `
background: radial-gradient(circle,
rgba(0, 0, 0, 0.4) 30%,
${colors.value.imageAccent}),
url("${player.cover_path}");
background-size: cover;
background-position: center;
`;
if (!(self.get_parent() as PlayerDrag)
.attribute.dragging) {
self.setCss(self.attribute.bgStyle);
}
}).catch((err) => {
if (err !== '') {
print(err);
}
});
});
},
});
export const TitleLabel = (player: MprisPlayer) => Label({
xalign: 0,
max_width_chars: 40,
truncate: 'end',
justification: 'left',
class_name: 'title',
label: player.bind('track_title'),
});
export const ArtistLabel = (player: MprisPlayer) => Label({
xalign: 0,
max_width_chars: 40,
truncate: 'end',
justification: 'left',
class_name: 'artist',
label: player.bind('track_artists')
.transform((a) => a.join(', ') || ''),
});
export const PlayerIcon = (player: MprisPlayer, overlay: PlayerOverlay) => {
const playerIcon = (
p: MprisPlayer,
widget?: PlayerOverlay,
playerBox?: PlayerBox,
) => CursorBox({
tooltip_text: p.identity || '',
on_primary_click_release: () => {
if (widget && playerBox) {
widget.attribute.moveToTop(playerBox);
}
},
child: Icon({
class_name: widget ? 'position-indicator' : 'player-icon',
size: widget ? 0 : ICON_SIZE,
setup: (self) => {
self.hook(p, () => {
self.icon = lookUpIcon(p.entry) ?
p.entry :
icons.mpris.fallback;
});
},
}),
});
return Box().hook(Mpris, (self) => {
const grandPa = self.get_parent()?.get_parent() as AgsWidget;
if (!grandPa) {
return;
}
const thisIndex = overlay.overlays
.indexOf(grandPa);
self.children = (overlay.overlays as PlayerBox[])
.map((playerBox, i) => {
self.children.push(Separator(2));
return i === thisIndex ?
playerIcon(player) :
playerIcon(playerBox.attribute.player, overlay, playerBox);
})
.reverse();
});
};
const { Gdk } = imports.gi;
const display = Gdk.Display.get_default();
export const PositionSlider = (
player: MprisPlayer,
colors: Var<Colors>,
) => Slider({
class_name: 'position-slider',
vpack: 'center',
hexpand: true,
draw_value: false,
on_change: ({ value }) => {
player.position = player.length * value;
},
setup: (self) => {
const update = () => {
if (!self.dragging) {
self.visible = player.length > 0;
if (player.length > 0) {
self.value = player.position / player.length;
}
}
};
self
.poll(1000, () => update())
.hook(player, () => update(), 'position')
.hook(colors, () => {
if (colors.value) {
const c = colors.value;
self.setCss(`
highlight { background-color: ${c.buttonAccent}; }
slider { background-color: ${c.buttonAccent}; }
slider:hover { background-color: ${c.hoverAccent}; }
trough { background-color: ${c.buttonText}; }
`);
}
})
// OnClick
.on('button-press-event', () => {
if (!display) {
return;
}
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'grabbing',
));
})
// OnRelease
.on('button-release-event', () => {
if (!display) {
return;
}
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
})
// OnHover
.on('enter-notify-event', () => {
if (!display) {
return;
}
self.window.set_cursor(Gdk.Cursor.new_from_name(
display,
'pointer',
));
self.toggleClassName('hover', true);
})
// OnHoverLost
.on('leave-notify-event', () => {
self.window.set_cursor(null);
self.toggleClassName('hover', false);
});
},
});
const PlayerButton = ({
player,
colors,
children = {},
onClick,
prop,
}: PlayerButtonType) => CursorBox({
child: Button({
attribute: { hovered: false },
child: Stack({ children }),
on_primary_click_release: () => player[onClick](),
on_hover: (self) => {
self.attribute.hovered = true;
if (prop === 'playBackStatus' && colors.value) {
const c = colors.value;
Object.values(children).forEach((ch: AgsWidget) => {
ch.setCss(`
background-color: ${c.hoverAccent};
color: ${c.buttonText};
min-height: 40px;
min-width: 36px;
margin-bottom: 1px;
margin-right: 1px;
`);
});
}
},
on_hover_lost: (self) => {
self.attribute.hovered = false;
if (prop === 'playBackStatus' && colors.value) {
const c = colors.value;
Object.values(children).forEach((ch: AgsWidget) => {
ch.setCss(`
background-color: ${c.buttonAccent};
color: ${c.buttonText};
min-height: 42px;
min-width: 38px;
`);
});
}
},
setup: (self) => {
self
.hook(player, () => {
self.child.shown = `${player[prop]}`;
})
.hook(colors, () => {
if (!Mpris.players.find((p) => player === p)) {
return;
}
if (colors.value) {
const c = colors.value;
if (prop === 'playBackStatus') {
if (self.attribute.hovered) {
Object.values(children)
.forEach((ch: AgsWidget) => {
ch.setCss(`
background-color: ${c.hoverAccent};
color: ${c.buttonText};
min-height: 40px;
min-width: 36px;
margin-bottom: 1px;
margin-right: 1px;
`);
});
}
else {
Object.values(children)
.forEach((ch: AgsWidget) => {
ch.setCss(`
background-color: ${c.buttonAccent};
color: ${c.buttonText};
min-height: 42px;
min-width: 38px;
`);
});
}
}
else {
self.setCss(`
* { color: ${c.buttonAccent}; }
*:hover { color: ${c.hoverAccent}; }
`);
}
}
});
},
}),
});
export const ShuffleButton = (
player: MprisPlayer,
colors: Var<Colors>,
) => PlayerButton({
player,
colors,
children: {
true: Label({
class_name: 'shuffle enabled',
label: icons.mpris.shuffle.enabled,
}),
false: Label({
class_name: 'shuffle disabled',
label: icons.mpris.shuffle.disabled,
}),
},
onClick: 'shuffle',
prop: 'shuffleStatus',
});
export const LoopButton = (
player: MprisPlayer,
colors: Var<Colors>,
) => PlayerButton({
player,
colors,
children: {
None: Label({
class_name: 'loop none',
label: icons.mpris.loop.none,
}),
Track: Label({
class_name: 'loop track',
label: icons.mpris.loop.track,
}),
Playlist: Label({
class_name: 'loop playlist',
label: icons.mpris.loop.playlist,
}),
},
onClick: 'loop',
prop: 'loopStatus',
});
export const PlayPauseButton = (
player: MprisPlayer,
colors: Var<Colors>,
) => PlayerButton({
player,
colors,
children: {
Playing: Label({
class_name: 'pausebutton playing',
label: icons.mpris.playing,
}),
Paused: Label({
class_name: 'pausebutton paused',
label: icons.mpris.paused,
}),
Stopped: Label({
class_name: 'pausebutton stopped paused',
label: icons.mpris.stopped,
}),
},
onClick: 'playPause',
prop: 'playBackStatus',
});
export const PreviousButton = (
player: MprisPlayer,
colors: Var<Colors>,
) => PlayerButton({
player,
colors,
children: {
true: Label({
class_name: 'previous',
label: icons.mpris.prev,
}),
false: Label({
class_name: 'previous',
label: icons.mpris.prev,
}),
},
onClick: 'previous',
prop: 'canGoPrev',
});
export const NextButton = (
player: MprisPlayer,
colors: Var<Colors>,
) => PlayerButton({
player,
colors,
children: {
true: Label({
class_name: 'next',
label: icons.mpris.next,
}),
false: Label({
class_name: 'next',
label: icons.mpris.next,
}),
},
onClick: 'next',
prop: 'canGoNext',
});

View file

@ -1,201 +0,0 @@
const Mpris = await Service.import('mpris');
const { Box, CenterBox } = Widget;
import * as mpris from './mpris.ts';
import PlayerGesture from './gesture.ts';
import Separator from '../misc/separator.ts';
const FAVE_PLAYER = 'org.mpris.MediaPlayer2.spotify';
const SPACING = 8;
// Types
import { MprisPlayer } from 'types/service/mpris.ts';
import { Variable as Var } from 'types/variable';
import { Colors, PlayerBox, PlayerOverlay } from 'global-types';
const Top = (
player: MprisPlayer,
overlay: PlayerOverlay,
) => Box({
class_name: 'top',
hpack: 'start',
vpack: 'start',
children: [
mpris.PlayerIcon(player, overlay),
],
});
const Center = (
player: MprisPlayer,
colors: Var<Colors>,
) => Box({
class_name: 'center',
children: [
CenterBox({
vertical: true,
start_widget: Box({
class_name: 'metadata',
vertical: true,
hpack: 'start',
vpack: 'center',
hexpand: true,
children: [
mpris.TitleLabel(player),
mpris.ArtistLabel(player),
],
}),
}),
CenterBox({
vertical: true,
center_widget: mpris.PlayPauseButton(player, colors),
}),
],
});
const Bottom = (
player: MprisPlayer,
colors: Var<Colors>,
) => Box({
class_name: 'bottom',
children: [
mpris.PreviousButton(player, colors),
Separator(SPACING),
mpris.PositionSlider(player, colors),
Separator(SPACING),
mpris.NextButton(player, colors),
Separator(SPACING),
mpris.ShuffleButton(player, colors),
Separator(SPACING),
mpris.LoopButton(player, colors),
],
});
const PlayerBox = (
player: MprisPlayer,
colors: Var<Colors>,
overlay: PlayerOverlay,
) => {
const widget = mpris.CoverArt(player, colors, {
class_name: `player ${player.name}`,
hexpand: true,
start_widget: Top(player, overlay),
center_widget: Center(player, colors),
end_widget: Bottom(player, colors),
});
widget.visible = false;
return widget;
};
export default () => {
const content = PlayerGesture({
setup: (self: PlayerOverlay) => {
self
.hook(Mpris, (_, bus_name) => {
const players = self.attribute.players;
if (players.has(bus_name)) {
return;
}
// Sometimes the signal doesn't give the bus_name
if (!bus_name) {
const player = Mpris.players.find((p) => {
return !players.has(p.bus_name);
});
if (player) {
bus_name = player.bus_name;
}
else {
return;
}
}
// Get the one on top so we can move it up later
const previousFirst = self.overlays.at(-1) as PlayerBox;
// Make the new player
const player = Mpris.getPlayer(bus_name);
const colorsVar = Variable({
imageAccent: '#6b4fa2',
buttonAccent: '#ecdcff',
buttonText: '#25005a',
hoverAccent: '#d4baff',
});
if (!player) {
return;
}
players.set(
bus_name,
PlayerBox(player, colorsVar, self),
);
self.overlays = Array.from(players.values())
.map((widget) => widget) as PlayerBox[];
const includes = self.attribute
.includesWidget(previousFirst);
// Select favorite player at startup
const attrs = self.attribute;
if (!attrs.setup && players.has(FAVE_PLAYER)) {
attrs.moveToTop(players.get(FAVE_PLAYER));
attrs.setup = true;
}
// Move previousFirst on top again
else if (includes) {
attrs.moveToTop(previousFirst);
}
}, 'player-added')
.hook(Mpris, (_, bus_name) => {
const players = self.attribute.players;
if (!bus_name || !players.has(bus_name)) {
return;
}
// Get the one on top so we can move it up later
const previousFirst = self.overlays.at(-1) as PlayerBox;
// Remake overlays without deleted one
players.delete(bus_name);
self.overlays = Array.from(players.values())
.map((widget) => widget) as PlayerBox[];
// Move previousFirst on top again
const includes = self.attribute
.includesWidget(previousFirst);
if (includes) {
self.attribute.moveToTop(previousFirst);
}
}, 'player-closed');
},
});
return Box({
class_name: 'media',
child: content,
});
};

File diff suppressed because one or more lines are too long

45
packages/coloryou/coloryou.py Executable file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env python
"""
The original script:
https://github.com/dharmx/vile/blob/7d486c128c7e553912673755f97b118aaab0193d/src/shell/playerctl.py#L2
"""
import argparse
import json
from material_color_utilities_python import themeFromImage, hexFromArgb, Image
def range_type(value_string):
value = int(value_string)
if value not in range(0, 101):
raise argparse.ArgumentTypeError("%s is out of range, choose in [0-100]" % value)
return value
parser = argparse.ArgumentParser(
prog='coloryou',
description='This program extract Material You colors from an image. It returns them as a JSON object for scripting.')
parser.add_argument("image_path", help="A full path to your image", type=str)
parser.add_argument('-i', '--image', dest='image', default=40, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the main image accent.")
parser.add_argument('-b', '--button', dest='button', default=90, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the button accent.")
parser.add_argument('-t', '--text', dest='text', default=10, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the button text accent.")
parser.add_argument('-o', '--hover', dest='hover', default=80, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the hovering effect accent.")
args = parser.parse_args()
img = Image.open(args.image_path)
basewidth = 64
wpercent = (basewidth/float(img.size[0]))
hsize = int((float(img.size[1])*float(wpercent)))
img = img.resize((basewidth,hsize),Image.Resampling.LANCZOS)
theme = themeFromImage(img).get("palettes").get("primary")
parsed_colors = {"imageAccent": hexFromArgb(theme.tone(args.image)),
"buttonAccent": hexFromArgb(theme.tone(args.button)),
"buttonText": hexFromArgb(theme.tone(args.text)),
"hoverAccent": hexFromArgb(theme.tone(args.hover))}
print(json.dumps(parsed_colors))

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,45 @@
#!/usr/bin/env python
"""
The original script:
https://github.com/dharmx/vile/blob/7d486c128c7e553912673755f97b118aaab0193d/src/shell/playerctl.py#L2
"""
import argparse
import json
from material_color_utilities_python import themeFromImage, hexFromArgb, Image
def range_type(value_string):
value = int(value_string)
if value not in range(0, 101):
raise argparse.ArgumentTypeError("%s is out of range, choose in [0-100]" % value)
return value
parser = argparse.ArgumentParser(
prog='coloryou',
description='This program extract Material You colors from an image. It returns them as a JSON object for scripting.')
parser.add_argument("image_path", help="A full path to your image", type=str)
parser.add_argument('-i', '--image', dest='image', default=40, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the main image accent.")
parser.add_argument('-b', '--button', dest='button', default=90, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the button accent.")
parser.add_argument('-t', '--text', dest='text', default=10, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the button text accent.")
parser.add_argument('-o', '--hover', dest='hover', default=80, type=range_type, metavar='i', choices=range(0,101), help="Value should be within [0, 100] (default: %(default)s). Set the tone for the hovering effect accent.")
args = parser.parse_args()
img = Image.open(args.image_path)
basewidth = 64
wpercent = (basewidth/float(img.size[0]))
hsize = int((float(img.size[1])*float(wpercent)))
img = img.resize((basewidth,hsize),Image.Resampling.LANCZOS)
theme = themeFromImage(img).get("palettes").get("primary")
parsed_colors = {"imageAccent": hexFromArgb(theme.tone(args.image)),
"buttonAccent": hexFromArgb(theme.tone(args.button)),
"buttonText": hexFromArgb(theme.tone(args.text)),
"hoverAccent": hexFromArgb(theme.tone(args.hover))}
print(json.dumps(parsed_colors))

View file

@ -0,0 +1,20 @@
{ python3Packages }:
python3Packages.buildPythonPackage rec {
pname = "coloryou";
version = "0.0.1";
src = ./.;
propagatedBuildInputs = with python3Packages; [ utils material-color-utilities ];
postInstall = ''
mv -v $out/bin/coloryou.py $out/bin/coloryou
'';
meta = {
description = ''
Get Material You colors from an image.
'';
};
}

View file

@ -0,0 +1,2 @@
material-color-utilities
utils

View file

@ -0,0 +1,7 @@
from distutils.core import setup
setup(
name='coloryou',
version='0.0.1',
scripts=['coloryou.py',],
)

View file

@ -0,0 +1,8 @@
with import <nixpkgs> {};
with pkgs.python311Packages;
buildPythonPackage rec {
name = "coloryou";
src = ./.;
propagatedBuildInputs = [ material-color-utilities utils ];
}

View file

@ -0,0 +1,20 @@
{ python3Packages }:
python3Packages.buildPythonPackage rec {
pname = "coloryou";
version = "0.0.1";
src = ./.;
propagatedBuildInputs = with python3Packages; [ utils material-color-utilities ];
postInstall = ''
mv -v $out/bin/coloryou.py $out/bin/coloryou
'';
meta = {
description = ''
Get Material You colors from an image.
'';
};
}

View file

@ -0,0 +1,2 @@
material-color-utilities
utils

View file

@ -0,0 +1,7 @@
from distutils.core import setup
setup(
name='coloryou',
version='0.0.1',
scripts=['coloryou.py',],
)

View file

@ -0,0 +1,8 @@
with import <nixpkgs> {};
with pkgs.python311Packages;
buildPythonPackage rec {
name = "coloryou";
src = ./.;
propagatedBuildInputs = [ material-color-utilities utils ];
}

View file

@ -4,6 +4,8 @@
pkgs, pkgs,
... ...
}: { }: {
coloryou = pkgs.callPackage ./coloryou {};
gpu-screen-recorder = pkgs.callPackage ./gpu-screen-recorder/gpu-screen-recorder.nix { gpu-screen-recorder = pkgs.callPackage ./gpu-screen-recorder/gpu-screen-recorder.nix {
inherit (inputs) gpu-screen-recorder-src; inherit (inputs) gpu-screen-recorder-src;
}; };