Initial commit

This commit is contained in:
kossLAN 2025-06-07 04:01:14 -04:00
commit 05cd51b54e
Signed by: kossLAN
SSH key fingerprint: SHA256:bdV0x+wdQHGJ6LgmstH3KV8OpWY+OOFmJcPcB0wQPV8
148 changed files with 10112 additions and 0 deletions

View file

@ -0,0 +1,5 @@
# This directory is a Syncthing folder marker.
# Do not delete.
folderID: quickshell
created: 2024-12-23T02:09:06-05:00

View file

@ -0,0 +1,8 @@
pragma Singleton
import QtQuick
import Quickshell
Singleton {
property var screens: ["LG ULTRAGEAR+", "NE135A1M-NY1"]
}

View file

@ -0,0 +1,126 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
Scope {
id: root
property bool failed;
property string errorString;
// Connect to the Quickshell global to listen for the reload signals.
Connections {
target: Quickshell
function onReloadCompleted() {
root.failed = false;
popupLoader.loading = true;
}
function onReloadFailed(error: string) {
// Close any existing popup before making a new one.
popupLoader.active = false;
root.failed = true;
root.errorString = error;
popupLoader.loading = true;
}
}
// Keep the popup in a loader because it isn't needed most of the timeand will take up
// memory that could be used for something else.
LazyLoader {
id: popupLoader
PanelWindow {
id: popup
anchors {
top: true
left: true
}
margins {
top: 25
left: 25
}
width: rect.width
height: rect.height
// color blending is a bit odd as detailed in the type reference.
color: "black"
Rectangle {
id: rect
color: failed ? "#40802020" : "#40009020"
implicitHeight: layout.implicitHeight + 50
implicitWidth: layout.implicitWidth + 30
// Fills the whole area of the rectangle, making any clicks go to it,
// which dismiss the popup.
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked: popupLoader.active = false
// makes the mouse area track mouse hovering, so the hide animation
// can be paused when hovering.
hoverEnabled: true
}
ColumnLayout {
id: layout
anchors {
top: parent.top
topMargin: 20
horizontalCenter: parent.horizontalCenter
}
Text {
text: root.failed ? "Reload failed." : "Reloaded completed!"
color: "white"
}
Text {
text: root.errorString
color: "white"
// When visible is false, it also takes up no space.
visible: root.errorString != ""
}
}
// A progress bar on the bottom of the screen, showing how long until the
// popup is removed.
Rectangle {
id: bar
color: "#20ffffff"
anchors.bottom: parent.bottom
anchors.left: parent.left
height: 20
PropertyAnimation {
id: anim
target: bar
property: "width"
from: rect.width
to: 0
duration: failed ? 10000 : 800
onFinished: popupLoader.active = false
// Pause the animation when the mouse is hovering over the popup,
// so it stays onscreen while reading. This updates reactively
// when the mouse moves on and off the popup.
paused: mouseArea.containsMouse
}
}
// We could set `running: true` inside the animation, but the width of the
// rectangle might not be calculated yet, due to the layout.
// In the `Component.onCompleted` event handler, all of the component's
// properties and children have been initialized.
Component.onCompleted: anim.start()
}
}
}
}

View file

@ -0,0 +1,27 @@
pragma Singleton
import QtQuick
import Quickshell
Singleton {
SystemPalette { id: activePalette; colorGroup: SystemPalette.Active }
readonly property var colors: QtObject {
readonly property color accent: activePalette.accent;
readonly property color alternateBase: activePalette.alternateBase;
readonly property color base: activePalette.base;
readonly property color button: activePalette.button;
readonly property color buttonText: activePalette.button;
readonly property color dark: activePalette.dark;
readonly property color highlight: activePalette.highlight;
readonly property color textHighlight: activePalette.highlightedText;
readonly property color light: activePalette.light;
readonly property color mid: activePalette.mid;
readonly property color midlight: activePalette.midlight;
readonly property color shadow: activePalette.shadow;
readonly property color text: activePalette.text;
readonly property color window: activePalette.window;
readonly property color windowText: activePalette.windowText;
readonly property color innerHighlight: "#416563";
}
}

View file

@ -0,0 +1,32 @@
pragma Singleton
import QtQuick
import Quickshell
Singleton {
SystemPalette {
id: activePalette
colorGroup: SystemPalette.Active
}
readonly property var colors: QtObject {
readonly property color accent: "lightblue"
//readonly property color accent: "#5AA097"
readonly property color base: "#161616"
readonly property color mid: "#1E1F1F"
readonly property color light: "#353636"
//readonly property color button: activePalette.button
//readonly property color buttonText: activePalette.button
//readonly property color dark: activePalette.dark
readonly property color highlight: activePalette.highlight
//readonly property color textHighlight: activePalette.highlightedText
//readonly property color light: activePalette.light
//readonly property color mid: activePalette.mid
readonly property color midlight: activePalette.midlight
readonly property color text: activePalette.text
//readonly property color window: activePalette.window
//readonly property color innerHighlight: "#416563"
//readonly property color accent: "#5AA097"
}
}

View file

@ -0,0 +1,31 @@
import QtQuick
import Quickshell.Hyprland
import ".."
Rectangle {
width: 200;
height: parent.height;
Text {
id: windowText;
text: "";
color: ShellGlobals.colors.text;
font.pointSize: 11;
visible: text !== "";
elide: Text.ElideRight;
anchors {
left: parent.left
right: parent.right;
verticalCenter: verticalCenter.parent;
Connections {
target: Hyprland;
function onRawEvent(event) {
if (event.name === "activewindow") {
windowText.text = event.parse(2)[1];
}
}
}
}
}

View file

@ -0,0 +1,33 @@
import QtQuick
import Quickshell.Hyprland
import ".."
Rectangle {
width: 200;
height: parent.height;
color: "black"
Text {
id: windowText;
text: "";
color: ShellGlobals.colors.text;
font.pointSize: 11;
visible: text !== "";
elide: Text.ElideRight;
anchors {
left: parent.left
right: parent.right;
verticalCenter: verticalCenter.parent;
}
Connections {
target: Hyprland;
function onRawEvent(event) {
if (event.name === "activewindow") {
windowText.text = event.parse(2)[1];
}
}
}
}
}

View file

@ -0,0 +1,34 @@
import QtQuick
import Quickshell.Hyprland
import ".."
Rectangle {
width: 200;
height: parent.height;
color: "black"
Text {
id: windowText;
text: "";
color: ShellGlobals.colors.text;
font.pointSize: 11;
visible: text !== "";
elide: Text.ElideRight;
anchors {
left: parent.left
//right: parent.right;
verticalCenter: verticalCenter.parent;
}
Connections {
target: Hyprland;
function onRawEvent(event) {
if (event.name === "activewindow") {
windowText.text = event.parse(2)[1];
}
}
}
}
}

View file

@ -0,0 +1,34 @@
import QtQuick
import Quickshell.Hyprland
import ".."
Rectangle {
width: 200;
height: parent.height;
color: "black"
Text {
id: windowText;
text: "";
color: ShellGlobals.colors.text;
font.pointSize: 11;
visible: text !== "";
elide: Text.ElideRight;
//anchors {
// left: parent.left
// //right: parent.right;
// verticalCenter: verticalCenter.parent;
//}
Connections {
target: Hyprland;
function onRawEvent(event) {
if (event.name === "activewindow") {
windowText.text = event.parse(2)[1];
}
}
}
}
}

View file

@ -0,0 +1,22 @@
import QtQuick
import Quickshell.Hyprland
import ".."
Text {
id: windowText;
text: "";
color: ShellGlobals.colors.text;
font.pointSize: 11;
visible: text !== "";
elide: Text.ElideRight;
Connections {
target: Hyprland;
function onRawEvent(event) {
if (event.name === "activewindow") {
windowText.text = event.parse(2)[1];
}
}
}
}

View file

@ -0,0 +1,94 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import "mpris" as Mpris
import "notifications" as Notifications
import "control" as Control
import ".."
PanelWindow {
id: root;
color: ShellGlobals.colors.window;
height: 25
anchors {
top: true
left: true
right: true
}
// Notifications
Notifications.Notifications {
bar: root;
}
// Widgets - Everything here is sorted where it appears on the bar.
// Left
RowLayout {
spacing: 15;
anchors {
top: parent.top;
left: parent.left;
bottom: parent.bottom;
leftMargin: 10;
}
Workspaces {}
Separator {
visible: activeWindow.visible;
}
ActiveWindow {
id: activeWindow;
Layout.preferredWidth: 250;
}
}
// Middle
Mpris.MediaInfo {
id: mediaInfo;
bar: root;
anchors.centerIn: parent;
}
// Right
RowLayout {
spacing: 20;
anchors {
top: parent.top;
bottom: parent.bottom;
right: parent.right;
rightMargin: 10;
}
SysTray {
id: sysTray;
bar: root;
}
Separator {
visible: sysTray.visible
}
BatteryIndicator {
id: batteryIndicator
}
Control.Control {
bar: root;
}
Separator {}
Clock {
id: clock;
color: ShellGlobals.colors.text;
}
}
}

View file

@ -0,0 +1,98 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import "mpris" as Mpris
import "notifications" as Notifications
import "control" as Control
import ".."
PanelWindow {
id: root;
color: ShellGlobals.colors.window;
height: 25
anchors {
top: true
left: true
right: true
}
// Notifications
Notifications.Notifications {
bar: root;
}
// Widgets - Everything here is sorted where it appears on the bar.
// Left
RowLayout {
spacing: 15;
anchors {
top: parent.top;
left: parent.left;
bottom: parent.bottom;
leftMargin: 10;
}
Workspaces {}
Separator {
visible: activeWindow.visible;
}
ActiveWindow {
id: activeWindow;
Layout.preferredWidth: 250;
}
}
// Middle
Mpris.MediaInfo {
id: mediaInfo;
bar: root;
anchors.centerIn: parent;
}
// Right
RowLayout {
spacing: 20;
anchors {
top: parent.top;
bottom: parent.bottom;
right: parent.right;
rightMargin: 10;
}
SysTray {
id: sysTray;
bar: root;
}
Separator {
visible: sysTray.visible
}
RowLayout {
spacing: 15;
BatteryIndicator {
id: batteryIndicator
}
Control.Control {
bar: root;
}
}
Separator {}
Clock {
id: clock;
color: ShellGlobals.colors.text;
}
}
}

View file

@ -0,0 +1,94 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import "mpris" as Mpris
import "notifications" as Notifications
import "control" as Control
import ".."
PanelWindow {
id: root;
color: ShellGlobals.colors.window;
height: 25
anchors {
top: true
left: true
right: true
}
// Notifications
Notifications.Notifications {
bar: root;
}
// Widgets - Everything here is sorted where it appears on the bar.
// Left
RowLayout {
spacing: 15;
anchors {
top: parent.top;
left: parent.left;
bottom: parent.bottom;
leftMargin: 10;
}
Workspaces {}
Separator {
visible: activeWindow.visible;
}
ActiveWindow {
id: activeWindow;
Layout.preferredWidth: 250;
}
}
// Middle
Mpris.MediaInfo {
id: mediaInfo;
bar: root;
anchors.centerIn: parent;
}
// Right
RowLayout {
spacing: 15;
anchors {
top: parent.top;
bottom: parent.bottom;
right: parent.right;
rightMargin: 10;
}
SysTray {
id: sysTray;
bar: root;
}
Separator {
visible: sysTray.visible
}
BatteryIndicator {
id: batteryIndicator
}
Control.Control {
bar: root;
}
Separator {}
Clock {
id: clock;
color: ShellGlobals.colors.text;
}
}
}

View file

@ -0,0 +1,97 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import "mpris" as Mpris
import "notifications" as Notifications
import "control" as Control
import ".."
PanelWindow {
id: root;
color: ShellGlobals.colors.window;
height: 25
anchors {
top: true
left: true
right: true
}
// Notifications
Notifications.Notifications {
bar: root;
}
// Widgets - Everything here is sorted where it appears on the bar.
// Left
RowLayout {
spacing: 15;
anchors {
top: parent.top;
left: parent.left;
bottom: parent.bottom;
leftMargin: 10;
}
Workspaces {}
Separator {
visible: activeWindow.visible;
}
ActiveWindow {
id: activeWindow;
Layout.preferredWidth: 250;
}
}
// Middle
Mpris.MediaInfo {
id: mediaInfo;
bar: root;
anchors.centerIn: parent;
}
// Right
RowLayout {
spacing: 15;
anchors {
top: parent.top;
bottom: parent.bottom;
right: parent.right;
rightMargin: 10;
}
SysTray {
id: sysTray;
bar: root;
}
Separator {
visible: sysTray.visible
}
RowLayout {
spacing: 5;
BatteryIndicator {
id: batteryIndicator
}
Control.Control {
bar: root;
}
}
Separator {}
Clock {
id: clock;
color: ShellGlobals.colors.text;
}
}
}

View file

@ -0,0 +1,93 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import "mpris" as Mpris
import "volume" as Volume
import "../widgets" as Widgets
import ".."
PanelWindow {
id: root
color: ShellGlobals.colors.base
height: 25
anchors {
top: true
left: true
right: true
}
/// Widgets - Everything here is sorted where it appears on the bar.
// Left
RowLayout {
spacing: 15
anchors {
top: parent.top
left: parent.left
bottom: parent.bottom
leftMargin: 10
}
// Whatever is available will display
HyprWorkspaces {}
SwayWorkspaces {}
Widgets.Separator {
visible: activeWindow.visible
}
ActiveWindow {
id: activeWindow
Layout.preferredWidth: 250
}
}
// Middle
Mpris.Status {
id: mprisStatus
bar: root
anchors.centerIn: parent
}
// Right
RowLayout {
spacing: 15
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
rightMargin: 10
}
SysTray {
id: sysTray
bar: root
}
Widgets.Separator {
visible: sysTray.visible
}
RowLayout {
spacing: 5
BatteryIndicator {
id: batteryIndicator
}
Volume.Button {
bar: root
}
}
Widgets.Separator {}
Clock {
id: clock
color: ShellGlobals.colors.text
}
}
}

View file

@ -0,0 +1,38 @@
import QtQuick
import Quickshell.Widgets
import Quickshell.Services.UPower
import ".."
Item {
property string batteryStatus: {
if (!UPower.onBattery) {
return "charging";
}
let percentage = UPower.displayDevice.percentage * 100;
let roundedValue = Math.floor(percentage / 5) * 5;
return roundedValue.toString();
}
width: 30;
height: parent.height;
visible: UPower.displayDevice.isLaptopBattery;
Rectangle {
color: ShellGlobals.colors.highlight;
width: 12;
height: 8;
visible: batteryStatus === "charging";
anchors {
centerIn: batteryImage;
}
}
IconImage {
id: batteryImage;
implicitSize: 20;
source: Qt.resolvedUrl(`../resources/battery/battery-${batteryStatus}.svg`);
anchors.centerIn: parent;
}
}

View file

@ -0,0 +1,20 @@
import QtQuick
import Quickshell
Text {
property string ap: sysClock.hours >= 12 ? "PM" : "AM";
property string minutes: sysClock.minutes.toString().padStart(2, '0');
property string hours: {
var value = sysClock.hours % 12;
if (value === 0) return 12;
return value;
}
SystemClock {
id: sysClock;
enabled: true;
}
text: `${hours}:${minutes} ${ap}`
font.pointSize: 11;
}

View file

@ -0,0 +1,51 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import ".."
RowLayout {
property var sortedWorkspaces: {
let values = Hyprland.workspaces.values.slice();
values.sort(function (a, b) {
return a.id - b.id;
});
return values;
}
spacing: 6
visible: Hyprland.monitors.values.length != 0
Repeater {
model: parent.sortedWorkspaces
Rectangle {
required property var modelData
width: 25
height: 12
radius: 10
color: {
let value = ShellGlobals.colors.light;
if (!modelData?.id || !Hyprland.focusedMonitor?.activeWorkspace?.id)
return value;
if (workspaceButton.containsMouse) {
value = ShellGlobals.colors.midlight;
} else if (Hyprland.focusedMonitor.activeWorkspace.id == modelData.id) {
value = ShellGlobals.colors.accent;
}
return value;
}
MouseArea {
id: workspaceButton
anchors.fill: parent
hoverEnabled: true
onPressed: Hyprland.dispatch(`workspace ${parent.modelData.id}`)
}
}
}
}

View file

@ -0,0 +1,9 @@
import QtQuick
import ".."
Rectangle {
color: ShellGlobals.colors.highlight;
radius: 5;
width: 7.5;
height: 7.5;
}

View file

@ -0,0 +1,56 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.I3
import ".."
RowLayout {
property var sortedWorkspaces: {
let values = I3.workspaces.values.slice();
values.sort(function (a, b) {
if (!a?.num)
return 1;
if (!b?.num)
return -1;
return a.num - b.num;
});
return values;
}
spacing: 6
visible: I3.monitors.values.length != 0
Repeater {
model: parent.sortedWorkspaces
Rectangle {
required property var modelData
width: 25
height: 12
radius: 10
color: getColor(modelData, workspaceButton.containsMouse)
MouseArea {
id: workspaceButton
anchors.fill: parent
hoverEnabled: true
onPressed: I3.dispatch(`workspace number ${parent.modelData.num}`)
}
}
}
function getColor(modelData, isHovered) {
if (!modelData?.id || !I3.focusedMonitor?.focusedWorkspace?.num)
return ShellGlobals.colors.light;
if (isHovered)
return ShellGlobals.colors.midlight;
if (I3.focusedMonitor.focusedWorkspace.num == modelData.num)
return ShellGlobals.colors.accent;
return ShellGlobals.colors.light;
}
}

View file

@ -0,0 +1,26 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Widgets
import Quickshell.Services.SystemTray
import "../widgets" as Widgets
import ".."
RowLayout {
required property var bar;
spacing: 10;
visible: SystemTray.items.values.length > 0
Repeater {
model: SystemTray.items;
Widgets.IconButton {
id: iconButton;
implicitSize: 20;
source: modelData.icon;
onClicked: modelData.display(bar, -parent.mapFromGlobal(0, 0).x, root.height+5);
}
}
}

View file

@ -0,0 +1,41 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.SystemTray
import "../widgets" as Widgets
import ".."
RowLayout {
id: root
required property var bar
spacing: 10
visible: SystemTray.items.values.length > 0
Repeater {
model: SystemTray.items
Widgets.IconButton {
id: iconButton
implicitSize: 20
source: modelData.icon
padding: 0
QsMenuAnchor {
id: menuAnchor
menu: modelData.menu
anchor {
window: bar
adjustment: PopupAdjustment.Flip
onAnchoring: {
anchor.rect = mapToItem(bar.contentItem, -2, height + 4, width + 2, 0);
}
}
}
onClicked: menuAnchor.open()
}
}
}

View file

@ -0,0 +1,49 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import Quickshell.Io
import ".."
RowLayout {
property var sortedWorkspaces: {
let values = Hyprland.workspaces.values.slice();
values.sort(function(a, b) { return a.id - b.id; });
return values;
};
spacing: 6;
Repeater {
model: sortedWorkspaces;
Rectangle {
required property var modelData;
width: 25;
height: 12;
radius: 10;
color: {
let value = ShellGlobals.colors.light;
if (!modelData?.id || !Hyprland.focusedMonitor?.activeWorkspace?.id)
return value;
if (workspaceButton.containsMouse) {
value = ShellGlobals.colors.midlight;
} else if (Hyprland.focusedMonitor.activeWorkspace.id == modelData.id) {
value = ShellGlobals.colors.highlight;
}
return value;
}
MouseArea {
id: workspaceButton;
anchors.fill: parent;
hoverEnabled: true;
onPressed: Hyprland.dispatch('workspace ' + modelData.id);
}
}
}
}

View file

@ -0,0 +1,74 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell.Widgets
import "../.."
Item {
property string source;
property string text: "";
property string subText: "";
property real implicitSize; // icon implicit size
property real padding: 0;
property real radius: 5;
signal clicked();
id: root;
width: implicitSize*3;
height: implicitSize*1.25;
Rectangle {
id: iconBackground;
color: iconButton.containsMouse
? ShellGlobals.colors.innerHighlight
: ShellGlobals.colors.midlight;
border.color: iconButton.containsMouse
? ShellGlobals.colors.highlight
: ShellGlobals.colors.light;
radius: root.radius;
anchors.fill: parent;
RowLayout {
spacing: 5;
anchors {
fill: parent;
margins: root.padding;
}
IconImage {
id: iconImage;
implicitSize: root.implicitSize;
source: root.source;
}
ColumnLayout {
id: textLayout;
spacing: 3;
Layout.fillWidth: true;
Text {
text: root.text;
color: ShellGlobals.colors.text;
font.pointSize: 11;
font.bold: true;
visible: text.length > 0;
}
Text {
text: root.subText;
color: ShellGlobals.colors.text;
font.pointSize: 10;
visible: text.length > 0;
}
}
}
MouseArea {
id: iconButton;
hoverEnabled: true;
anchors.fill: parent;
onPressed: root.clicked();
}
}
}

View file

@ -0,0 +1,96 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Io
import Quickshell.Services.UPower
import "../../widgets" as Widgets
import "../.."
PopupWindow {
id: root;
width: controlContainer.implicitWidth+25
height: controlContainer.implicitHeight+25
//width: 275;
//height: 400;
color: "transparent"
visible: controlContainer.opacity > 0;
function show(x, y) {
root.anchor.rect.x = x;
root.anchor.rect.y = y;
controlContainer.opacity = 1;
}
function hide() {
controlContainer.opacity = 0;
}
HoverHandler {
id: hoverHandler;
enabled: true;
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad;
onHoveredChanged: {
if (hovered === false) {
hide();
}
}
}
Rectangle {
id: controlContainer;
color: ShellGlobals.colors.window;
radius: 5;
opacity: 0; // TODO: change to 0
layer.enabled: true;
layer.effect: DropShadow {
transparentBorder: true;
spread: 0.02;
samples: 25;
color: "#80000000";
}
implicitWidth: columnLayout.implicitWidth + 20 // Add margins
implicitHeight: columnLayout.implicitHeight + 20 // Add margins
anchors {
centerIn: parent;
margins: 5;
}
Behavior on opacity {
NumberAnimation {
duration: 300;
easing.type: Easing.OutCubic;
}
}
ColumnLayout {
id: columnLayout
spacing: 10;
anchors {
left: parent.left
right: parent.right
top: parent.top
bottom: parent.bottom
margins: 10 // Padding from the parent rectangle
}
RowLayout {
spacing: 10;
Rectangle {
width: 120;
height: 120;
}
Rectangle {
width: 120;
height: 120;
}
}
}
}
}

View file

@ -0,0 +1,64 @@
import QtQuick
import Qt5Compat.GraphicalEffects
import QtQuick.Controls
import Quickshell.Widgets
import "../.."
Slider {
id: slider;
from: 0;
to: 100;
value: 50;
background: Rectangle {
id: sliderContainer;
width: slider.availableWidth;
height: slider.availableHeight;
color: "#e0e0e0";
radius: 10;
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: sliderContainer.width;
height: sliderContainer.height;
radius: sliderContainer.radius;
color: "white";
}
maskSource: Rectangle {
width: sliderContainer.width;
height: sliderContainer.height;
radius: sliderContainer.radius;
color: "black";
}
}
Rectangle {
id: handle;
width: sliderContainer.width * (slider.value / slider.to);
height: sliderContainer.height;
color: ShellGlobals.colors.highlight;
Behavior on width {
NumberAnimation {
duration: 100;
easing.type: Easing.OutQuad;
}
}
}
//IconImage {
// implicitSize: 20;
// source: "root:resources/control/sleep.svg"
//
// anchors {
// verticalCenter: parent.verticalCenter;
// left: parent.left;
// leftMargin: 15;
// }
//}
}
handle: Item { }
}

View file

@ -0,0 +1,70 @@
import QtQuick
import Qt5Compat.GraphicalEffects
import QtQuick.Controls
import Quickshell.Widgets
import "../.."
Slider {
id: slider;
from: 0;
to: 100;
value: 50;
orientation: Qt.Vertical;
background: Rectangle {
id: sliderContainer;
width: slider.availableWidth;
height: slider.availableHeight;
color: "#e0e0e0";
radius: 10;
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: sliderContainer.width;
height: sliderContainer.height;
radius: sliderContainer.radius;
color: "white";
}
maskSource: Rectangle {
width: sliderContainer.width;
height: sliderContainer.height;
radius: sliderContainer.radius;
color: "black";
}
}
Rectangle {
id: handle;
width: sliderContainer.width;
height: sliderContainer.height * (slider.value / slider.to);
color: ShellGlobals.colors.highlight;
anchors {
bottom: sliderContainer.bottom;
horizontalCenter: sliderContainer.horizontalCenter;
}
Behavior on height {
NumberAnimation {
duration: 100;
easing.type: Easing.OutQuad;
}
}
}
//IconImage {
// implicitSize: 20;
// source: "root:resources/control/sleep.svg"
//
// anchors {
// verticalCenter: parent.verticalCenter;
// left: parent.left;
// leftMargin: 15;
// }
//}
}
handle: Item { }
}

View file

@ -0,0 +1,32 @@
import QtQuick
import Quickshell
import Quickshell.Widgets
import "../../widgets" as Widgets
import "../.."
Widgets.IconButton {
required property var bar;
id: root;
implicitSize: 20;
padding: 2;
source: "root:/resources/control/controls-button.svg";
onClicked: {
if (controlLoader.item.visible) {
controlLoader.item.hide();
} else {
controlLoader.item.show(-root.mapFromGlobal(0, 0).x, bar.height);
}
}
LazyLoader {
id: controlLoader;
loading: true;
ControlPanel {
id: controlPanel;
anchor.window: bar;
}
}
}

View file

@ -0,0 +1,238 @@
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import "../.."
import "../../widgets" as Widgets
Rectangle {
required property var player
radius: 5
color: "transparent"
implicitHeight: 220
RowLayout {
id: cardLayout
spacing: 15
anchors {
fill: parent
leftMargin: 10
rightMargin: 10
topMargin: 10 // Added top margin for better spacing
bottomMargin: 10 // Added bottom margin for better spacing
}
Rectangle {
id: mprisImage
color: "transparent"
radius: 10
width: 200
height: 200
Layout.alignment: Qt.AlignVCenter
visible: true
Image {
anchors.fill: parent
source: player.trackArtUrl
sourceSize.width: 256
sourceSize.height: 256
fillMode: Image.PreserveAspectFit
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: mprisImage.width
height: mprisImage.height
radius: 10
color: "white"
}
maskSource: Rectangle {
width: mprisImage.width
height: mprisImage.height
radius: 10
color: "black"
}
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
spread: 0.02
samples: 25
color: "#80000000"
}
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 5
Text {
text: player.trackArtist
color: ShellGlobals.colors.text
font.pointSize: 13
font.bold: true
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
elide: Text.ElideRight
}
Text {
text: player.trackTitle
color: ShellGlobals.colors.text
font.pointSize: 13
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
elide: Text.ElideRight
}
RowLayout {
spacing: 2
Text {
text: timeStr(player.position)
color: ShellGlobals.colors.text
font {
pointSize: 9
bold: true
}
}
ColorQuantizer {
id: colorQuantizer
source: Qt.resolvedUrl(Media.trackedPlayer?.trackArtUrl ?? "")
depth: 0
rescaleSize: 64
}
Slider {
id: slider
from: 0
to: player.length
enabled: false
//enabled: player.canSeek
value: player.position
implicitHeight: 7
Layout.fillWidth: true
Layout.margins: 10
Layout.leftMargin: 5
Layout.rightMargin: 5
Layout.alignment: Qt.AlignBottom
background: Rectangle {
id: sliderContainer
width: slider.availableWidth
height: slider.implicitHeight
color: "white"
radius: 4
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: sliderContainer.width
height: sliderContainer.height
radius: sliderContainer.radius
color: "white"
}
maskSource: Rectangle {
width: sliderContainer.width
height: sliderContainer.height
radius: sliderContainer.radius
color: "black"
}
}
Rectangle {
id: handle
width: sliderContainer.width * (slider.value / slider.to)
height: sliderContainer.height
color: colorQuantizer.colors[0].darker(1.2)
Behavior on width {
NumberAnimation {
duration: 100
easing.type: Easing.OutQuad
}
}
}
}
handle: Rectangle {
x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
y: slider.topPadding + slider.availableHeight / 2 - height / 2
width: 16
height: 16
radius: width / 2
color: colorQuantizer.colors[0].darker(1.4)
layer.enabled: true
layer.effect: DropShadow {
horizontalOffset: 0
verticalOffset: 1
radius: 4.0
samples: 9
color: "#30000000"
}
}
}
Text {
text: timeStr(player.length)
color: ShellGlobals.colors.text
font {
pointSize: 9
bold: true
}
}
}
// Music Controls
RowLayout {
spacing: 2
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Widgets.IconButton {
implicitSize: 36
padding: 4
source: "root:resources/mpris/previous.svg"
onClicked: player.previous()
}
Widgets.IconButton {
implicitSize: 36
padding: 4
source: player?.isPlaying ? "root:resources/mpris/pause.svg" : "root:resources/mpris/play.svg"
onClicked: {
if (!player.canPlay)
return;
player.isPlaying ? player.pause() : player.play();
}
}
Widgets.IconButton {
implicitSize: 36
padding: 4
source: "root:resources/mpris/next.svg"
onClicked: player.next()
}
}
}
}
function timeStr(time: int): string {
const seconds = time % 60;
const minutes = Math.floor(time / 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
}

View file

@ -0,0 +1,95 @@
import QtQuick
import Quickshell.Services.Mpris
import Quickshell.Widgets
import "../.."
Item {
required property var bar;
width: statusInfo.width;
height: parent.height;
MediaSwitcher {
id: mediaSwitcher;
anchor.window: bar;
}
MouseArea {
id: playButton;
hoverEnabled: true;
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse)=> {
if (mouse.button === Qt.LeftButton) {
mediaSwitcher.visible = !mediaSwitcher.visible;
} else {
if (!Media.trackedPlayer.canPlay || Media.trackedPlayer == null)
return;
if (Media.trackedPlayer.isPlaying)
Media.trackedPlayer.pause();
else
Media.trackedPlayer.play();
}
}
anchors.fill: parent;
}
Item {
id: statusInfo;
width: statusIcon.width + statusIcon.anchors.rightMargin + nowPlayingText.width;
visible: Media.trackedPlayer != null;
anchors {
horizontalCenter: parent.horizontalCenter;
verticalCenter: parent.verticalCenter;
top: parent.top;
bottom: parent.botton;
margins: 3.5;
}
Rectangle {
color: ShellGlobals.colors.innerHighlight;
border.color: ShellGlobals.colors.highlight;
radius: 5;
width: parent.width + 25;
height: parent.height;
visible: playButton.containsMouse;
anchors.centerIn: parent;
}
IconImage {
id: statusIcon;
implicitSize: 13;
source: Media.trackedPlayer?.isPlaying
? Qt.resolvedUrl("../../resources/mpris/pause.svg")
: Qt.resolvedUrl("../../resources/mpris/play.svg");
anchors {
verticalCenter: parent.verticalCenter;
right: nowPlayingText.left;
rightMargin: 10;
}
}
Text {
id: nowPlayingText
color: ShellGlobals.colors.text;
text: `${Media.trackedPlayer?.trackArtist} - ${Media.trackedPlayer?.trackTitle}`;
font.pointSize: 11;
elide: Text.ElideRight;
anchors {
verticalCenter: parent.verticalCenter;
right: parent.right;
}
}
}
function truncate(text) {
if (text?.length > 40) {
return text.substring(0, 40) + " ..."
}
return text
}
}

View file

@ -0,0 +1,96 @@
import QtQuick
import Quickshell.Services.Mpris
import Quickshell.Widgets
import "../.."
Item {
required property var bar;
width: statusInfo.width;
height: parent.height;
MediaSwitcher {
id: mediaSwitcher;
anchor.window: bar;
}
MouseArea {
id: playButton;
hoverEnabled: true;
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse)=> {
if (mouse.button === Qt.LeftButton) {
mediaSwitcher.visible = !mediaSwitcher.visible;
} else {
if (!Media.trackedPlayer.canPlay || Media.trackedPlayer == null)
return;
if (Media.trackedPlayer.isPlaying)
Media.trackedPlayer.pause();
else
Media.trackedPlayer.play();
}
}
anchors.fill: parent;
}
Item {
id: statusInfo;
width: statusIcon.width + statusIcon.anchors.rightMargin + nowPlayingText.width;
visible: Media.trackedPlayer != null;
anchors {
horizontalCenter: parent.horizontalCenter;
verticalCenter: parent.verticalCenter;
top: parent.top;
bottom: parent.botton;
margins: 3.5;
}
Rectangle {
color: ShellGlobals.colors.innerHighlight;
border.color: ShellGlobals.colors.highlight;
radius: 5;
width: parent.width + 25;
height: parent.height;
visible: playButton.containsMouse;
anchors.centerIn: parent;
}
IconImage {
id: statusIcon;
implicitSize: 13;
source: Media.trackedPlayer?.isPlaying
? Qt.resolvedUrl("../../resources/mpris/pause.svg")
: Qt.resolvedUrl("../../resources/mpris/play.svg");
anchors {
verticalCenter: parent.verticalCenter;
right: nowPlayingText.left;
rightMargin: 10;
}
}
Text {
id: nowPlayingText
color: ShellGlobals.colors.text;
text: `${Media.trackedPlayer?.trackArtist} - ${Media.trackedPlayer?.trackTitle}`;
font.pointSize: 11;
elide: Text.ElideRight;
contentWidth: 100;
anchors {
verticalCenter: parent.verticalCenter;
right: parent.right;
}
}
}
function truncate(text) {
if (text?.length > 40) {
return text.substring(0, 40) + " ..."
}
return text
}
}

View file

@ -0,0 +1,96 @@
import QtQuick
import Quickshell.Services.Mpris
import Quickshell.Widgets
import "../.."
Item {
required property var bar;
width: statusInfo.width;
height: parent.height;
MediaSwitcher {
id: mediaSwitcher;
anchor.window: bar;
}
MouseArea {
id: playButton;
hoverEnabled: true;
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse)=> {
if (mouse.button === Qt.LeftButton) {
mediaSwitcher.visible = !mediaSwitcher.visible;
} else {
if (!Media.trackedPlayer.canPlay || Media.trackedPlayer == null)
return;
if (Media.trackedPlayer.isPlaying)
Media.trackedPlayer.pause();
else
Media.trackedPlayer.play();
}
}
anchors.fill: parent;
}
Item {
id: statusInfo;
width: statusIcon.width + statusIcon.anchors.rightMargin + nowPlayingText.width;
visible: Media.trackedPlayer != null;
anchors {
horizontalCenter: parent.horizontalCenter;
verticalCenter: parent.verticalCenter;
top: parent.top;
bottom: parent.botton;
margins: 3.5;
}
Rectangle {
color: ShellGlobals.colors.innerHighlight;
border.color: ShellGlobals.colors.highlight;
radius: 5;
width: parent.width + 25;
height: parent.height;
visible: playButton.containsMouse;
anchors.centerIn: parent;
}
IconImage {
id: statusIcon;
implicitSize: 13;
source: Media.trackedPlayer?.isPlaying
? Qt.resolvedUrl("../../resources/mpris/pause.svg")
: Qt.resolvedUrl("../../resources/mpris/play.svg");
anchors {
verticalCenter: parent.verticalCenter;
right: nowPlayingText.left;
rightMargin: 10;
}
}
Text {
id: nowPlayingText
color: ShellGlobals.colors.text;
text: `${Media.trackedPlayer?.trackArtist} - ${Media.trackedPlayer?.trackTitle}`;
font.pointSize: 11;
elide: Text.ElideRight;
width: 100;
anchors {
verticalCenter: parent.verticalCenter;
right: parent.right;
}
}
}
function truncate(text) {
if (text?.length > 40) {
return text.substring(0, 40) + " ..."
}
return text
}
}

View file

@ -0,0 +1,96 @@
import QtQuick
import Quickshell.Services.Mpris
import Quickshell.Widgets
import "../.."
Item {
required property var bar;
width: statusInfo.width;
height: parent.height;
MediaSwitcher {
id: mediaSwitcher;
anchor.window: bar;
}
MouseArea {
id: playButton;
hoverEnabled: true;
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse)=> {
if (mouse.button === Qt.LeftButton) {
mediaSwitcher.visible = !mediaSwitcher.visible;
} else {
if (!Media.trackedPlayer.canPlay || Media.trackedPlayer == null)
return;
if (Media.trackedPlayer.isPlaying)
Media.trackedPlayer.pause();
else
Media.trackedPlayer.play();
}
}
anchors.fill: parent;
}
Item {
id: statusInfo;
width: statusIcon.width + statusIcon.anchors.rightMargin + nowPlayingText.width;
visible: Media.trackedPlayer != null;
anchors {
horizontalCenter: parent.horizontalCenter;
verticalCenter: parent.verticalCenter;
top: parent.top;
bottom: parent.botton;
margins: 3.5;
}
Rectangle {
color: ShellGlobals.colors.innerHighlight;
border.color: ShellGlobals.colors.highlight;
radius: 5;
width: parent.width + 25;
height: parent.height;
visible: playButton.containsMouse;
anchors.centerIn: parent;
}
IconImage {
id: statusIcon;
implicitSize: 13;
source: Media.trackedPlayer?.isPlaying
? Qt.resolvedUrl("../../resources/mpris/pause.svg")
: Qt.resolvedUrl("../../resources/mpris/play.svg");
anchors {
verticalCenter: parent.verticalCenter;
right: nowPlayingText.left;
rightMargin: 10;
}
}
Text {
id: nowPlayingText
color: ShellGlobals.colors.text;
text: `${Media.trackedPlayer?.trackArtist} - ${Media.trackedPlayer?.trackTitle}`;
font.pointSize: 11;
width: 250;
elide: Text.ElideRight;
anchors {
verticalCenter: parent.verticalCenter;
right: parent.right;
}
}
}
function truncate(text) {
if (text?.length > 40) {
return text.substring(0, 40) + " ..."
}
return text
}
}

View file

@ -0,0 +1,96 @@
import QtQuick
import Quickshell.Services.Mpris
import Quickshell.Widgets
import "../.."
Item {
required property var bar;
width: statusInfo.width;
height: parent.height;
MediaSwitcher {
id: mediaSwitcher;
anchor.window: bar;
anchor.rect.x: parentWindow.width / 2 - width / 2;
anchor.rect.y: parentWindow.height;
}
MouseArea {
id: playButton;
hoverEnabled: true;
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: (mouse)=> {
if (mouse.button === Qt.LeftButton) {
if (mediaSwitcher.visible) {
mediaSwitcher.hide();
} else {
mediaSwitcher.show();
}
//mediaSwitcher.visible = !mediaSwitcher.visible;
} else {
if (!Media.trackedPlayer.canPlay || Media.trackedPlayer == null)
return;
if (Media.trackedPlayer.isPlaying)
Media.trackedPlayer.pause();
else
Media.trackedPlayer.play();
}
}
anchors.fill: parent;
}
Item {
id: statusInfo;
width: statusIcon.width + statusIcon.anchors.rightMargin + nowPlayingText.width;
visible: Media.trackedPlayer != null;
anchors {
horizontalCenter: parent.horizontalCenter;
verticalCenter: parent.verticalCenter;
top: parent.top;
bottom: parent.botton;
margins: 3.5;
}
Rectangle {
color: ShellGlobals.colors.innerHighlight;
border.color: ShellGlobals.colors.highlight;
radius: 3;
width: parent.width + 25;
height: parent.height;
visible: playButton.containsMouse;
anchors.centerIn: parent;
}
IconImage {
id: statusIcon;
implicitSize: 13;
source: Media.trackedPlayer?.isPlaying
? "root:resources/mpris/pause.svg"
: "root:resources/mpris/play.svg";
anchors {
verticalCenter: parent.verticalCenter;
right: nowPlayingText.left;
rightMargin: 10;
}
}
Text {
id: nowPlayingText
color: ShellGlobals.colors.text;
text: `${Media.trackedPlayer?.trackArtist} - ${Media.trackedPlayer?.trackTitle}`;
font.pointSize: 11;
width: Math.min(implicitWidth, 250);
elide: Text.ElideRight;
anchors {
verticalCenter: parent.verticalCenter;
right: parent.right;
}
}
}
}

View file

@ -0,0 +1,122 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import "../.."
PopupWindow {
id: root;
width: 500
height: 500
color: "transparent";
visible: false;
anchor.rect.x: parentWindow.width / 2 - width / 2
anchor.rect.y: parentWindow.height + 5;
Rectangle {
color: ShellGlobals.colors.window;
radius:5;
border.color: mouse.hovered
? ShellGlobals.colors.highlight
: ShellGlobals.colors.light;
border.width: 2;
anchors.fill: parent;
// NOTE: You cannot stack mouseArea's that have hovered enabled.
// This is the workaround for panel hover detection.
HoverHandler {
id: mouse
enabled: true;
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad;
onHoveredChanged: {
if (hovered == false) {
root.visible = false;
}
}
}
ColumnLayout {
spacing: 5;
anchors {
horizontalCenter: parent.horizontalCenter;
top: parent.top;
margins: 20;
}
Image {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: 300
Layout.preferredHeight: 300
source: Media.trackedPlayer.trackArtUrl
fillMode: Image.PreserveAspectFit
sourceSize {
width: 512
height: 512
}
}
Text {
text: truncate(Media.trackedPlayer.trackArtist);
color: ShellGlobals.colors.text;
font.pointSize: 13;
Layout.alignment: Qt.AlignHCenter;
Layout.topMargin: 20;
}
Text {
text: truncate(Media.trackedPlayer.trackTitle);
color: ShellGlobals.colors.text;
font.pointSize: 13;
Layout.alignment: Qt.AlignHCenter;
}
RowLayout {
spacing: 20;
Layout.alignment: Qt.AlignHCenter;
Layout.topMargin: 10;
IconButton {
implicitSize: 32
source: Qt.resolvedUrl("../../resources/mpris/previous.svg");
onClicked: {
Media.trackedPlayer.previous();
}
}
IconButton {
implicitSize: 36;
source: Media.trackedPlayer?.isPlaying
? Qt.resolvedUrl("../../resources/mpris/pause.svg")
: Qt.resolvedUrl("../../resources/mpris/play.svg");
onClicked: {
if (!Media.trackedPlayer.canPlay || Media.trackedPlayer == null)
return;
if (Media.trackedPlayer.isPlaying)
Media.trackedPlayer.pause();
else
Media.trackedPlayer.play();
}
}
IconButton {
implicitSize: 32;
source: Qt.resolvedUrl("../../resources/mpris/next.svg");
onClicked: {
Media.trackedPlayer.next();
}
}
}
}
}
// TODO: make this some sort of global function
function truncate(text) {
if (text?.length > 60) {
return text.substring(0, 60) + " ..."
}
return text
}
}

View file

@ -0,0 +1,179 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Services.Mpris
import "../.."
PopupWindow {
id: root
width: mediaPlayerContainer.width + 15;
height: mediaPlayerContainer.height + 15;
color: "transparent"
visible: false
anchor.rect.x: parentWindow.width / 2 - width / 2
anchor.rect.y: parentWindow.height;
HoverHandler {
id: hoverHandler
enabled: true
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onHoveredChanged: {
if (hovered == false) {
root.visible = false
}
}
}
Rectangle {
id: mediaPlayerContainer;
width: 500;
height: mediaPlayerColumn.height + 20;
color: ShellGlobals.colors.window
radius: 5
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true;
spread: 0.02;
samples: 25;
color: "#80000000";
}
anchors.centerIn: parent;
//border.color: hoverHandler.hovered
// ? ShellGlobals.colors.highlight
// : ShellGlobals.colors.light
//border.width: 2
ColumnLayout {
id: mediaPlayerColumn;
spacing: 10
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 10
}
Repeater {
model: Mpris.players
Rectangle {
// TODO: do color quant for a background gradient and then blur it
required property var modelData;
radius: 5;
color: ShellGlobals.colors.light;
height: 80
Layout.fillWidth: true
RowLayout {
spacing: 15
anchors {
fill: parent
margins: 10
}
Item {
Layout.preferredWidth: 60
Layout.preferredHeight: 60
Rectangle {
id: mask
anchors.fill: parent
radius: 5;
visible: false
}
Image {
anchors.fill: parent
source: modelData.trackArtUrl
fillMode: Image.PreserveAspectFit
layer.enabled: true
layer.effect: OpacityMask {
maskSource: mask
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 5
Layout.alignment: Qt.AlignVCenter
Text {
text: modelData.trackArtist;
color: ShellGlobals.colors.text
font.pointSize: 13
font.bold: true
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
width: 350;
elide: Text.ElideRight;
}
Text {
text: modelData.trackTitle;
color: ShellGlobals.colors.text;
font.pointSize: 13;
Layout.alignment: Qt.AlignLeft;
Layout.fillWidth: true;
//width: 350;
elide: Text.ElideRight;
}
}
// Spacer to push controls to the right
Item {
Layout.fillWidth: true
Layout.minimumWidth: 20
}
// Controls container
RowLayout {
spacing: 2
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
IconButton {
implicitSize: 24
source: Qt.resolvedUrl("../../resources/mpris/previous.svg")
onClicked: modelData.previous()
}
IconButton {
implicitSize: 24
source: modelData?.isPlaying
? Qt.resolvedUrl("../../resources/mpris/pause.svg")
: Qt.resolvedUrl("../../resources/mpris/play.svg")
onClicked: {
if (!modelData.canPlay)
return
modelData.isPlaying
? modelData.pause()
: modelData.play()
}
}
IconButton {
implicitSize: 24
source: Qt.resolvedUrl("../../resources/mpris/next.svg")
onClicked: modelData.next()
}
}
}
}
}
}
}
function truncate(text) {
if (text?.length > 30) {
return text.substring(0, 30) + " ..."
}
return text
}
}

View file

@ -0,0 +1,178 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Services.Mpris
import "../.."
PopupWindow {
id: root
width: mediaPlayerContainer.width + 15;
height: mediaPlayerContainer.height + 15;
color: "transparent"
visible: false
anchor.rect.x: parentWindow.width / 2 - width / 2
anchor.rect.y: parentWindow.height;
HoverHandler {
id: hoverHandler
enabled: true
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onHoveredChanged: {
if (hovered == false) {
root.visible = false
}
}
}
Rectangle {
id: mediaPlayerContainer;
width: 500;
height: mediaPlayerColumn.height + 20;
color: ShellGlobals.colors.window
radius: 5
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true;
spread: 0.02;
samples: 25;
color: "#80000000";
}
anchors.centerIn: parent;
//border.color: hoverHandler.hovered
// ? ShellGlobals.colors.highlight
// : ShellGlobals.colors.light
//border.width: 2
ColumnLayout {
id: mediaPlayerColumn;
spacing: 10
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 10
}
Repeater {
model: Mpris.players
Rectangle {
// TODO: do color quant for a background gradient and then blur it
required property var modelData;
radius: 5;
color: ShellGlobals.colors.light;
height: 80
Layout.fillWidth: true
RowLayout {
spacing: 15
anchors {
fill: parent
margins: 10
}
Item {
Layout.preferredWidth: 60
Layout.preferredHeight: 60
Rectangle {
id: mask
anchors.fill: parent
radius: 5;
visible: false
}
Image {
anchors.fill: parent
source: modelData.trackArtUrl
fillMode: Image.PreserveAspectFit
layer.enabled: true
layer.effect: OpacityMask {
maskSource: mask
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 5
Layout.alignment: Qt.AlignVCenter
Text {
text: modelData.trackArtist;
color: ShellGlobals.colors.text
font.pointSize: 13
font.bold: true
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
width: 350;
elide: Text.ElideRight;
}
Text {
text: modelData.trackTitle;
color: ShellGlobals.colors.text;
font.pointSize: 13;
Layout.alignment: Qt.AlignLeft;
Layout.fillWidth: true;
elide: Text.ElideRight;
}
}
// Spacer to push controls to the right
Item {
Layout.fillWidth: true
Layout.minimumWidth: 20
}
// Controls container
RowLayout {
spacing: 2
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
IconButton {
implicitSize: 24
source: Qt.resolvedUrl("../../resources/mpris/previous.svg")
onClicked: modelData.previous()
}
IconButton {
implicitSize: 24
source: modelData?.isPlaying
? Qt.resolvedUrl("../../resources/mpris/pause.svg")
: Qt.resolvedUrl("../../resources/mpris/play.svg")
onClicked: {
if (!modelData.canPlay)
return
modelData.isPlaying
? modelData.pause()
: modelData.play()
}
}
IconButton {
implicitSize: 24
source: Qt.resolvedUrl("../../resources/mpris/next.svg")
onClicked: modelData.next()
}
}
}
}
}
}
}
function truncate(text) {
if (text?.length > 30) {
return text.substring(0, 30) + " ..."
}
return text
}
}

View file

@ -0,0 +1,178 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Services.Mpris
import "../.."
PopupWindow {
id: root
width: mediaPlayerContainer.width + 15;
height: mediaPlayerContainer.height + 15;
color: "transparent"
visible: false
anchor.rect.x: parentWindow.width / 2 - width / 2
anchor.rect.y: parentWindow.height;
HoverHandler {
id: hoverHandler
enabled: true
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onHoveredChanged: {
if (hovered == false) {
root.visible = false
}
}
}
Rectangle {
id: mediaPlayerContainer;
width: 500;
height: mediaPlayerColumn.height + 20;
color: ShellGlobals.colors.window
radius: 5
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true;
spread: 0.02;
samples: 25;
color: "#80000000";
}
anchors.centerIn: parent;
//border.color: hoverHandler.hovered
// ? ShellGlobals.colors.highlight
// : ShellGlobals.colors.light
//border.width: 2
ColumnLayout {
id: mediaPlayerColumn;
spacing: 10
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 10
}
Repeater {
model: Mpris.players
Rectangle {
// TODO: do color quant for a background gradient and then blur it
required property var modelData;
radius: 5;
color: ShellGlobals.colors.light;
height: 80
Layout.fillWidth: true
RowLayout {
spacing: 15
anchors {
fill: parent
margins: 10
}
Item {
Layout.preferredWidth: 60
Layout.preferredHeight: 60
Rectangle {
id: mask
anchors.fill: parent
radius: 5;
visible: false
}
Image {
anchors.fill: parent
source: modelData.trackArtUrl
fillMode: Image.PreserveAspectFit
layer.enabled: true
layer.effect: OpacityMask {
maskSource: mask
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 5
Layout.alignment: Qt.AlignVCenter
Text {
text: modelData.trackArtist;
color: ShellGlobals.colors.text
font.pointSize: 13
font.bold: true
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
//width: 350;
elide: Text.ElideRight;
}
Text {
text: modelData.trackTitle;
color: ShellGlobals.colors.text;
font.pointSize: 13;
Layout.alignment: Qt.AlignLeft;
Layout.fillWidth: true;
elide: Text.ElideRight;
}
}
// Spacer to push controls to the right
//Item {
// Layout.fillWidth: true
// Layout.minimumWidth: 20
//}
// Controls container
RowLayout {
spacing: 2
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
IconButton {
implicitSize: 24
source: Qt.resolvedUrl("../../resources/mpris/previous.svg")
onClicked: modelData.previous()
}
IconButton {
implicitSize: 24
source: modelData?.isPlaying
? Qt.resolvedUrl("../../resources/mpris/pause.svg")
: Qt.resolvedUrl("../../resources/mpris/play.svg")
onClicked: {
if (!modelData.canPlay)
return
modelData.isPlaying
? modelData.pause()
: modelData.play()
}
}
IconButton {
implicitSize: 24
source: Qt.resolvedUrl("../../resources/mpris/next.svg")
onClicked: modelData.next()
}
}
}
}
}
}
}
function truncate(text) {
if (text?.length > 30) {
return text.substring(0, 30) + " ..."
}
return text
}
}

View file

@ -0,0 +1,171 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Services.Mpris
import "../.."
PopupWindow {
id: root
width: mediaPlayerContainer.width + 15;
height: mediaPlayerContainer.height + 15;
color: "transparent"
visible: false
anchor.rect.x: parentWindow.width / 2 - width / 2
anchor.rect.y: parentWindow.height;
HoverHandler {
id: hoverHandler
enabled: true
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onHoveredChanged: {
if (hovered == false) {
root.visible = false
}
}
}
Rectangle {
id: mediaPlayerContainer;
width: 500;
height: mediaPlayerColumn.height + 20;
color: ShellGlobals.colors.window
radius: 5
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true;
spread: 0.02;
samples: 25;
color: "#80000000";
}
anchors.centerIn: parent;
//border.color: hoverHandler.hovered
// ? ShellGlobals.colors.highlight
// : ShellGlobals.colors.light
//border.width: 2
ColumnLayout {
id: mediaPlayerColumn;
spacing: 10
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 10
}
Repeater {
model: Mpris.players
Rectangle {
// TODO: do color quant for a background gradient and then blur it
required property var modelData;
radius: 5;
color: ShellGlobals.colors.light;
height: 80
Layout.fillWidth: true
RowLayout {
spacing: 15
anchors {
fill: parent
margins: 10
}
Item {
Layout.preferredWidth: 60
Layout.preferredHeight: 60
Rectangle {
id: mask
anchors.fill: parent
radius: 5;
visible: false
}
Image {
anchors.fill: parent
source: modelData.trackArtUrl
fillMode: Image.PreserveAspectFit
layer.enabled: true
layer.effect: OpacityMask {
maskSource: mask
}
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 5
Layout.alignment: Qt.AlignVCenter
Text {
text: modelData.trackArtist;
color: ShellGlobals.colors.text
font.pointSize: 13
font.bold: true
Layout.alignment: Qt.AlignLeft
Layout.fillWidth: true
elide: Text.ElideRight;
}
Text {
text: modelData.trackTitle;
color: ShellGlobals.colors.text;
font.pointSize: 13;
Layout.alignment: Qt.AlignLeft;
Layout.fillWidth: true;
elide: Text.ElideRight;
}
}
// Controls container
RowLayout {
spacing: 2
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
IconButton {
implicitSize: 24
source: Qt.resolvedUrl("../../resources/mpris/previous.svg")
onClicked: modelData.previous()
}
IconButton {
implicitSize: 24
source: modelData?.isPlaying
? Qt.resolvedUrl("../../resources/mpris/pause.svg")
: Qt.resolvedUrl("../../resources/mpris/play.svg")
onClicked: {
if (!modelData.canPlay)
return
modelData.isPlaying
? modelData.pause()
: modelData.play()
}
}
IconButton {
implicitSize: 24
source: Qt.resolvedUrl("../../resources/mpris/next.svg")
onClicked: modelData.next()
}
}
}
}
}
}
}
function truncate(text) {
if (text?.length > 30) {
return text.substring(0, 30) + " ..."
}
return text
}
}

View file

@ -0,0 +1,191 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Services.Mpris
import "../../widgets/" as Widgets
import "../.."
PopupWindow {
id: root
width: mediaPlayerContainer.width + 10;
height: mediaPlayerContainer.height + 10;
color: "transparent"
visible: mediaPlayerContainer.opacity > 0;
anchor.rect.x: parentWindow.width / 2 - width / 2;
anchor.rect.y: parentWindow.height;
function show() {
mediaPlayerContainer.opacity = 1;
}
function hide() {
mediaPlayerContainer.opacity = 0;
}
HoverHandler {
id: hoverHandler;
enabled: true;
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad;
onHoveredChanged: {
if (hovered == false) {
hide();
}
}
}
Rectangle {
id: mediaPlayerContainer;
width: 500;
height: mediaPlayerColumn.height + 20;
color: ShellGlobals.colors.window;
radius: 5;
opacity: 0;
layer.enabled: true;
layer.effect: DropShadow {
transparentBorder: true;
spread: 0.02;
samples: 25;
color: "#80000000";
}
anchors.centerIn: parent;
Behavior on opacity {
NumberAnimation {
duration: 300;
easing.type: Easing.OutCubic;
}
}
ColumnLayout {
id: mediaPlayerColumn;
spacing: 10
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 10
}
Repeater {
model: Mpris.players
Rectangle {
required property var modelData;
radius: 5;
color: ShellGlobals.colors.midlight;
border.color: ShellGlobals.colors.light;
height: 75;
Layout.fillWidth: true;
RowLayout {
spacing: 15;
anchors {
fill: parent;
leftMargin: 10;
rightMargin: 10;
topMargin: 0;
bottomMargin: 0;
}
Item {
Layout.preferredWidth: 60;
Layout.preferredHeight: 60;
Layout.alignment: Qt.AlignVCenter;
visible: modelData.trackArtUrl != "";
Rectangle {
id: mask;
anchors.fill: parent;
radius: 5;
visible: false;
}
Image {
anchors.fill: parent;
source: modelData.trackArtUrl;
fillMode: Image.PreserveAspectFit;
layer.enabled: true;
layer.effect: OpacityMask {
maskSource: mask;
}
}
}
ColumnLayout {
Layout.fillWidth: true;
Layout.fillHeight: true;
spacing: 5;
Layout.alignment: Qt.AlignVCenter;
Item { Layout.fillHeight: true; }
Text {
text: modelData.trackArtist;
color: ShellGlobals.colors.text;
font.pointSize: 13;
font.bold: true;
Layout.alignment: Qt.AlignLeft;
Layout.fillWidth: true;
elide: Text.ElideRight;
}
Text {
text: modelData.trackTitle;
color: ShellGlobals.colors.text;
font.pointSize: 13;
Layout.alignment: Qt.AlignLeft;
Layout.fillWidth: true;
elide: Text.ElideRight;
}
Item { Layout.fillHeight: true; }
}
RowLayout {
spacing: 2;
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter;
Widgets.IconButton {
implicitSize: 28;
padding: 4;
source: "root:resources/mpris/previous.svg";
onClicked: modelData.previous();
}
Widgets.IconButton {
implicitSize: 28;
padding: 4;
source: modelData?.isPlaying
? "root:resources/mpris/pause.svg"
: "root:resources/mpris/play.svg";
onClicked: {
if (!modelData.canPlay)
return;
modelData.isPlaying
? modelData.pause()
: modelData.play();
}
}
Widgets.IconButton {
implicitSize: 28;
padding: 4;
source: "root:resources/mpris/next.svg";
onClicked: modelData.next();
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,45 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Mpris
Singleton {
property MprisPlayer trackedPlayer;
id: root;
Instantiator {
model: Mpris.players;
Connections {
required property MprisPlayer modelData;
target: modelData;
Component.onCompleted: {
if (root.trackedPlayer == null || modelData.isPlaying) {
root.trackedPlayer = modelData;
}
}
Component.onDestruction: {
if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) {
for (const player of Mpris.players.values) {
if (player.playbackState.isPlaying) {
root.trackedPlayer = player;
break;
}
}
if (trackedPlayer == null && Mpris.players.values.length != 0) {
trackedPlayer = Mpris.players.values[0];
}
}
}
function onPlaybackStateChanged() {
if (root.trackedPlayer !== modelData) root.trackedPlayer = modelData;
}
}
}
}

View file

@ -0,0 +1,151 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Services.Mpris
import "../.."
PopupWindow {
id: root
width: mediaPlayerContainer.width + 10
height: mediaPlayerContainer.height + 10
color: "transparent"
visible: mediaPlayerContainer.opacity > 0
anchor.rect.x: parentWindow.width / 2 - width / 2
anchor.rect.y: parentWindow.height
function show() {
mediaPlayerContainer.opacity = 1;
}
function hide() {
mediaPlayerContainer.opacity = 0;
}
HoverHandler {
id: hoverHandler
enabled: true
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onHoveredChanged: {
if (hovered == false) {
hide();
}
}
}
Rectangle {
id: mediaPlayerContainer
width: 500
height: mediaPlayerColumn.height + 20
color: ShellGlobals.colors.base
radius: 5
opacity: 0
anchors.centerIn: parent
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: mediaPlayerContainer.width
height: mediaPlayerContainer.height
radius: mediaPlayerContainer.radius
color: "white"
}
maskSource: Rectangle {
width: mediaPlayerContainer.width
height: mediaPlayerContainer.height
radius: mediaPlayerContainer.radius
color: "black"
}
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
spread: 0.02
samples: 25
color: "#80000000"
}
}
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
ColorQuantizer {
id: colorQuantizer
source: Qt.resolvedUrl(Media.trackedPlayer?.trackArtUrl ?? "")
depth: 2
rescaleSize: 64
onColorsChanged: {
Media.colors = colors;
}
}
ShaderEffect {
property color topLeftColor: colorQuantizer?.colors[0] ?? "white"
property color topRightColor: colorQuantizer?.colors[1] ?? "black"
property color bottomLeftColor: colorQuantizer?.colors[2] ?? "white"
property color bottomRightColor: colorQuantizer?.colors[3] ?? "black"
anchors.fill: parent
fragmentShader: "root:/shaders/vertexgradient.frag.qsb"
vertexShader: "root:/shaders/vertexgradient.vert.qsb"
Behavior on topLeftColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
Behavior on topRightColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
Behavior on bottomLeftColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
Behavior on bottomRightColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
}
ColumnLayout {
id: mediaPlayerColumn
spacing: 10
Layout.fillWidth: true
Layout.preferredWidth: parent.width
Layout.margins: 10
implicitHeight: childrenRect.height
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 10
}
// Media Cards
Repeater {
model: Mpris.players
Card {
required property var modelData
player: modelData
Layout.fillWidth: true
}
}
}
}
}

View file

@ -0,0 +1,174 @@
import QtQuick
import Quickshell.Widgets
import Quickshell.Services.Mpris
import Qt5Compat.GraphicalEffects
import "../.."
Item {
id: root
required property var bar
width: statusInfo.width + 125
height: parent.height
visible: Mpris.players.values.length != 0
Player {
id: mediaPlayer
anchor.window: bar
anchor.rect.x: parentWindow.width / 2 - width / 2
anchor.rect.y: parentWindow.height
}
MouseArea {
id: playButton
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (mediaPlayer.visible) {
mediaPlayer.hide();
} else {
mediaPlayer.show();
}
} else {
if (!Media.trackedPlayer.canPlay || Media.trackedPlayer == null)
return;
if (Media.trackedPlayer.isPlaying)
Media.trackedPlayer.pause();
else
Media.trackedPlayer.play();
}
}
anchors.fill: parent
}
ShaderEffect {
id: gradientShader
property color topLeftColor: Media?.colors[0] ?? "white"
property color topRightColor: Media?.colors[1] ?? "black"
property color bottomLeftColor: Media?.colors[2] ?? "white"
property color bottomRightColor: Media?.colors[3] ?? "black"
anchors.fill: parent
visible: false
fragmentShader: "root:/shaders/vertexgradient.frag.qsb"
vertexShader: "root:/shaders/vertexgradient.vert.qsb"
Behavior on topLeftColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
Behavior on topRightColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
Behavior on bottomLeftColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
Behavior on bottomRightColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
}
Rectangle {
id: artRect
anchors.fill: gradientShader
antialiasing: true
visible: false
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 0.5
color: "white"
}
GradientStop {
position: 1.0
color: "transparent"
}
}
}
OpacityMask {
id: clip
source: gradientShader
anchors.fill: gradientShader
maskSource: artRect
cached: false
visible: false
}
GaussianBlur {
id: blur
visible: root.visible
source: clip
anchors.fill: clip
radius: 16
samples: radius * 2
transparentBorder: true
}
Item {
id: statusInfo
width: statusIcon.width + statusIcon.anchors.rightMargin + nowPlayingText.width
height: parent.height
visible: Media.trackedPlayer != null
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
top: parent.top
bottom: parent.botton
margins: 3.5
}
//Rectangle {
// color: ShellGlobals.colors.accent
// radius: 3
// width: parent.width + 25
// height: parent.height - 7
// visible: playButton.containsMouse
// anchors.centerIn: parent
//}
IconImage {
id: statusIcon
implicitSize: 13
source: Media.trackedPlayer?.isPlaying ? "root:resources/mpris/pause.svg" : "root:resources/mpris/play.svg"
anchors {
verticalCenter: parent.verticalCenter
right: nowPlayingText.left
rightMargin: 10
}
}
Text {
id: nowPlayingText
color: ShellGlobals.colors.text
text: `${Media.trackedPlayer?.trackArtist} - ${Media.trackedPlayer?.trackTitle}`
font.pointSize: 11
width: Math.min(implicitWidth, 250)
elide: Text.ElideRight
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
}
}
}
}

View file

@ -0,0 +1,61 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Notifications
import ".."
PanelWindow {
required property var bar;
id: notificationWindow;
color: "transparent";
width: 550;
height: 600;
visible: true;
mask: Region { item: notifLayout; }
anchors {
top: true;
bottom: true;
}
margins {
top: 5;
bottom: 5;
right: 5;
}
NotificationServer {
id: notificationServer;
actionsSupported: true;
persistenceSupported: true;
}
Connections {
target: notificationServer;
function onNotification(notification) {
notification.tracked = true;
}
}
ColumnLayout {
id: notifLayout;
spacing: 5;
anchors {
left: parent.left;
right: parent.right;
}
Repeater {
model: notificationServer.trackedNotifications;
Toast {
required property var modelData;
notification: modelData;
}
}
}
}

View file

@ -0,0 +1,71 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Notifications
import ".."
Scope {
required property var bar;
NotificationServer {
id: notificationServer;
actionsSupported: true;
persistenceSupported: true;
}
Connections {
target: notificationServer;
function onNotification(notification) {
notificationLoader.item.visible = true;
notification.tracked = true;
}
}
LazyLoader {
id: notificationLoader;
loading: true;
PanelWindow {
id: notificationWindow;
color: "transparent";
width: 500;
visible: false;
exclusionMode: ExclusionMode.Normal;
mask: Region { item: notifLayout; }
anchors {
top: true;
bottom: true;
right: true;
}
margins {
top: 5;
bottom: 5;
right: 5;
}
ColumnLayout {
id: notifLayout;
spacing: 15;
anchors {
top: parent.top;
left: parent.left;
right: parent.right;
margins: 5;
}
Repeater {
model: notificationServer.trackedNotifications;
Toast {
required property var modelData;
notification: modelData;
}
}
}
}
}
}

View file

@ -0,0 +1,105 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import "../.."
Rectangle {
required property var notification
radius: 5;
color: ShellGlobals.colors.bar;
border.color: notificationArea.containsMouse
? ShellGlobals.colors.highlight
: ShellGlobals.colors.light;
border.width: 2;
width: parent.width;
height: column.implicitHeight + 20;
MouseArea {
id: notificationArea;
hoverEnabled: true;
anchors.fill: parent;
}
ColumnLayout {
id: column;
spacing: 5;
anchors {
fill: parent;
margins: 10;
}
RowLayout {
spacing: 5;
Layout.fillWidth: true;
IconImage {
visible: notification.appIcon == null;
source: Qt.resolvedUrl(notification.appIcon);
implicitSize: 25;
}
Text {
id: summaryText
text: notification.summary
color: ShellGlobals.colors.text
font.pointSize: 14
font.bold: true
wrapMode: Text.Wrap;
Layout.fillWidth: true
Layout.alignment: Qt.AlignBottom;
}
Item {
width: 16;
height: 16;
Layout.alignment: Qt.AlighRight | Qt.AlignTop;
Rectangle {
color: "#FF474D";
radius: 5;
visible: closeButtonArea.containsMouse;
anchors.fill: parent;
}
MouseArea {
id: closeButtonArea;
hoverEnabled: true;
anchors.fill: parent;
onPressed: {
notification.dismiss();
}
}
IconImage {
source: "image://icon/window-close";
implicitSize: 28;
anchors.centerIn: parent;
}
}
}
RowLayout {
Text {
id: bodyText
text: notification.body
color: ShellGlobals.colors.text
font.pointSize: 11;
wrapMode: Text.Wrap
Layout.fillWidth: true
}
//IconImage {
// visible: notification.image != null;
// source: Qt.resolvedUrl(notification.image);
// implicitSize: 25;
//}
Layout.fillWidth: true;
}
}
}

View file

@ -0,0 +1,105 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import "../.."
Rectangle {
required property var notification
radius: 5;
color: ShellGlobals.colors.bar;
border.color: notificationArea.containsMouse
? ShellGlobals.colors.highlight
: ShellGlobals.colors.light;
border.width: 2;
width: parent.width;
height: column.implicitHeight + 20;
MouseArea {
id: notificationArea;
hoverEnabled: true;
anchors.fill: parent;
}
ColumnLayout {
id: column;
spacing: 5;
anchors {
fill: parent;
margins: 10;
}
RowLayout {
spacing: 5;
Layout.fillWidth: true;
//IconImage {
// visible: notification.appIcon == null;
// source: Qt.resolvedUrl(notification.appIcon);
// implicitSize: 25;
//}
Text {
id: summaryText
text: notification.summary
color: ShellGlobals.colors.text
font.pointSize: 14
font.bold: true
wrapMode: Text.Wrap;
Layout.fillWidth: true
Layout.alignment: Qt.AlignBottom;
}
Item {
width: 16;
height: 16;
Layout.alignment: Qt.AlighRight | Qt.AlignTop;
Rectangle {
color: "#FF474D";
radius: 5;
visible: closeButtonArea.containsMouse;
anchors.fill: parent;
}
MouseArea {
id: closeButtonArea;
hoverEnabled: true;
anchors.fill: parent;
onPressed: {
notification.dismiss();
}
}
IconImage {
source: "image://icon/window-close";
implicitSize: 28;
anchors.centerIn: parent;
}
}
}
RowLayout {
Text {
id: bodyText
text: notification.body
color: ShellGlobals.colors.text
font.pointSize: 11;
wrapMode: Text.Wrap
Layout.fillWidth: true
}
//IconImage {
// visible: notification.image != null;
// source: Qt.resolvedUrl(notification.image);
// implicitSize: 25;
//}
Layout.fillWidth: true;
}
}
}

View file

@ -0,0 +1,105 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import "../.."
Rectangle {
required property var notification
radius: 5;
color: ShellGlobals.colors.bar;
border.color: notificationArea.containsMouse
? ShellGlobals.colors.highlight
: ShellGlobals.colors.light;
border.width: 2;
width: parent.width;
height: column.implicitHeight + 20;
MouseArea {
id: notificationArea;
hoverEnabled: true;
anchors.fill: parent;
}
ColumnLayout {
id: column;
spacing: 5;
anchors {
fill: parent;
margins: 10;
}
RowLayout {
spacing: 5;
Layout.fillWidth: true;
//IconImage {
// visible: notification.appIcon == null;
// source: Qt.resolvedUrl(notification.appIcon);
// implicitSize: 25;
//}
Text {
id: summaryText
text: notification.summary
color: ShellGlobals.colors.text
font.pointSize: 14
font.bold: true
wrapMode: Text.Wrap;
Layout.fillWidth: true
Layout.alignment: Qt.AlignBottom;
}
Item {
width: 16;
height: 16;
Layout.alignment: Qt.AlighRight | Qt.AlignTop;
Rectangle {
color: "#FF474D";
radius: 5;
visible: closeButtonArea.containsMouse;
anchors.fill: parent;
}
MouseArea {
id: closeButtonArea;
hoverEnabled: true;
anchors.fill: parent;
onPressed: {
notification.dismiss();
}
}
IconImage {
source: "image://icon/window-close";
implicitSize: 28;
anchors.centerIn: parent;
}
}
}
RowLayout {
Text {
id: bodyText
text: notification.body
color: ShellGlobals.colors.text
font.pointSize: 11;
wrapMode: Text.Wrap
Layout.fillWidth: true
}
IconImage {
visible: notification.image != null;
source: Qt.resolvedUrl(notification.image);
implicitSize: 25;
}
Layout.fillWidth: true;
}
}
}

View file

@ -0,0 +1,105 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import "../.."
Rectangle {
required property var notification
radius: 5;
color: ShellGlobals.colors.bar;
border.color: notificationArea.containsMouse
? ShellGlobals.colors.highlight
: ShellGlobals.colors.light;
border.width: 2;
width: parent.width;
height: column.implicitHeight + 20;
MouseArea {
id: notificationArea;
hoverEnabled: true;
anchors.fill: parent;
}
ColumnLayout {
id: column;
spacing: 5;
anchors {
fill: parent;
margins: 10;
}
RowLayout {
spacing: 5;
Layout.fillWidth: true;
IconImage {
visible: notification.appIcon == null;
source: Qt.resolvedUrl(notification.appIcon);
implicitSize: 25;
}
Text {
id: summaryText
text: notification.summary
color: ShellGlobals.colors.text
font.pointSize: 14
font.bold: true
wrapMode: Text.Wrap;
Layout.fillWidth: true
Layout.alignment: Qt.AlignBottom;
}
Item {
width: 16;
height: 16;
Layout.alignment: Qt.AlighRight | Qt.AlignTop;
Rectangle {
color: "#FF474D";
radius: 5;
visible: closeButtonArea.containsMouse;
anchors.fill: parent;
}
MouseArea {
id: closeButtonArea;
hoverEnabled: true;
anchors.fill: parent;
onPressed: {
notification.dismiss();
}
}
IconImage {
source: "image://icon/window-close";
implicitSize: 28;
anchors.centerIn: parent;
}
}
}
RowLayout {
Text {
id: bodyText
text: notification.body
color: ShellGlobals.colors.text
font.pointSize: 11;
wrapMode: Text.Wrap
Layout.fillWidth: true
}
IconImage {
visible: notification.image != null;
source: Qt.resolvedUrl(notification.image);
implicitSize: 25;
}
Layout.fillWidth: true;
}
}
}

View file

@ -0,0 +1,215 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import ".."
import "../.."
Rectangle {
required property var notification;
id: notificationRoot;
radius: 5;
color: ShellGlobals.colors.window;
width: parent.width;
height: column.implicitHeight + 30;
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true;
spread: 0.01;
samples: 25;
color: "#80000000";
}
Item {
property int totalDuration: 5000;
property int remainingTime: totalDuration;
property bool isRunning: false;
property real lastTime: 0;
id: timerController;
Timer {
id: internalTimer;
interval: 16;
repeat: true;
running: timerController.isRunning;
onTriggered: {
var currentTime = Date.now()
if (timerController.lastTime > 0) {
var delta = currentTime - timerController.lastTime;
timerController.remainingTime -= delta;
if (timerController.remainingTime <= 0) {
timerController.isRunning = false;
notification.expire();
}
}
timerController.lastTime = currentTime;
}
}
function start() {
if (!isRunning) {
lastTime = Date.now();
isRunning = true;
}
}
function pause() {
isRunning = false;
lastTime = 0;
}
Component.onCompleted: {
start();
}
}
MouseArea {
id: notificationArea;
hoverEnabled: true;
anchors.fill: parent;
onContainsMouseChanged: {
progressAnimation.paused = containsMouse;
if (containsMouse) {
timerController.pause();
} else {
timerController.start();
}
}
}
RowLayout {
id: column;
spacing: 5;
anchors {
fill: parent;
margins: 15;
}
ColumnLayout {
Layout.fillWidth: true;
RowLayout {
id: topRow;
spacing: 10;
IconImage {
visible: notification.appIcon != "";
source: Quickshell.iconPath(notification.appIcon);
implicitSize: 24;
}
RowLayout {
Text {
id: appName;
text: notification.appName;
color: ShellGlobals.colors.text;
font.pointSize: 11;
font.bold: true;
wrapMode: Text.Wrap;
Layout.fillWidth: false;
}
Separator {}
Text {
id: summaryText;
text: notification.summary;
color: ShellGlobals.colors.text;
font.pointSize: 11;
wrapMode: Text.Wrap;
Layout.fillWidth: true;
}
}
Item {
id: closeButton;
width: 24;
height: 24;
Layout.alignment: Qt.AlignTop;
Canvas {
id: progressCircle;
anchors.fill: parent;
antialiasing: true;
property real progress: 1.0;
onProgressChanged: requestPaint();
onPaint: {
var ctx = getContext("2d");
ctx.reset();
var centerX = width / 2;
var centerY = height / 2;
var radius = Math.min(width, height) / 2 - 2;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + 2 * Math.PI * progress);
ctx.strokeStyle = ShellGlobals.colors.highlight;
ctx.lineWidth = 2;
ctx.stroke();
}
}
NumberAnimation {
id: progressAnimation;
target: progressCircle;
property: "progress";
from: 1.0;
to: 0.0;
duration: 5000;
running: true;
easing.type: Easing.Linear;
}
Rectangle {
id: closeButtonBg;
anchors.centerIn: parent;
width: 16;
height: 16;
color: "#FF474D";
radius: 10;
visible: closeButtonArea.containsMouse;
}
MouseArea {
id: closeButtonArea;
hoverEnabled: true;
anchors.fill: parent;
onPressed: {
notification.dismiss();
}
}
IconImage {
source: "image://icon/window-close";
implicitSize: 16;
anchors.centerIn: parent;
}
}
}
RowLayout {
ColumnLayout {
Text {
id: bodyText;
text: notification.body;
color: ShellGlobals.colors.text;
font.pointSize: 11;
wrapMode: Text.Wrap;
Layout.fillWidth: true;
}
}
}
}
}
}

View file

@ -0,0 +1,35 @@
import QtQuick
import Quickshell
import "../../widgets/" as Widgets
import "../.."
Widgets.IconButton {
required property var bar;
id: iconButton
implicitSize: 20
source: "root:/resources/volume/volume-full.svg"
padding: 2
onClicked:{
if (volumeControl.visible) {
volumeControl.hide()
}
else {
volumeControl.show()
}
}
ControlPanel {
id: volumeControl
anchor {
window: bar
onAnchoring: {
anchor.rect = mapToItem(bar.contentItem, 0, bar.height, width , 0);
}
}
}
}

View file

@ -0,0 +1,76 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Pipewire
import "../.."
import "../../widgets/" as Widgets
Rectangle {
id: root
required property PwNode node
color: ShellGlobals.colors.light
radius: 5
PwObjectTracker {
id: defaultSourceTracker
objects: [root.node]
}
RowLayout {
anchors.fill: parent
spacing: 8
Widgets.IconButton {
source: {
if (!node.properties["application.icon-name"]) {
return root.node.audio.muted ? "root:resources/volume/volume-mute.svg" : "root:resources/volume/volume-full.svg";
} else {
return `image://icon/${node.properties["application.icon-name"]}`;
}
}
implicitSize: 32
padding: 4
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: 5
onClicked: {
root.node.audio.muted = !root.node.audio.muted;
}
}
ColumnLayout {
spacing: 4
Layout.fillWidth: true
Layout.fillHeight: true
Text {
color: ShellGlobals.colors.text
text: {
// Taken from quickshell-examples
const app = node.properties["application.name"] ?? (node.description != "" ? node.description : node.name);
const media = node.properties["media.name"];
return media != undefined ? `${app} - ${media}` : app;
}
font.bold: true
elide: Text.ElideRight
Layout.fillWidth: true
Layout.topMargin: 5
Layout.rightMargin: 5
Layout.bottomMargin: 5
}
Widgets.RoundSlider {
implicitHeight: 7
from: 0
to: 1
value: root.node.audio.volume
onValueChanged: node.audio.volume = value
Layout.fillWidth: true
Layout.rightMargin: 10
Layout.bottomMargin: 5
}
}
}
}

View file

@ -0,0 +1,117 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Services.Pipewire
import "../.."
PopupWindow {
id: root
width: mainContainer.width + 10
height: mainContainer.height + 10
color: "transparent"
visible: mainContainer.opacity > 0
function show() {
mainContainer.opacity = 1;
}
function hide() {
mainContainer.opacity = 0;
}
HoverHandler {
id: hoverHandler
enabled: true
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onHoveredChanged: {
if (hovered == false) {
hide();
}
}
}
Rectangle {
id: mainContainer
width: 400
height: 400
color: ShellGlobals.colors.base
radius: 5
opacity: 0
anchors.centerIn: parent
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
spread: 0.02
samples: 25
color: "#80000000"
}
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
ColumnLayout {
id: mainColumn
spacing: 10
Layout.fillWidth: true
Layout.preferredWidth: parent.width
Layout.margins: 10
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 10
}
PwNodeLinkTracker {
id: linkTracker
node: Pipewire.defaultAudioSink
}
Card {
node: Pipewire.defaultAudioSink
Layout.fillWidth: true
Layout.preferredHeight: 50
}
Rectangle {
Layout.fillWidth: true
color: ShellGlobals.colors.light
implicitHeight: 2
radius: 1
}
ScrollView {
Layout.fillWidth: true
Layout.fillHeight: true
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentWidth: availableWidth
ColumnLayout {
width: parent.width
spacing: 10
Repeater {
model: linkTracker.linkGroups
Card {
required property PwLinkGroup modelData
node: modelData.source
Layout.fillWidth: true
Layout.preferredHeight: 45
}
}
}
}
}
}
}

View file

@ -0,0 +1,285 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import Quickshell.Services.SystemTray
import ".."
Singleton {
PersistentProperties {
id: persist
property bool launcherOpen: false;
}
IpcHandler {
target: "launcher"
function open(): void {
persist.launcherOpen = true;
}
function close(): void {
persist.launcherOpen = false;
}
function toggle(): void {
persist.launcherOpen = !persist.launcherOpen
}
}
LazyLoader {
id: loader
activeAsync: persist.launcherOpen
PanelWindow {
width: 500
height: 7 + searchContainer.implicitHeight + list.topMargin * 2 + list.delegateHeight * 10
color: "transparent"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
WlrLayershell.namespace: "shell:launcher"
Rectangle {
Behavior on height { NumberAnimation { duration: 200; easing.type: Easing.OutCubic } }
color: ShellGlobals.colors.window
radius: 5
layer.enabled: true;
layer.effect: DropShadow {
transparentBorder: true;
spread: 0.02;
samples: 25;
color: "#80000000";
}
anchors {
fill: parent
margins: 10;
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 7
anchors.bottomMargin: 0
spacing: 0
Rectangle {
id: searchContainer
Layout.fillWidth: true
implicitHeight: searchbox.implicitHeight + 10
radius: 3
color: ShellGlobals.colors.midlight;
border.color: ShellGlobals.colors.light;
RowLayout {
id: searchbox
anchors.fill: parent
anchors.margins: 5
TextInput {
id: search;
Layout.fillWidth: true;
color: ShellGlobals.colors.text;
focus: true
Keys.forwardTo: [list]
Keys.onEscapePressed: persist.launcherOpen = false
Keys.onPressed: event => {
if (event.modifiers & Qt.ControlModifier) {
if (event.key == Qt.Key_J) {
list.currentIndex = list.currentIndex == list.count - 1 ? 0 : list.currentIndex + 1;
event.accepted = true;
} else if (event.key == Qt.Key_K) {
list.currentIndex = list.currentIndex == 0 ? list.count - 1 : list.currentIndex - 1;
event.accepted = true;
}
}
}
onAccepted: {
if (list.currentItem) {
list.currentItem.clicked(null);
}
}
onTextChanged: {
list.currentIndex = 0;
}
}
}
}
ListView {
id: list
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
cacheBuffer: 0 // works around QTBUG-131106
//reuseItems: true
model: ScriptModel {
values: DesktopEntries.applications.values
.map(object => {
const stxt = search.text.toLowerCase();
const ntxt = object.name.toLowerCase();
let si = 0;
let ni = 0;
let matches = [];
let startMatch = -1;
for (let si = 0; si != stxt.length; ++si) {
const sc = stxt[si];
while (true) {
// Drop any entries with letters that don't exist in order
if (ni == ntxt.length) return null;
const nc = ntxt[ni++];
if (nc == sc) {
if (startMatch == -1) startMatch = ni;
break;
} else {
if (startMatch != -1) {
matches.push({
index: startMatch,
length: ni - startMatch,
});
startMatch = -1;
}
}
}
}
if (startMatch != -1) {
matches.push({
index: startMatch,
length: ni - startMatch + 1,
});
}
return {
object: object,
matches: matches,
};
})
.filter(entry => entry !== null)
.sort((a, b) => {
let ai = 0;
let bi = 0;
let s = 0;
while (ai != a.matches.length && bi != b.matches.length) {
const am = a.matches[ai];
const bm = b.matches[bi];
s = bm.length - am.length;
if (s != 0) return s;
s = am.index - bm.index;
if (s != 0) return s;
++ai;
++bi;
}
s = a.matches.length - b.matches.length;
if (s != 0) return s;
s = a.object.name.length - b.object.name.length;
if (s != 0) return s;
return a.object.name.localeCompare(b.object.name);
})
.map(entry => entry.object);
onValuesChanged: list.currentIndex = 0
}
topMargin: 7
bottomMargin: list.count == 0 ? 0 : 7
add: Transition {
NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 100 }
}
displaced: Transition {
NumberAnimation { property: "y"; duration: 200; easing.type: Easing.OutCubic }
NumberAnimation { property: "opacity"; to: 1; duration: 100 }
}
move: Transition {
NumberAnimation { property: "y"; duration: 200; easing.type: Easing.OutCubic }
NumberAnimation { property: "opacity"; to: 1; duration: 100 }
}
remove: Transition {
NumberAnimation { property: "y"; duration: 200; easing.type: Easing.OutCubic }
NumberAnimation { property: "opacity"; to: 0; duration: 100 }
}
highlight: Rectangle {
radius: 5
color: ShellGlobals.colors.innerHighlight;
border.color: ShellGlobals.colors.highlight;
}
keyNavigationEnabled: true
keyNavigationWraps: true
highlightMoveVelocity: -1
highlightMoveDuration: 100
preferredHighlightBegin: list.topMargin
preferredHighlightEnd: list.height - list.bottomMargin
highlightRangeMode: ListView.ApplyRange
snapMode: ListView.SnapToItem
readonly property real delegateHeight: 44
delegate: MouseArea {
required property DesktopEntry modelData;
implicitHeight: list.delegateHeight
implicitWidth: ListView.view.width
onClicked: {
modelData.execute();
persist.launcherOpen = false;
}
RowLayout {
id: delegateLayout
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: 5
}
IconImage {
Layout.alignment: Qt.AlignVCenter
asynchronous: true
implicitSize: 30
source: Quickshell.iconPath(modelData.icon)
}
Text {
text: modelData.name
color: ShellGlobals.colors.text;
Layout.alignment: Qt.AlignVCenter
}
}
}
}
}
}
}
}
function init() {}
}

View file

@ -0,0 +1,321 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import Quickshell.Services.SystemTray
import ".."
Singleton {
PersistentProperties {
id: persist
property bool launcherOpen: false
}
IpcHandler {
target: "launcher"
function open(): void {
persist.launcherOpen = true;
}
function close(): void {
persist.launcherOpen = false;
}
function toggle(): void {
persist.launcherOpen = !persist.launcherOpen;
}
}
LazyLoader {
id: loader
activeAsync: persist.launcherOpen
PanelWindow {
width: 500
height: 7 + searchContainer.implicitHeight + list.topMargin * 2 + list.delegateHeight * 10
color: "transparent"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
WlrLayershell.namespace: "shell:launcher"
Rectangle {
Behavior on height {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
color: ShellGlobals.colors.base
radius: 5
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
spread: 0.02
samples: 25
color: "#80000000"
}
anchors {
fill: parent
margins: 10
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 7
anchors.bottomMargin: 0
spacing: 0
Rectangle {
id: searchContainer
Layout.fillWidth: true
implicitHeight: searchbox.implicitHeight + 10
radius: 3
color: ShellGlobals.colors.midlight
border.color: ShellGlobals.colors.light
RowLayout {
id: searchbox
anchors.fill: parent
anchors.margins: 5
TextInput {
id: search
Layout.fillWidth: true
color: ShellGlobals.colors.text
focus: true
Keys.forwardTo: [list]
Keys.onEscapePressed: persist.launcherOpen = false
Keys.onPressed: event => {
if (event.modifiers & Qt.ControlModifier) {
if (event.key == Qt.Key_J) {
list.currentIndex = list.currentIndex == list.count - 1 ? 0 : list.currentIndex + 1;
event.accepted = true;
} else if (event.key == Qt.Key_K) {
list.currentIndex = list.currentIndex == 0 ? list.count - 1 : list.currentIndex - 1;
event.accepted = true;
}
}
}
onAccepted: {
if (list.currentItem) {
list.currentItem.clicked(null);
}
}
onTextChanged: {
list.currentIndex = 0;
}
}
}
}
ListView {
id: list
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
cacheBuffer: 0 // works around QTBUG-131106
//reuseItems: true
model: ScriptModel {
values: DesktopEntries.applications.values.map(object => {
const stxt = search.text.toLowerCase();
const ntxt = object.name.toLowerCase();
let si = 0;
let ni = 0;
let matches = [];
let startMatch = -1;
for (let si = 0; si != stxt.length; ++si) {
const sc = stxt[si];
while (true) {
// Drop any entries with letters that don't exist in order
if (ni == ntxt.length)
return null;
const nc = ntxt[ni++];
if (nc == sc) {
if (startMatch == -1)
startMatch = ni;
break;
} else {
if (startMatch != -1) {
matches.push({
index: startMatch,
length: ni - startMatch
});
startMatch = -1;
}
}
}
}
if (startMatch != -1) {
matches.push({
index: startMatch,
length: ni - startMatch + 1
});
}
return {
object: object,
matches: matches
};
}).filter(entry => entry !== null).sort((a, b) => {
let ai = 0;
let bi = 0;
let s = 0;
while (ai != a.matches.length && bi != b.matches.length) {
const am = a.matches[ai];
const bm = b.matches[bi];
s = bm.length - am.length;
if (s != 0)
return s;
s = am.index - bm.index;
if (s != 0)
return s;
++ai;
++bi;
}
s = a.matches.length - b.matches.length;
if (s != 0)
return s;
s = a.object.name.length - b.object.name.length;
if (s != 0)
return s;
return a.object.name.localeCompare(b.object.name);
}).map(entry => entry.object)
onValuesChanged: list.currentIndex = 0
}
topMargin: 7
bottomMargin: list.count == 0 ? 0 : 7
add: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: 100
}
}
displaced: Transition {
NumberAnimation {
property: "y"
duration: 200
easing.type: Easing.OutCubic
}
NumberAnimation {
property: "opacity"
to: 1
duration: 100
}
}
move: Transition {
NumberAnimation {
property: "y"
duration: 200
easing.type: Easing.OutCubic
}
NumberAnimation {
property: "opacity"
to: 1
duration: 100
}
}
remove: Transition {
NumberAnimation {
property: "y"
duration: 200
easing.type: Easing.OutCubic
}
NumberAnimation {
property: "opacity"
to: 0
duration: 100
}
}
highlight: Rectangle {
radius: 5
color: ShellGlobals.colors.accent
}
keyNavigationEnabled: true
keyNavigationWraps: true
highlightMoveVelocity: -1
highlightMoveDuration: 100
preferredHighlightBegin: list.topMargin
preferredHighlightEnd: list.height - list.bottomMargin
highlightRangeMode: ListView.ApplyRange
snapMode: ListView.SnapToItem
readonly property real delegateHeight: 44
delegate: MouseArea {
required property DesktopEntry modelData
implicitHeight: list.delegateHeight
implicitWidth: ListView.view.width
onClicked: {
modelData.execute();
persist.launcherOpen = false;
}
RowLayout {
id: delegateLayout
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: 5
}
IconImage {
Layout.alignment: Qt.AlignVCenter
asynchronous: true
implicitSize: 30
source: Quickshell.iconPath(modelData.icon)
}
Text {
text: modelData.name
color: ShellGlobals.colors.text
Layout.alignment: Qt.AlignVCenter
}
}
}
}
}
}
}
}
function init() {
}
}

View file

@ -0,0 +1,74 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Notifications
import ".."
Scope {
id: root
required property var screen
NotificationServer {
id: notificationServer
actionsSupported: true
persistenceSupported: true
}
Connections {
target: notificationServer
function onNotification(notification) {
notificationLoader.item.visible = true;
notification.tracked = true;
}
}
LazyLoader {
id: notificationLoader
loading: true
PanelWindow {
id: notificationWindow
color: "transparent"
width: 500
visible: false
exclusionMode: ExclusionMode.Normal
mask: Region {
item: notifLayout
}
anchors {
top: true
bottom: true
right: true
}
margins {
top: 5
bottom: 5
right: 5
}
ColumnLayout {
id: notifLayout
spacing: 15
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 5
}
Repeater {
model: notificationServer.trackedNotifications
Toast {
required property var modelData
notification: modelData
}
}
}
}
}
}

View file

@ -0,0 +1,23 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
Singleton {
property alias notificationServer: notifServer
NotificationServer {
id: notifServer
actionsSupported: true
persistenceSupported: true
}
Connections {
target: notifServer
function onNotification(notification) {
notification.tracked = true;
}
}
}

View file

@ -0,0 +1,213 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import "../widgets/" as Widgets
import ".."
import "../.."
Rectangle {
id: notificationRoot
required property var notification
radius: 5
color: ShellGlobals.colors.base
width: parent.width
height: column.implicitHeight + 30
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
spread: 0.01
samples: 25
color: "#80000000"
}
Item {
id: timerController
property int totalDuration: 5000
property int remainingTime: totalDuration
property bool isRunning: false
property real lastTime: 0
Timer {
id: internalTimer
interval: 16
repeat: true
running: timerController.isRunning
onTriggered: {
var currentTime = Date.now();
if (timerController.lastTime > 0) {
var delta = currentTime - timerController.lastTime;
timerController.remainingTime -= delta;
if (timerController.remainingTime <= 0) {
timerController.isRunning = false;
notification.expire();
}
}
timerController.lastTime = currentTime;
}
}
function start() {
if (!isRunning) {
lastTime = Date.now();
isRunning = true;
}
}
function pause() {
isRunning = false;
lastTime = 0;
}
Component.onCompleted: {
start();
}
}
MouseArea {
id: notificationArea
hoverEnabled: true
anchors.fill: parent
onContainsMouseChanged: {
progressAnimation.paused = containsMouse;
if (containsMouse) {
timerController.pause();
} else {
timerController.start();
}
}
}
RowLayout {
id: column
spacing: 5
anchors {
fill: parent
margins: 15
}
ColumnLayout {
Layout.fillWidth: true
RowLayout {
id: topRow
spacing: 10
IconImage {
visible: notification.appIcon != ""
source: Quickshell.iconPath(notification.appIcon)
implicitSize: 24
}
RowLayout {
Text {
id: appName
text: notification.appName
color: ShellGlobals.colors.text
font.pointSize: 11
font.bold: true
wrapMode: Text.Wrap
Layout.fillWidth: false
}
Widgets.Separator {}
Text {
id: summaryText
text: notification.summary
color: ShellGlobals.colors.text
font.pointSize: 11
wrapMode: Text.Wrap
Layout.fillWidth: true
}
}
Item {
id: closeButton
width: 24
height: 24
Layout.alignment: Qt.AlignTop
Canvas {
id: progressCircle
anchors.fill: parent
antialiasing: true
property real progress: 1.0
onProgressChanged: requestPaint()
onPaint: {
var ctx = getContext("2d");
ctx.reset();
var centerX = width / 2;
var centerY = height / 2;
var radius = Math.min(width, height) / 2 - 2;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + 2 * Math.PI * progress);
ctx.strokeStyle = ShellGlobals.colors.accent;
ctx.lineWidth = 2;
ctx.stroke();
}
}
NumberAnimation {
id: progressAnimation
target: progressCircle
property: "progress"
from: 1.0
to: 0.0
duration: 5000
running: true
easing.type: Easing.Linear
}
Rectangle {
id: closeButtonBg
anchors.centerIn: parent
width: 16
height: 16
color: "#FF474D"
radius: 10
visible: closeButtonArea.containsMouse
}
MouseArea {
id: closeButtonArea
hoverEnabled: true
anchors.fill: parent
onPressed: {
notification.dismiss();
}
}
IconImage {
source: "image://icon/window-close"
implicitSize: 16
anchors.centerIn: parent
}
}
}
RowLayout {
ColumnLayout {
Text {
id: bodyText
text: notification.body
color: ShellGlobals.colors.text
font.pointSize: 11
wrapMode: Text.Wrap
Layout.fillWidth: true
}
}
}
}
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M20,15.67V14h2V10H20V8.33A1.34,1.34,0,0,0,18.67,7H3.34A1.34,1.34,0,0,0,2,8.33v7.33A1.34,1.34,0,0,0,3.33,17H18.67A1.34,1.34,0,0,0,20,15.67Z"/>
<path d="M20,15.67V14h2V10H20V8.33A1.34,1.34,0,0,0,18.67,7H3.34A1.34,1.34,0,0,0,2,8.33v7.33A1.34,1.34,0,0,0,3.33,17H18.67A1.34,1.34,0,0,0,20,15.67Z" fill="grey"/>
<path d="M24,0V24H0V0Z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 580 B

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M20,15.67V14h2V10H20V8.33A1.34,1.34,0,0,0,18.67,7H3.34A1.34,1.34,0,0,0,2,8.33v7.33A1.34,1.34,0,0,0,3.33,17H18.67A1.34,1.34,0,0,0,20,15.67Z"/>
<path d="M20,15.67V14h2V10H20V8.33A1.34,1.34,0,0,0,18.67,7H3.34A1.34,1.34,0,0,0,2,8.33v7.33A1.34,1.34,0,0,0,3.33,17H18.67A1.34,1.34,0,0,0,20,15.67Z" fill="white"/>
<path d="M24,0V24H0V0Z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 581 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M24,0V24H0V0Z" fill="none"/>
<path d="M20,10V8.33A1.34,1.34,0,0,0,18.67,7H3.34A1.34,1.34,0,0,0,2,8.33v7.33A1.34,1.34,0,0,0,3.33,17H18.67A1.34,1.34,0,0,0,20,15.67V14h2V10Zm-8.5,3v2L4,11H9.5V9L17,13Z" fill="lightgrey"/>
</svg>

After

Width:  |  Height:  |  Size: 452 B

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M24,0V24H0V0Z" fill="none"/>
<path d="M20,10V8.33A1.34,1.34,0,0,0,18.67,7H3.34A1.34,1.34,0,0,0,2,8.33v7.33A1.34,1.34,0,0,0,3.33,17H18.67A1.34,1.34,0,0,0,20,15.67V14h2V10Zm-8.5,3v2L4,11H9.5V9L17,13Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 448 B

View file

@ -0,0 +1,24 @@
//@ pragma UseQApplication
import Quickshell
import QtQuick
import "bar" as Bar
import "launcher" as Launcher
ShellRoot {
Component.onCompleted: [Launcher.Controller.init()]
Variants {
model: Quickshell.screens;
Scope {
property var modelData;
Bar.Bar {
screen: modelData;
}
}
}
ReloadPopup {}
}

View file

@ -0,0 +1,51 @@
//@ pragma UseQApplication
import Quickshell
import Quickshell.Io
import QtQuick
import "bar" as Bar
import "notifications" as Notifications
import "launcher" as Launcher
ShellRoot {
Component.onCompleted: [Launcher.Controller.init()]
Variants {
model: {
// Check PriorityScreens for priortized screens, I only want the bar showing on
// screen at a time, because it doesnt make alot of sense to have on multiple
// monitors at a time.
const screens = Quickshell.screens;
console.log("Available Screens: " + screens.map(screen => screen.model));
const priorityScreen = PriorityScreens.screens.reduce((found, model) => {
if (found)
return found;
return screens.find(screen => screen.model === model);
}, null);
return priorityScreen ? [priorityScreen] : [];
}
Scope {
id: scope
property var modelData
Bar.Bar {
screen: scope.modelData
}
Notifications.Notifications {
screen: scope.modelData
}
Process {
id: xPrimaryMoniorSetter
running: true
command: ["xrandr", "--output", scope.modelData.name, "--primary"]
}
}
}
ReloadPopup {}
}

View file

@ -0,0 +1,42 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Widgets
import ".."
Item {
property string source;
property var implicitSize;
property var padding: 0;
property var radius: 5;
signal clicked();
id: root;
implicitWidth: implicitSize;
implicitHeight: implicitSize;
Rectangle {
id: iconBackground;
color: ShellGlobals.colors.innerHighlight;
border.color: ShellGlobals.colors.highlight;
radius: root.radius;
visible: iconButton.containsMouse;
anchors.fill: parent;
}
IconImage {
id: iconImage;
source: root.source;
anchors {
fill: parent;
margins: padding;
}
}
MouseArea {
id: iconButton;
hoverEnabled: true;
anchors.fill: parent;
onPressed: root.clicked();
}
}

View file

@ -0,0 +1,40 @@
import QtQuick
import Quickshell.Widgets
import ".."
Item {
id: root
property string source
property var implicitSize: 24 // Default size if not specified
property var padding: 0
property var radius: 5
signal clicked
implicitWidth: implicitSize
implicitHeight: implicitSize
Rectangle {
id: iconBackground
color: ShellGlobals.colors.accent
radius: root.radius
visible: iconButton.containsMouse
anchors.fill: parent
}
IconImage {
id: iconImage
source: root.source
anchors {
fill: parent
margins: root.padding
}
}
MouseArea {
id: iconButton
hoverEnabled: true
anchors.fill: parent
onPressed: root.clicked()
}
}

View file

@ -0,0 +1,60 @@
import QtQuick
import QtQuick.Controls
import Qt5Compat.GraphicalEffects
import ".."
Slider {
id: slider
background: Rectangle {
id: sliderContainer
width: slider.availableWidth
height: slider.implicitHeight
color: "white"
radius: 4
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: sliderContainer.width
height: sliderContainer.height
radius: sliderContainer.radius
color: "white"
}
maskSource: Rectangle {
width: sliderContainer.width
height: sliderContainer.height
radius: sliderContainer.radius
color: "black"
}
}
Rectangle {
id: handle
width: sliderContainer.width * (slider.value / slider.to)
height: sliderContainer.height
color: ShellGlobals.colors.accent
}
}
handle: Rectangle {
x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
y: slider.topPadding + slider.availableHeight / 2 - height / 2
width: 16
height: 16
radius: width / 2
color: slider.pressed ? ShellGlobals.colors.accent.darker(1.2) : ShellGlobals.colors.accent
layer.enabled: true
layer.effect: DropShadow {
horizontalOffset: 0
verticalOffset: 1
radius: 4.0
samples: 9
color: "#30000000"
}
}
//handle: Item {}
}

View file

@ -0,0 +1,9 @@
import QtQuick
import ".."
Rectangle {
color: ShellGlobals.colors.accent
radius: 5
width: 7.5
height: 7.5
}

0
README.md Normal file
View file

126
ReloadPopup.qml Normal file
View file

@ -0,0 +1,126 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
Scope {
id: root
property bool failed
property string errorString
// Connect to the Quickshell global to listen for the reload signals.
Connections {
target: Quickshell
function onReloadCompleted() {
root.failed = false;
popupLoader.loading = true;
}
function onReloadFailed(error: string) {
// Close any existing popup before making a new one.
popupLoader.active = false;
root.failed = true;
root.errorString = error;
popupLoader.loading = true;
}
}
// Keep the popup in a loader because it isn't needed most of the timeand will take up
// memory that could be used for something else.
LazyLoader {
id: popupLoader
PanelWindow {
id: popup
anchors {
top: true
left: true
}
margins {
top: 25
left: 25
}
implicitWidth: rect.width
implicitHeight: rect.height
// color blending is a bit odd as detailed in the type reference.
color: "black"
Rectangle {
id: rect
color: failed ? "#40802020" : "#40009020"
implicitHeight: layout.implicitHeight + 50
implicitWidth: layout.implicitWidth + 30
// Fills the whole area of the rectangle, making any clicks go to it,
// which dismiss the popup.
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked: popupLoader.active = false
// makes the mouse area track mouse hovering, so the hide animation
// can be paused when hovering.
hoverEnabled: true
}
ColumnLayout {
id: layout
anchors {
top: parent.top
topMargin: 20
horizontalCenter: parent.horizontalCenter
}
Text {
text: root.failed ? "Reload failed." : "Reloaded completed!"
color: "white"
}
Text {
text: root.errorString
color: "white"
// When visible is false, it also takes up no space.
visible: root.errorString != ""
}
}
// A progress bar on the bottom of the screen, showing how long until the
// popup is removed.
Rectangle {
id: bar
color: "#20ffffff"
anchors.bottom: parent.bottom
anchors.left: parent.left
height: 20
PropertyAnimation {
id: anim
target: bar
property: "width"
from: rect.width
to: 0
duration: failed ? 10000 : 800
onFinished: popupLoader.active = false
// Pause the animation when the mouse is hovering over the popup,
// so it stays onscreen while reading. This updates reactively
// when the mouse moves on and off the popup.
paused: mouseArea.containsMouse
}
}
// We could set `running: true` inside the animation, but the width of the
// rectangle might not be calculated yet, due to the layout.
// In the `Component.onCompleted` event handler, all of the component's
// properties and children have been initialized.
Component.onCompleted: anim.start()
}
}
}
}

78
ShellSettings.qml Normal file
View file

@ -0,0 +1,78 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
property alias settings: jsonAdapter
FileView {
// todo change to Quickshell.dataPath("quickshell/settings.json")
path: `${Quickshell.env("XDG_DATA_HOME")}/quickshell/settings.json`
watchChanges: true
// onFileChanged: reload()
onAdapterUpdated: writeAdapter()
blockLoading: true
JsonAdapter {
id: jsonAdapter
property int barHeight: 25
property string wallpaperUrl: Qt.resolvedUrl("root:resources/wallpapers/pixelart0.jpg")
property string colorScheme: "scheme-fruit-salad"
property var colors: {
"background": "#131313",
"error": "#ffb4ab",
"error_container": "#93000a",
"inverse_on_surface": "#303030",
"inverse_primary": "#9c4236",
"inverse_surface": "#e2e2e2",
"on_background": "#e2e2e2",
"on_error": "#690005",
"on_error_container": "#ffdad6",
"on_primary": "#5f150d",
"on_primary_container": "#ffdad4",
"on_primary_fixed": "#410000",
"on_primary_fixed_variant": "#7d2b21",
"on_secondary": "#442925",
"on_secondary_container": "#ffdad4",
"on_secondary_fixed": "#2c1512",
"on_secondary_fixed_variant": "#5d3f3b",
"on_surface": "#e2e2e2",
"on_surface_variant": "#c6c6c6",
"on_tertiary": "#3e2e04",
"on_tertiary_container": "#fbdfa6",
"on_tertiary_fixed": "#251a00",
"on_tertiary_fixed_variant": "#564419",
"outline": "#919191",
"outline_variant": "#474747",
"primary": "#ffb4a8",
"primary_container": "#7d2b21",
"primary_fixed": "#ffdad4",
"primary_fixed_dim": "#ffb4a8",
"scrim": "#000000",
"secondary": "#e7bdb6",
"secondary_container": "#5d3f3b",
"secondary_fixed": "#ffdad4",
"secondary_fixed_dim": "#e7bdb6",
"shadow": "#000000",
"source_color": "#df4332",
"surface": "#131313",
"surface_bright": "#393939",
"surface_container": "#1f1f1f",
"surface_container_high": "#2a2a2a",
"surface_container_highest": "#353535",
"surface_container_low": "#1b1b1b",
"surface_container_lowest": "#0e0e0e",
"surface_dim": "#131313",
"surface_tint": "#ffb4a8",
"surface_variant": "#474747",
"tertiary": "#dec38c",
"tertiary_container": "#564419",
"tertiary_fixed": "#fbdfa6",
"tertiary_fixed_dim": "#dec38c"
}
}
}
}

12
bar/ActiveWindow.qml Normal file
View file

@ -0,0 +1,12 @@
import QtQuick
import Quickshell.Wayland
import ".."
Text {
id: windowText
text: ToplevelManager.activeToplevel?.title ?? ""
color: ShellSettings.settings.colors["inverse_surface"]
font.pointSize: 11
visible: text !== ""
elide: Text.ElideRight
}

94
bar/Bar.qml Normal file
View file

@ -0,0 +1,94 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import "control" as Control
import "systray" as SysTray
import "notifications" as Notifications
import "popups" as Popup
import "../widgets" as Widgets
import ".."
PanelWindow {
id: root
color: ShellSettings.settings.colors["surface"]
implicitHeight: ShellSettings.settings.barHeight
property alias popup: popupWindow
anchors {
top: true
left: true
right: true
}
// Popup window for all popups
Popup.MenuWindow {
id: popupWindow
bar: root
}
// Left
RowLayout {
spacing: 15
anchors {
top: parent.top
left: parent.left
bottom: parent.bottom
leftMargin: 4
}
HyprWorkspaces {
Layout.fillWidth: false
Layout.preferredHeight: parent.height
Layout.margins: 4
}
Widgets.Separator {
visible: activeWindow.visible
}
ActiveWindow {
id: activeWindow
Layout.preferredWidth: 400
}
}
// Right
RowLayout {
spacing: 15
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
rightMargin: 10
}
SysTray.SysTray {
id: sysTray
popup: root.popup
Layout.rightMargin: 300
}
// Notifications.NotificationButton {
// implicitSize: 16
// bar: root
// }
BatteryIndicator {
id: batteryIndicator
}
// Control.Button {
// bar: root
// screen: root
// }
Widgets.Separator {}
Clock {
id: clock
color: ShellSettings.settings.colors["inverse_surface"]
}
}
}

50
bar/BatteryIndicator.qml Normal file
View file

@ -0,0 +1,50 @@
pragma ComponentBehavior: Bound
import QtQuick
import Qt5Compat.GraphicalEffects
import Quickshell.Widgets
import Quickshell.Services.UPower
import ".."
Item {
id: root
implicitWidth: 22
implicitHeight: 22
visible: UPower.displayDevice.isLaptopBattery
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: root.width
height: root.height
color: "white"
}
maskSource: IconImage {
implicitSize: root.width
source: "root:resources/battery/battery.svg"
}
}
Rectangle {
id: batteryBackground
color: Qt.color(ShellSettings.settings.colors["surface"]).lighter(4)
opacity: 0.75
anchors {
fill: parent
margins: 2
}
}
Rectangle {
id: batteryPercentage
width: (parent.width - 4) * UPower.displayDevice.percentage
color: ShellSettings.settings.colors["inverse_surface"]
anchors {
left: batteryBackground.left
top: batteryBackground.top
bottom: batteryBackground.bottom
}
}
}

21
bar/Clock.qml Normal file
View file

@ -0,0 +1,21 @@
import QtQuick
import Quickshell
Text {
property string ap: sysClock.hours >= 12 ? "PM" : "AM"
property string minutes: sysClock.minutes.toString().padStart(2, '0')
property string hours: {
var value = sysClock.hours % 12;
if (value === 0)
return 12;
return value;
}
SystemClock {
id: sysClock
enabled: true
}
text: `${hours}:${minutes} ${ap}`
font.pointSize: 11
}

66
bar/HyprWorkspaces.qml Normal file
View file

@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
import ".."
RowLayout {
property var sortedWorkspaces: {
let values = Hyprland.workspaces.values.slice();
values.sort(function (a, b) {
return a.id - b.id;
});
return values;
}
spacing: 6
visible: Hyprland.monitors.values.length != 0
Repeater {
model: parent.sortedWorkspaces
Rectangle {
required property var modelData
radius: height / 2
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: 12
Layout.preferredWidth: {
if (Hyprland.focusedMonitor.activeWorkspace.id == modelData.id)
return 25;
return 12;
}
color: {
let value = Qt.color(ShellSettings.settings.colors["secondary"]).darker(2);
if (!modelData?.id || !Hyprland.focusedMonitor?.activeWorkspace?.id)
return value;
if (workspaceButton.containsMouse) {
value = ShellSettings.settings.colors["on_primary"];
} else if (Hyprland.focusedMonitor.activeWorkspace.id == modelData.id) {
value = ShellSettings.settings.colors["primary"];
}
return value;
}
Behavior on Layout.preferredWidth {
SmoothedAnimation {
duration: 150
velocity: 200
easing.type: Easing.OutCubic
}
}
MouseArea {
id: workspaceButton
anchors.fill: parent
hoverEnabled: true
onPressed: Hyprland.dispatch(`workspace ${parent.modelData.id}`)
}
}
}
}

33
bar/control/Button.qml Normal file
View file

@ -0,0 +1,33 @@
import Quickshell
import QtQuick
import "../../widgets/" as Widgets
Widgets.IconButton {
id: root
required property var bar
required property var screen
implicitSize: 20
source: "root:/resources/general/nixos.svg"
padding: 2
onClicked: {
if (controlPanel.visible) {
controlPanel.hide();
} else {
controlPanel.show();
}
}
ControlPanel {
id: controlPanel
anchor {
window: root.screen
onAnchoring: {
anchor.rect = mapToItem(root.screen.contentItem, 0, root.screen.height, width, 0);
}
}
}
}

View file

@ -0,0 +1,308 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Services.Mpris
import Qt5Compat.GraphicalEffects
import "volume" as Volume
import "../../widgets/" as Widgets
import "../.."
// Change to PopupWindow
PopupWindow {
id: root
implicitWidth: 400
implicitHeight: container.height + 10
color: "transparent"
visible: container.opacity > 0
anchor.rect.x: 0
anchor.rect.y: parentWindow.implicitHeight
// anchors {
// top: true
// left: true
// }
function show() {
container.opacity = 1;
grab.active = true;
}
function hide() {
container.opacity = 0;
grab.active = false;
}
HyprlandFocusGrab {
id: grab
windows: [root]
onCleared: {
root.hide();
}
}
// Add drop shadow effect
// Rectangle {
// id: shadowSource
// color: ShellSettings.settings.colors["surface"]
// radius: 8
// opacity: container.opacity
// width: container.width
// height: container.height
//
// anchors {
// top: parent.top
// left: parent.left
// margins: 5
// }
//
// layer.enabled: true
// layer.effect: DropShadow {
// horizontalOffset: 0
// verticalOffset: 2
// radius: 8.0
// samples: 17
// color: Qt.rgba(0, 0, 0, 0.5)
// transparentBorder: true
// }
// visible: false // Hide the source rectangle
// }
Item {
id: shadowItem
anchors.fill: container
z: container.z - 1
opacity: container.opacity
Rectangle {
id: shadowRect
anchors.fill: parent
color: "transparent"
layer.enabled: true
layer.effect: DropShadow {
horizontalOffset: 0
verticalOffset: 2
radius: 8.0
samples: 17
color: Qt.rgba(0, 0, 0, 0.5)
source: container
}
}
}
Rectangle {
id: container
color: ShellSettings.settings.colors["surface"]
radius: 18
opacity: 0
width: parent.width - 10
height: contentColumn.implicitHeight + 20
anchors {
top: parent.top
left: parent.left
margins: 5
}
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
ColumnLayout {
id: contentColumn
spacing: 10
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 10
}
// RowLayout {
// Layout.fillWidth: true
// Layout.preferredHeight: 40
//
// Rectangle {
// radius: 20
// color: ShellSettings.settings.colors["surface_container_high"]
// Layout.fillWidth: true
// Layout.fillHeight: true
//
// RowLayout {
// anchors {
// fill: parent
// leftMargin: 6
// }
//
// ProfileImage {
// id: profileImage
// Layout.preferredWidth: 25
// Layout.preferredHeight: 25
// // implicitWidth: 30
// // implicitHeight: 30
// }
//
// Text {
// text: "kossLAN"
// color: ShellSettings.settings.colors["inverse_surface"]
// font.pointSize: 12
// verticalAlignment: Text.AlignVCenter
// Layout.fillWidth: true
// Layout.fillHeight: true
// Layout.margins: 4
// }
// }
// }
//
// Rectangle {
// radius: 20
// color: ShellSettings.settings.colors["surface_container_high"]
// Layout.preferredWidth: powerButtons.implicitWidth + 10
// Layout.fillHeight: true
//
// RowLayout {
// id: powerButtons
// spacing: 10
//
// anchors {
// fill: parent
// leftMargin: 5
// rightMargin: 5
// }
//
// Widgets.IconButton {
// id: sleepButton
// implicitSize: 24
// radius: 20
// source: "root:resources/control/sleep.svg"
// onClicked: sleepProcess.running = true
// }
//
// Process {
// id: sleepProcess
// running: false
// command: ["hyprctl", "dispatch", "dpms", "off"]
// }
//
// Rectangle {
// radius: 20
// color: ShellSettings.settings.colors["surface_bright"]
// Layout.preferredWidth: 2
// Layout.fillHeight: true
// Layout.topMargin: 4
// Layout.bottomMargin: 4
// }
//
// Widgets.IconButton {
// id: powerButton
// implicitSize: 24
// radius: 20
// source: "root:resources/control/shutdown.svg"
// }
// }
// }
// }
RowLayout {
spacing: 15
Layout.fillWidth: true
Rectangle {
color: ShellSettings.settings.colors["surface_container_high"]
radius: 12
Layout.fillWidth: true
Layout.preferredHeight: 30
}
}
RowLayout {
spacing: 15
Layout.fillWidth: true
Layout.alignment: Qt.AlignHCenter
Repeater {
model: [1, 2, 3, 4, 5]
delegate: Rectangle {
color: ShellSettings.settings.colors["surface_container_high"]
radius: width / 2
Layout.preferredWidth: 45
Layout.preferredHeight: 45
}
}
}
ColumnLayout {
spacing: 10
Layout.fillWidth: true
RowLayout {
spacing: 10
Layout.fillWidth: true
Layout.preferredHeight: 55
Rectangle {
color: ShellSettings.settings.colors["primary"]
radius: width / 2
Layout.fillWidth: true
Layout.fillHeight: true
}
Rectangle {
color: ShellSettings.settings.colors["primary"]
radius: width / 2
Layout.fillWidth: true
Layout.fillHeight: true
}
}
RowLayout {
spacing: 10
Layout.fillWidth: true
Layout.preferredHeight: 55
Rectangle {
color: ShellSettings.settings.colors["surface_container_high"]
radius: width / 2
Layout.fillWidth: true
Layout.fillHeight: true
}
Rectangle {
color: ShellSettings.settings.colors["surface_container_high"]
radius: width / 2
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
Volume.Mixer {
id: sinkMixer
isSink: true
Layout.fillWidth: true
}
Volume.Mixer {
id: sourceMixer
isSink: false
Layout.fillWidth: true
}
MediaPlayer {
player: Mpris.players?.values[0]
visible: Mpris.players?.values.length != 0
Layout.fillWidth: true
Layout.preferredHeight: 150
}
}
}
}

248
bar/control/MediaPlayer.qml Normal file
View file

@ -0,0 +1,248 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Services.Mpris
import "../.."
import "../../widgets" as Widgets
Item {
id: root
required property var player
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: root.width
height: root.height
radius: 14
color: "black"
}
}
ColorQuantizer {
id: gradientQuantizer
source: root.player?.trackArtUrl ?? ""
depth: 2
rescaleSize: 64
}
ColorQuantizer {
id: accentQuantizer
source: root.player?.trackArtUrl ?? ""
depth: 0
rescaleSize: 64
}
ShaderEffect {
property color topLeftColor: gradientQuantizer?.colors[0] ?? "white"
property color topRightColor: gradientQuantizer?.colors[1] ?? "black"
property color bottomLeftColor: gradientQuantizer?.colors[2] ?? "white"
property color bottomRightColor: gradientQuantizer?.colors[3] ?? "black"
anchors.fill: parent
fragmentShader: "root:/shaders/vertexgradient.frag.qsb"
vertexShader: "root:/shaders/vertexgradient.vert.qsb"
Behavior on topLeftColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
Behavior on topRightColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
Behavior on bottomLeftColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
Behavior on bottomRightColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
}
RowLayout {
id: cardLayout
spacing: 15
anchors {
fill: parent
margins: 10
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 5
RowLayout {
Rectangle {
id: mprisImage
color: "transparent"
radius: 10
width: 50
height: 50
Layout.alignment: Qt.AlignVCenter
visible: true
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
spread: 0.02
samples: 25
color: "#80000000"
}
Image {
anchors.fill: parent
source: root.player?.trackArtUrl ?? ""
sourceSize.width: 1024
sourceSize.height: 1024
fillMode: Image.PreserveAspectFit
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: mprisImage.width
height: mprisImage.height
radius: 10
color: "white"
}
maskSource: Rectangle {
width: mprisImage.width
height: mprisImage.height
radius: 10
color: "black"
}
}
}
}
ColumnLayout {
Layout.leftMargin: 7.5
Layout.alignment: Qt.AlignBottom
Text {
text: root.player?.trackArtist ?? "NA"
color: "white"
font.pointSize: 13
font.bold: true
horizontalAlignment: Text.AlignLeft
Layout.fillWidth: true
elide: Text.ElideRight
}
Text {
text: root.player?.trackTitle ?? "NA"
color: "white"
font.pointSize: 13
horizontalAlignment: Text.AlignLeft
Layout.fillWidth: true
elide: Text.ElideRight
}
}
}
RowLayout {
spacing: 6
Text {
text: timeStr(root.player?.position)
color: "white"
font {
pointSize: 9
bold: true
}
}
FrameAnimation {
running: root.player?.playbackState == MprisPlaybackState.Playing
onTriggered: root.player?.positionChanged()
}
Widgets.RoundSlider {
id: positionSlider
implicitHeight: 7
from: 0
to: root.player?.length
accentColor: accentQuantizer.colors[0]?.darker(1.2) ?? "purple"
value: root.player?.position ?? 0
Layout.fillWidth: true
onMoved: {
if (root.player == null)
return;
root.player.position = value;
}
}
Text {
text: timeStr(root.player?.length)
color: "white"
font {
pointSize: 9
bold: true
}
}
}
// Music Controls
RowLayout {
spacing: 2
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Widgets.IconButton {
implicitSize: 40
activeRectangle: false
padding: 4
source: "root:resources/mpris/previous.svg"
onClicked: root.player?.previous()
}
Widgets.IconButton {
implicitSize: 40
activeRectangle: false
padding: 4
source: root.player?.isPlaying ? "root:resources/mpris/pause.svg" : "root:resources/mpris/play.svg"
onClicked: {
if (!root.player?.canPlay)
return;
player.isPlaying ? player.pause() : player.play();
}
}
Widgets.IconButton {
implicitSize: 40
activeRectangle: false
padding: 4
source: "root:resources/mpris/next.svg"
onClicked: root.player?.next()
}
}
}
}
function timeStr(time: int): string {
const seconds = time % 60;
const minutes = Math.floor(time / 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
}

View file

@ -0,0 +1,33 @@
pragma ComponentBehavior: Bound
import QtQuick
import Qt5Compat.GraphicalEffects
Rectangle {
id: profileImage
color: "transparent"
Image {
anchors.fill: parent
source: "root:resources/general/pfp.png"
sourceSize.width: 100
sourceSize.height: 100
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: profileImage.width
height: profileImage.height
radius: 10
color: "white"
}
maskSource: Rectangle {
width: profileImage.width
height: profileImage.height
radius: 10
color: "black"
}
}
}
}

View file

@ -0,0 +1,79 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Pipewire
import "../../.."
import "../../../widgets" as Widgets
Rectangle {
id: root
required property PwNode node
required property var isSink
color: ShellSettings.settings.colors["surface_container_high"]
PwObjectTracker {
id: defaultSourceTracker
objects: [root.node]
}
RowLayout {
anchors.fill: parent
spacing: 8
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
Layout.leftMargin: 2
spacing: 10
Text {
color: ShellSettings.settings.colors["inverse_surface"]
text: {
// Taken from quickshell-examples
const app = root.node?.properties["application.name"] ?? (root.node?.description != "" ? root.node?.description : root.node?.name);
const media = root.node?.properties["media.name"];
const title = media != undefined ? `${app} - ${media}` : app;
return title != undefined ? title : "null";
}
font.bold: true
elide: Text.ElideRight
Layout.fillWidth: true
Layout.topMargin: 5
Layout.rightMargin: 5
}
Widgets.RoundSlider {
implicitHeight: 7
from: 0
to: 1
value: root.node?.audio.volume ?? 0
onValueChanged: root.node.audio.volume = value
Layout.fillWidth: true
Layout.bottomMargin: 7.5
}
}
Widgets.IconButton {
source: {
if (!root.isSink)
return root.node?.audio.muted ? "root:resources/volume/microphone-mute.svg" : "root:resources/volume/microphone-full.svg";
return root.node?.audio.muted ? "root:resources/volume/volume-mute.svg" : "root:resources/volume/volume-full.svg";
}
implicitSize: 36
padding: 4
radius: implicitSize / 2
Layout.rightMargin: 10
Layout.alignment: Qt.AlignLeft
onClicked: {
root.node.audio.muted = !root.node.audio.muted;
}
}
}
}

View file

@ -0,0 +1,176 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Widgets
import Quickshell.Services.Pipewire
import "../../../widgets/" as Widgets
import "../../.."
// TODO: refactor this trash
Rectangle {
id: root
required property var isSink
color: "transparent"
radius: 10
property bool expanded: false
property int baseHeight: 60
property int contentHeight: expanded ? (applicationVolumes.count * baseHeight) : 0
implicitHeight: baseHeight + contentHeight
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: root.width
height: root.height
radius: root.baseHeight / 2
color: "black"
}
}
Item {
id: headerSection
width: parent.width
height: root.baseHeight
anchors.top: parent.top
RowLayout {
spacing: 0
anchors.fill: parent
Rectangle {
color: ShellSettings.settings.colors["surface_container_high"]
Widgets.IconButton {
id: arrowButton
implicitSize: 44
activeRectangle: false
source: "root:resources/general/right-arrow.svg"
padding: 4
rotation: root.expanded ? 90 : 0
anchors.centerIn: parent
Behavior on rotation {
NumberAnimation {
duration: 150
easing.type: Easing.OutCubic
}
}
onClicked: {
root.expanded = !root.expanded;
}
}
Layout.preferredWidth: 40
Layout.preferredHeight: root.baseHeight
}
Card {
node: root.isSink ? Pipewire.defaultAudioSink : Pipewire.defaultAudioSource
isSink: root.isSink
Layout.fillWidth: true
Layout.preferredHeight: root.baseHeight
}
}
}
Rectangle {
id: divider
color: ShellSettings.settings.colors["surface_bright"]
height: 2
width: parent.width
anchors.top: headerSection.bottom
opacity: root.expanded ? 1.0 : 0.0
// Behavior on opacity {
// NumberAnimation {
// duration: 150
// easing.type: Easing.OutCubic
// }
// }
}
Item {
id: contentSection
width: parent.width
anchors.top: divider.bottom
height: root.contentHeight
clip: true
// Behavior on height {
// SmoothedAnimation {
// duration: 150
// velocity: 200
// easing.type: Easing.OutCubic
// }
// }
Column {
id: applicationsColumn
width: parent.width
anchors.top: parent.top
opacity: root.expanded ? 1.0 : 0.0
// Behavior on opacity {
// NumberAnimation {
// duration: 100
// easing.type: Easing.OutCubic
// }
// }
PwNodeLinkTracker {
id: linkTracker
node: root.isSink ? Pipewire.defaultAudioSink : Pipewire.defaultAudioSource
}
Repeater {
id: applicationVolumes
model: linkTracker.linkGroups
delegate: RowLayout {
id: cardRow
required property PwLinkGroup modelData
spacing: 0
width: applicationsColumn.width
height: root.baseHeight
Rectangle {
color: ShellSettings.settings.colors["surface_container_high"]
IconImage {
implicitSize: 32
source: {
if (cardRow.modelData.source?.properties["application.icon-name"] == null) {
return "root:resources/general/placeholder.svg";
}
return `image://icon/${cardRow.modelData.source?.properties["application.icon-name"]}`;
}
anchors {
fill: parent
leftMargin: 8
rightMargin: 8
}
}
Layout.preferredWidth: 40
Layout.preferredHeight: root.baseHeight
}
Card {
node: cardRow.modelData.source
isSink: root.isSink
Layout.fillWidth: true
Layout.preferredHeight: root.baseHeight
}
}
}
}
}
}

177
bar/mpris/Card.qml Normal file
View file

@ -0,0 +1,177 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import "../.."
import "../../widgets" as Widgets
Rectangle {
required property var player
radius: 5
color: "transparent"
implicitHeight: 220
RowLayout {
id: cardLayout
spacing: 15
anchors {
fill: parent
leftMargin: 10
rightMargin: 10
topMargin: 10 // Added top margin for better spacing
bottomMargin: 10 // Added bottom margin for better spacing
}
Rectangle {
id: mprisImage
color: "transparent"
radius: 10
width: 200
height: 200
Layout.alignment: Qt.AlignVCenter
visible: true
Image {
anchors.fill: parent
source: player.trackArtUrl
sourceSize.width: 1024
sourceSize.height: 1024
fillMode: Image.PreserveAspectFit
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: mprisImage.width
height: mprisImage.height
radius: 10
color: "white"
}
maskSource: Rectangle {
width: mprisImage.width
height: mprisImage.height
radius: 10
color: "black"
}
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
spread: 0.02
samples: 25
color: "#80000000"
}
}
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 5
Text {
text: player.trackArtist
color: "white"
font.pointSize: 13
font.bold: true
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
elide: Text.ElideRight
}
Text {
text: player.trackTitle
color: "white"
font.pointSize: 13
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
elide: Text.ElideRight
}
RowLayout {
spacing: 6
ColorQuantizer {
id: colorQuantizer
source: Qt.resolvedUrl(Media.trackedPlayer?.trackArtUrl ?? "")
depth: 0
rescaleSize: 64
}
Text {
text: timeStr(player.position)
color: "white"
font {
pointSize: 9
bold: true
}
}
Widgets.RoundSlider {
from: 0
to: 1
accentColor: colorQuantizer.colors[0]
//value: root.node.audio.volume
//onValueChanged: node.audio.volume = value
Layout.fillWidth: true
Layout.preferredHeight: 16
}
Text {
text: timeStr(player.length)
color: "white"
font {
pointSize: 9
bold: true
}
}
}
// Music Controls
RowLayout {
spacing: 2
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
Widgets.IconButton {
implicitSize: 36
activeRectangle: false
padding: 4
source: "root:resources/mpris/previous.svg"
onClicked: player.previous()
}
Widgets.IconButton {
implicitSize: 36
activeRectangle: false
padding: 4
source: player?.isPlaying ? "root:resources/mpris/pause.svg" : "root:resources/mpris/play.svg"
onClicked: {
if (!player.canPlay)
return;
player.isPlaying ? player.pause() : player.play();
}
}
Widgets.IconButton {
implicitSize: 36
activeRectangle: false
padding: 4
source: "root:resources/mpris/next.svg"
onClicked: player.next()
}
}
}
}
function timeStr(time: int): string {
const seconds = time % 60;
const minutes = Math.floor(time / 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
}

46
bar/mpris/Media.qml Normal file
View file

@ -0,0 +1,46 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Mpris
Singleton {
id: root
property MprisPlayer trackedPlayer
property var colors: ["white"]
Instantiator {
model: Mpris.players
Connections {
required property MprisPlayer modelData
target: modelData
Component.onCompleted: {
if (root.trackedPlayer == null || modelData.isPlaying) {
root.trackedPlayer = modelData;
}
}
Component.onDestruction: {
if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) {
for (const player of Mpris.players.values) {
if (player.playbackState.isPlaying) {
root.trackedPlayer = player;
break;
}
}
if (trackedPlayer == null && Mpris.players.values.length != 0) {
trackedPlayer = Mpris.players.values[0];
}
}
}
function onPlaybackStateChanged() {
if (root.trackedPlayer !== modelData)
root.trackedPlayer = modelData;
}
}
}
}

151
bar/mpris/Player.qml Normal file
View file

@ -0,0 +1,151 @@
import QtQuick
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Services.Mpris
import "../.."
PopupWindow {
id: root
width: mediaPlayerContainer.width + 10
height: mediaPlayerContainer.height + 10
color: "transparent"
visible: mediaPlayerContainer.opacity > 0
anchor.rect.x: parentWindow.width / 2 - width / 2
anchor.rect.y: parentWindow.height
function show() {
mediaPlayerContainer.opacity = 1;
}
function hide() {
mediaPlayerContainer.opacity = 0;
}
HoverHandler {
id: hoverHandler
enabled: true
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onHoveredChanged: {
if (hovered == false) {
hide();
}
}
}
Rectangle {
id: mediaPlayerContainer
width: 500
height: mediaPlayerColumn.height + 20
color: ShellGlobals.colors.background
radius: 5
opacity: 0
anchors.centerIn: parent
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: mediaPlayerContainer.width
height: mediaPlayerContainer.height
radius: mediaPlayerContainer.radius
color: "white"
}
maskSource: Rectangle {
width: mediaPlayerContainer.width
height: mediaPlayerContainer.height
radius: mediaPlayerContainer.radius
color: "black"
}
layer.enabled: true
layer.effect: DropShadow {
transparentBorder: true
spread: 0.02
samples: 25
color: "#80000000"
}
}
Behavior on opacity {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
ColorQuantizer {
id: colorQuantizer
source: Qt.resolvedUrl(Media.trackedPlayer?.trackArtUrl ?? "")
depth: 2
rescaleSize: 64
onColorsChanged: {
Media.colors = colors;
}
}
ShaderEffect {
property color topLeftColor: colorQuantizer?.colors[0]?.lighter(1.2) ?? "white"
property color topRightColor: colorQuantizer?.colors[1]?.lighter(1.2) ?? "black"
property color bottomLeftColor: colorQuantizer?.colors[2]?.lighter(1.2) ?? "white"
property color bottomRightColor: colorQuantizer?.colors[3]?.lighter(1.2) ?? "black"
anchors.fill: parent
fragmentShader: "root:/shaders/vertexgradient.frag.qsb"
vertexShader: "root:/shaders/vertexgradient.vert.qsb"
Behavior on topLeftColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
Behavior on topRightColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
Behavior on bottomLeftColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
Behavior on bottomRightColor {
ColorAnimation {
duration: 500
easing.type: Easing.InOutQuad
}
}
}
ColumnLayout {
id: mediaPlayerColumn
spacing: 10
Layout.fillWidth: true
Layout.preferredWidth: parent.width
Layout.margins: 10
implicitHeight: childrenRect.height
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 10
}
// Media Cards
Repeater {
model: Mpris.players
Card {
required property var modelData
player: modelData
Layout.fillWidth: true
}
}
}
}
}

86
bar/mpris/Status.qml Normal file
View file

@ -0,0 +1,86 @@
import QtQuick
import Quickshell.Widgets
import Quickshell.Services.Mpris
import "../.."
Item {
id: root
required property var bar
width: statusInfo.width + 125
height: parent.height
visible: Mpris.players.values.length != 0
Player {
id: mediaPlayer
anchor.window: bar
anchor.rect.x: parentWindow.width / 2 - width / 2
anchor.rect.y: parentWindow.height
}
MouseArea {
id: playButton
hoverEnabled: true
acceptedButtons: Qt.LeftButton | Qt.RightButton
onClicked: mouse => {
if (mouse.button === Qt.LeftButton) {
if (mediaPlayer.visible) {
mediaPlayer.hide();
} else {
mediaPlayer.show();
}
} else {
if (!Media.trackedPlayer.canPlay || Media.trackedPlayer == null)
return;
if (Media.trackedPlayer.isPlaying)
Media.trackedPlayer.pause();
else
Media.trackedPlayer.play();
}
}
anchors.fill: parent
}
Item {
id: statusInfo
width: statusIcon.width + statusIcon.anchors.rightMargin + nowPlayingText.width
height: parent.height
visible: Media.trackedPlayer != null
anchors {
horizontalCenter: parent.horizontalCenter
verticalCenter: parent.verticalCenter
top: parent.top
bottom: parent.botton
margins: 3.5
}
IconImage {
id: statusIcon
implicitSize: 13
source: Media.trackedPlayer?.isPlaying ? "root:resources/mpris/pause.svg" : "root:resources/mpris/play.svg"
anchors {
verticalCenter: parent.verticalCenter
right: nowPlayingText.left
rightMargin: 10
}
}
Text {
id: nowPlayingText
color: ShellGlobals.colors.text
text: `${Media.trackedPlayer?.trackArtist} - ${Media.trackedPlayer?.trackTitle}`
font.pointSize: 11
width: Math.min(implicitWidth, 250)
elide: Text.ElideRight
anchors {
verticalCenter: parent.verticalCenter
right: parent.right
}
}
}
}

View file

@ -0,0 +1,70 @@
pragma ComponentBehavior: Bound
import QtQuick
import Qt5Compat.GraphicalEffects
import Quickshell.Widgets
import "../.."
Item {
id: root
required property var bar
property var implicitSize: 0
readonly property real actualSize: Math.min(root.width, root.height)
implicitWidth: parent.height
implicitHeight: parent.height
NotificationCenter {
id: notificationCenter
}
Rectangle {
color: mouseArea.containsMouse ? ShellSettings.settings.colors["primary"] : "transparent"
radius: 5
anchors {
fill: parent
margins: 1
}
}
MouseArea {
id: mouseArea
hoverEnabled: true
anchors.fill: parent
onPressed: {
if (notificationCenter.visible) {
notificationCenter.hide();
} else {
notificationCenter.show();
}
}
}
Item {
implicitWidth: root.implicitSize
implicitHeight: root.implicitSize
anchors.centerIn: parent
layer.enabled: true
layer.effect: OpacityMask {
source: Rectangle {
width: root.actualSize
height: root.actualSize
color: "white"
}
maskSource: IconImage {
implicitSize: root.actualSize
source: "root:resources/general/notification.svg"
}
}
Rectangle {
color: mouseArea.containsMouse ? ShellSettings.settings.colors["inverse_primary"] : ShellSettings.settings.colors["inverse_surface"]
anchors.fill: parent
}
}
// TODO: notification number overlay
}

161
bar/popups/MenuWindow.qml Normal file
View file

@ -0,0 +1,161 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import "../.."
PopupWindow {
id: root
color: "transparent"
implicitWidth: bar.width
implicitHeight: Math.max(popupContainer.height, 800) + 20
mask: Region {
item: popupContainer
}
anchor {
window: bar
rect: Qt.rect(0, 0, bar.width, bar.height)
edges: Edges.Bottom | Edges.Left
gravity: Edges.Bottom | Edges.Right
adjustment: PopupAdjustment.None
}
required property var bar
function set(item, content) {
content.visible = true;
let itemPos = item.mapToItem(bar.contentItem, 0, bar.height, item.width, 0).x;
// let contentWidth = content.width;
popupContainer.x = itemPos;
popupContent.data = content;
// popupContent.opacity = 0;
// popupContainer.opacity = 0;
popupContainer.opacity = 1;
popupContent.opacity = 1;
root.visible = true;
}
// function set(item, content) {
// content.visible = true;
//
// let itemPos = item.mapToItem(bar.contentItem, 0, bar.height, item.width, 0).x;
// let contentWidth = content.width;
// let padding = 5;
// let xPos = itemPos;
// let idealX = xPos;
// let idealRightEdge = idealX + contentWidth;
//
// // check if touching right edge
// let maxRightEdge = root.width - padding;
// let isTouchingRightEdge = idealRightEdge > maxRightEdge;
//
// if (isTouchingRightEdge) {
// // touching right edge
// let constrainedX = maxRightEdge - contentWidth;
// constrainedX = Math.max(0, constrainedX);
//
// popupContainer.x = constrainedX;
// popupContainer.implicitWidth = 0;
// popupContent.data = content;
// // popupContent.implicitWidth = contentWidth;
// } else {
// // not touching any edge
// // popupContent.implicitWidth = contentWidth;
// popupContainer.x = idealX;
// popupContent.data = content;
// }
//
// popupContainer.y = padding;
//
// popupContent.opacity = 0;
// popupContainer.opacity = 0;
// popupContainer.opacity = 1;
// popupContent.opacity = 1;
// root.visible = true;
// }
function clear() {
popupContainer.opacity = 0;
popupContent.opacity = 0;
popupContent.data = [];
}
WrapperRectangle {
id: popupContainer
property real targetX: 0
color: ShellSettings.settings.colors["surface"]
radius: 12
margin: 8
clip: true
opacity: 0
visible: opacity > 0
onVisibleChanged: {
if (!visible) {
root.visible = false;
}
}
Item {
id: popupContent
implicitWidth: Math.max(childrenRect.width, 120)
implicitHeight: Math.max(childrenRect.height, 60)
opacity: 1
// Behavior on opacity {
// NumberAnimation {
// id: contentOpacity
// duration: 350
// easing.type: Easing.Linear
// from: 0
// to: 1
// }
// }
}
HoverHandler {
id: hover
enabled: true
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onHoveredChanged: {
if (hovered == false)
root.clear();
}
}
// Behavior on opacity {
// NumberAnimation {
// duration: 500
// easing.type: Easing.InOutQuad
// }
// }
//
// Behavior on x {
// enabled: root.visible
// SmoothedAnimation {
// duration: 300
// easing.type: Easing.OutQuad
// }
// }
//
// Behavior on implicitWidth {
// enabled: root.visible
// SmoothedAnimation {
// duration: 300
// easing.type: Easing.OutQuad
// }
// }
//
// Behavior on implicitHeight {
// SmoothedAnimation {
// duration: 200
// easing.type: Easing.Linear
// }
// }
}
}

View file

@ -0,0 +1,9 @@
import Quickshell
import QtQuick
Rectangle {
id: root
visible: false
implicitWidth: 20
implicitHeight: 20
}

97
bar/systray/SysTray.qml Normal file
View file

@ -0,0 +1,97 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.SystemTray
import "../.."
RowLayout {
id: root
spacing: 10
visible: SystemTray.items.values.length > 0
implicitHeight: parent.height
required property var popup
Repeater {
model: SystemTray.items
delegate: Item {
id: trayField
implicitHeight: parent.height
implicitWidth: trayContainer.width
required property SystemTrayItem modelData
MouseArea {
id: trayButton
hoverEnabled: true
anchors.fill: parent
onClicked: {
// trayText.width = sysTrayContent.width - trayIcon.width - trayContainer.spacing;
// trayText.visible = true;
root.popup.set(this, trayMenu);
}
}
QsMenuOpener {
id: menuOpener
menu: trayField.modelData.menu
}
WrapperItem {
id: trayMenu
visible: false
ColumnLayout {
id: menuContainer
spacing: 2
Repeater {
model: menuOpener.children
delegate: TrayMenu {
id: sysTrayContent
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
}
Rectangle {
id: trayContainer
color: trayButton.containsMouse ? ShellSettings.settings.colors["primary"] : "transparent"
radius: width / 2
implicitHeight: parent.height - 2
implicitWidth: parent.height - 2
anchors.centerIn: parent
IconImage {
id: trayIcon
source: {
switch (trayField.modelData.id) {
case "obs":
return "image://icon/obs-tray";
default:
return trayField.modelData.icon;
}
}
anchors {
fill: parent
margins: 1
}
}
Behavior on color {
ColorAnimation {
duration: 100
}
}
}
}
}
}

24
bar/systray/TrayMenu.qml Normal file
View file

@ -0,0 +1,24 @@
import Quickshell
import QtQuick
import QtQuick.Layouts
import "../.."
ColumnLayout {
id: root
required property QsMenuEntry modelData
Rectangle {
visible: (root.modelData?.isSeparator ?? false)
color: ShellSettings.settings.colors["surface_container_high"]
Layout.fillWidth: true
Layout.preferredHeight: 2
Layout.leftMargin: 8
Layout.rightMargin: 8
}
TrayMenuEntry {
visible: !root.modelData?.isSeparator
menuData: root.modelData
Layout.fillWidth: true
}
}

View file

@ -0,0 +1,135 @@
import Quickshell
import Quickshell.Widgets
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../.."
ColumnLayout {
id: root
required property var menuData
WrapperRectangle {
Layout.fillWidth: true
Layout.preferredHeight: 25
radius: 6
color: {
if (!root.menuData?.enabled)
return "transparent";
if (entryArea.containsMouse)
return ShellSettings.settings.colors["primary"];
return "transparent";
}
WrapperMouseArea {
id: entryArea
hoverEnabled: true
anchors.fill: parent
onClicked: {
if (!root.menuData?.enabled)
return;
if (root.menuData?.hasChildren)
subTrayMenu.visible = !subTrayMenu.visible;
root.menuData?.triggered();
}
RowLayout {
id: menuEntry
spacing: 5
Layout.fillWidth: true
Item {
Layout.preferredWidth: 20
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: 5
RadioButton {
id: radioButton
visible: (root.menuData?.buttonType === QsMenuButtonType.RadioButton) ?? false
checked: (root.menuData?.checkState) ?? false
anchors.centerIn: parent
}
CheckBox {
id: checkBox
visible: (root.menuData?.buttonType === QsMenuButtonType.CheckBox) ?? false
checked: (root.menuData?.checkState) ?? false
anchors.centerIn: parent
}
IconImage {
id: entryImage
visible: (root.menuData?.buttonType === QsMenuButtonType.None && root.menuData?.icon !== "") ?? false
source: (root.menuData?.icon) ?? ""
anchors.fill: parent
}
}
Text {
id: text
text: (root.menuData?.text) ?? ""
verticalAlignment: Text.AlignVCenter
color: {
let color = Qt.color(ShellSettings.settings.colors["inverse_surface"]);
if (!root.menuData?.enabled)
return color.darker(2);
if (entryArea.containsMouse)
return Qt.color(ShellSettings.settings.colors["inverse_primary"]);
return color;
}
Layout.fillWidth: true
Layout.fillHeight: true
}
Item {
Layout.fillWidth: true
}
Item {
Layout.preferredHeight: 20
Layout.preferredWidth: 20
Layout.rightMargin: 5
}
}
}
}
WrapperRectangle {
id: subTrayMenu
color: ShellSettings.settings.colors["surface_container"]
radius: 8
visible: false
Layout.fillWidth: true
QsMenuOpener {
id: menuOpener
menu: root.menuData
}
ColumnLayout {
id: subTrayContainer
spacing: 2
Layout.fillWidth: true
Repeater {
model: menuOpener.children
delegate: BoundComponent {
id: subMenuEntry
source: "TrayMenu.qml"
Layout.fillWidth: true
required property var modelData
}
}
}
}
}

313
launcher/Controller.qml Normal file
View file

@ -0,0 +1,313 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import ".."
Singleton {
PersistentProperties {
id: persist
property bool launcherOpen: false
}
IpcHandler {
target: "launcher"
function open(): void {
persist.launcherOpen = true;
}
function close(): void {
persist.launcherOpen = false;
}
function toggle(): void {
persist.launcherOpen = !persist.launcherOpen;
}
}
LazyLoader {
id: loader
activeAsync: persist.launcherOpen
PanelWindow {
implicitWidth: 500
implicitHeight: 7 + searchContainer.implicitHeight + list.topMargin * 2 + list.delegateHeight * 10
color: "transparent"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
WlrLayershell.namespace: "shell:launcher"
Rectangle {
id: container
color: ShellSettings.settings.colors["surface"]
radius: 18
anchors {
fill: parent
margins: 10
}
Behavior on height {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 7
anchors.topMargin: 10
anchors.bottomMargin: 0
spacing: 0
Rectangle {
id: searchContainer
Layout.fillWidth: true
implicitHeight: searchbox.implicitHeight + 15
radius: 10
color: ShellSettings.settings.colors["surface_container"]
border.color: ShellSettings.settings.colors["secondary"]
RowLayout {
id: searchbox
anchors.fill: parent
anchors.margins: 5
TextInput {
id: search
Layout.fillWidth: true
color: ShellSettings.settings.colors["inverse_surface"]
focus: true
Keys.forwardTo: [list]
Keys.onEscapePressed: persist.launcherOpen = false
Keys.onPressed: event => {
if (event.modifiers & Qt.ControlModifier) {
if (event.key == Qt.Key_J) {
list.currentIndex = list.currentIndex == list.count - 1 ? 0 : list.currentIndex + 1;
event.accepted = true;
} else if (event.key == Qt.Key_K) {
list.currentIndex = list.currentIndex == 0 ? list.count - 1 : list.currentIndex - 1;
event.accepted = true;
}
}
}
onAccepted: {
if (list.currentItem) {
list.currentItem.clicked(null);
}
}
onTextChanged: {
list.currentIndex = 0;
}
}
}
}
ListView {
id: list
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
cacheBuffer: 0 // works around QTBUG-131106
//reuseItems: true
model: ScriptModel {
values: DesktopEntries.applications.values.map(object => {
const stxt = search.text.toLowerCase();
const ntxt = object.name.toLowerCase();
let si = 0;
let ni = 0;
let matches = [];
let startMatch = -1;
for (let si = 0; si != stxt.length; ++si) {
const sc = stxt[si];
while (true) {
// Drop any entries with letters that don't exist in order
if (ni == ntxt.length)
return null;
const nc = ntxt[ni++];
if (nc == sc) {
if (startMatch == -1)
startMatch = ni;
break;
} else {
if (startMatch != -1) {
matches.push({
index: startMatch,
length: ni - startMatch
});
startMatch = -1;
}
}
}
}
if (startMatch != -1) {
matches.push({
index: startMatch,
length: ni - startMatch + 1
});
}
return {
object: object,
matches: matches
};
}).filter(entry => entry !== null).sort((a, b) => {
let ai = 0;
let bi = 0;
let s = 0;
while (ai != a.matches.length && bi != b.matches.length) {
const am = a.matches[ai];
const bm = b.matches[bi];
s = bm.length - am.length;
if (s != 0)
return s;
s = am.index - bm.index;
if (s != 0)
return s;
++ai;
++bi;
}
s = a.matches.length - b.matches.length;
if (s != 0)
return s;
s = a.object.name.length - b.object.name.length;
if (s != 0)
return s;
return a.object.name.localeCompare(b.object.name);
}).map(entry => entry.object)
onValuesChanged: list.currentIndex = 0
}
topMargin: 7
bottomMargin: list.count == 0 ? 0 : 7
add: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: 100
}
}
displaced: Transition {
NumberAnimation {
property: "y"
duration: 200
easing.type: Easing.OutCubic
}
NumberAnimation {
property: "opacity"
to: 1
duration: 100
}
}
move: Transition {
NumberAnimation {
property: "y"
duration: 200
easing.type: Easing.OutCubic
}
NumberAnimation {
property: "opacity"
to: 1
duration: 100
}
}
remove: Transition {
NumberAnimation {
property: "y"
duration: 200
easing.type: Easing.OutCubic
}
NumberAnimation {
property: "opacity"
to: 0
duration: 100
}
}
highlight: Rectangle {
radius: 12
color: ShellSettings.settings.colors["primary"]
}
keyNavigationEnabled: true
keyNavigationWraps: true
highlightMoveVelocity: -1
highlightMoveDuration: 100
preferredHighlightBegin: list.topMargin
preferredHighlightEnd: list.height - list.bottomMargin
highlightRangeMode: ListView.ApplyRange
snapMode: ListView.SnapToItem
readonly property real delegateHeight: 44
delegate: MouseArea {
required property DesktopEntry modelData
implicitHeight: list.delegateHeight
implicitWidth: ListView.view.width
onClicked: {
modelData.execute();
persist.launcherOpen = false;
}
RowLayout {
id: delegateLayout
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: 5
}
IconImage {
Layout.alignment: Qt.AlignVCenter
asynchronous: true
implicitSize: 30
source: Quickshell.iconPath(modelData.icon)
}
Text {
text: modelData.name
color: ShellSettings.settings.colors["inverse_surface"]
Layout.alignment: Qt.AlignVCenter
}
}
}
}
}
}
}
}
function init() {
}
}

View file

@ -0,0 +1,218 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.Services.Notifications
import "../widgets/" as Widgets
import ".."
import "../.."
Item {
id: root
required property var notification
signal expired(Notification notification)
signal closed(Notification notification)
width: parent.width
height: Math.min(row.implicitHeight + 30, 400)
Rectangle {
id: container
radius: 10
color: ShellSettings.settings.colors["surface_container"]
anchors.fill: parent
Item {
id: timerController
property int totalDuration: 5000
property int remainingTime: totalDuration
property bool isRunning: false
property real lastTime: 0
Timer {
id: internalTimer
interval: 16
repeat: true
running: timerController.isRunning
onTriggered: {
var currentTime = Date.now();
if (timerController.lastTime > 0) {
var delta = currentTime - timerController.lastTime;
timerController.remainingTime -= delta;
if (timerController.remainingTime <= 0) {
timerController.isRunning = false;
root.expired(root.notification);
}
}
timerController.lastTime = currentTime;
}
}
function start() {
if (!isRunning) {
lastTime = Date.now();
isRunning = true;
}
}
function pause() {
isRunning = false;
lastTime = 0;
}
Component.onCompleted: {
start();
}
}
MouseArea {
id: notificationArea
hoverEnabled: true
anchors.fill: parent
onContainsMouseChanged: {
progressAnimation.paused = containsMouse;
if (containsMouse) {
timerController.pause();
} else {
timerController.start();
}
}
}
RowLayout {
id: row
spacing: 5
anchors {
fill: parent
margins: 15
}
ColumnLayout {
Layout.fillWidth: true
RowLayout {
id: topRow
spacing: 10
Layout.fillWidth: true
IconImage {
visible: root.notification.appIcon != ""
source: Quickshell.iconPath(root.notification.appIcon)
implicitSize: 24
}
RowLayout {
Layout.fillWidth: true
Text {
id: appName
text: root.notification.appName
color: ShellSettings.settings.colors["inverse_surface"]
font.pointSize: 11
font.bold: true
elide: Text.ElideRight
maximumLineCount: 1
Layout.preferredWidth: implicitWidth
Layout.maximumWidth: topRow.width * 0.3
}
Widgets.Separator {}
Text {
id: summaryText
text: root.notification.summary
color: ShellSettings.settings.colors["inverse_surface"]
font.pointSize: 11
elide: Text.ElideRight
maximumLineCount: 1
Layout.fillWidth: true
}
}
Item {
id: closeButton
width: 24
height: 24
Layout.alignment: Qt.AlignTop
Canvas {
id: progressCircle
anchors.fill: parent
antialiasing: true
property real progress: 1.0
onProgressChanged: requestPaint()
onPaint: {
var ctx = getContext("2d");
ctx.reset();
var centerX = width / 2;
var centerY = height / 2;
var radius = Math.min(width, height) / 2 - 2;
ctx.beginPath();
ctx.arc(centerX, centerY, radius, -Math.PI / 2, -Math.PI / 2 + 2 * Math.PI * progress);
ctx.strokeStyle = ShellSettings.settings.colors["primary"];
ctx.lineWidth = 2;
ctx.stroke();
}
}
NumberAnimation {
id: progressAnimation
target: progressCircle
property: "progress"
from: 1.0
to: 0.0
duration: 5000
running: true
easing.type: Easing.Linear
}
Rectangle {
id: closeButtonBg
anchors.centerIn: parent
width: 16
height: 16
color: "#FF474D"
radius: 10
visible: closeButtonArea.containsMouse
}
MouseArea {
id: closeButtonArea
hoverEnabled: true
anchors.fill: parent
onPressed: root.closed(root.notification)
}
IconImage {
source: "image://icon/window-close"
implicitSize: 16
anchors.centerIn: parent
}
}
}
ColumnLayout {
Layout.fillWidth: true
Text {
id: bodyText
text: root.notification.body
color: ShellSettings.settings.colors["inverse_surface"]
font.pointSize: 11
wrapMode: Text.Wrap
elide: Text.ElideRight
maximumLineCount: 10
Layout.fillWidth: true
}
}
}
}
}
}

View file

@ -0,0 +1,98 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ".."
Scope {
id: root
Connections {
target: Notifications.notificationServer
function onNotification(notification) {
notificationLoader.item.visible = true;
}
}
LazyLoader {
id: notificationLoader
loading: true
PanelWindow {
id: notificationWindow
property var visibleCount: {
let count = 0;
for (let i = 0; i < toastList.count; i++) {
let item = toastList.itemAt(i);
if (item && item.visible) {
count++;
}
}
return count;
}
onVisibleCountChanged: visible = visibleCount != 0
color: "transparent"
implicitWidth: 525
visible: false
exclusionMode: ExclusionMode.Normal
mask: Region {
item: notifLayout
}
anchors {
top: true
bottom: true
right: true
}
Text {
text: "length: " + notificationWindow.visibleCount
}
ColumnLayout {
id: notifLayout
spacing: 15
anchors {
top: parent.top
left: parent.left
right: parent.right
margins: 5
}
Repeater {
id: toastList
model: ScriptModel {
values: Notifications.notificationServer.trackedNotifications.values.concat()
}
delegate: ActiveToast {
id: toast
required property var modelData
notification: modelData
Connections {
target: toast
function onExpired(notification) {
toast.visible = false;
}
function onClosed(notification) {
notification.dismiss();
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,278 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Qt5Compat.GraphicalEffects
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Widgets
import "../widgets" as Widgets
import ".."
Singleton {
PersistentProperties {
id: persist
property bool notificationsOpen: false
}
IpcHandler {
id: ipc
target: "notifications"
function open(): void {
persist.notificationsOpen = true;
}
function close(): void {
persist.notificationsOpen = false;
}
function toggle(): void {
persist.notificationsOpen = !persist.notificationsOpen;
}
}
LazyLoader {
id: loader
activeAsync: persist.notificationsOpen
PanelWindow {
id: notificationPanel
color: "red"
implicitWidth: 500
exclusionMode: ExclusionMode.Normal
// WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
anchors {
top: true
right: true
bottom: true
}
ColumnLayout {
spacing: 10
anchors {
fill: parent
margins: 10
}
Text {
text: "Notifications: " + toastList.count
Layout.fillWidth: true
}
ListView {
id: toastList
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
spacing: 5
model: ScriptModel {
values: {
const notifications = Notifications.notificationServer.trackedNotifications.values.concat();
const groupedByApp = notifications.reduce((groups, notification) => {
const appName = notification.appName;
if (!groups[appName]) {
groups[appName] = {
appName: appName,
summaryGroups: {}
};
}
const summary = notification.summary;
const image = notification.image;
if (!groups[appName].summaryGroups[summary]) {
groups[appName].summaryGroups[summary] = {
summary: summary,
image: image,
notifications: []
};
}
groups[appName].summaryGroups[summary].notifications.push(notification);
return groups;
}, {});
return Object.values(groupedByApp).map(appGroup => {
return {
appName: appGroup.appName,
summaryGroups: Object.values(appGroup.summaryGroups)
};
});
}
}
delegate: Item {
id: toastWrapper
required property var modelData
width: ListView.view.width
height: toastContent.height
Item {
id: toastContent
width: parent.width
height: contentColumn.implicitHeight
anchors.centerIn: parent
ColumnLayout {
id: contentColumn
spacing: 2
anchors {
fill: parent
margins: 0
}
// Notification content
Repeater {
model: toastWrapper.modelData.summaryGroups
delegate: Rectangle {
id: summaryGroup
required property var modelData
required property int index
Layout.fillWidth: true
Layout.preferredHeight: groupContent.implicitHeight + 24
color: ShellSettings.settings.colors["surface_container"]
antialiasing: true
topLeftRadius: index === 0 ? 25 : 5
topRightRadius: index === 0 ? 25 : 5
bottomLeftRadius: index === (toastWrapper.modelData.summaryGroups.length - 1) ? 25 : 5
bottomRightRadius: index === (toastWrapper.modelData.summaryGroups.length - 1) ? 25 : 5
ColumnLayout {
id: groupContent
spacing: 8
anchors {
fill: parent
margins: 12
}
RowLayout {
spacing: 12
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
ColumnLayout {
Layout.alignment: Qt.AlignTop
spacing: 0
Item {
id: imageContainer
Layout.preferredWidth: 36
Layout.preferredHeight: 36
visible: summaryGroup.modelData.image != ""
antialiasing: true
Image {
id: notificationImage
anchors.fill: parent
source: summaryGroup.modelData.image
fillMode: Image.PreserveAspectCrop
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: notificationImage.width
height: notificationImage.height
radius: notificationImage.width / 2
antialiasing: true
}
}
}
}
}
// Content column
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignTop
spacing: 8
// Header row
RowLayout {
Layout.fillWidth: true
spacing: 8
Text {
text: summaryGroup.modelData.summary
font.pixelSize: 16
font.weight: Font.Medium
color: ShellSettings.settings.colors["on_surface"]
wrapMode: Text.WordWrap
maximumLineCount: 2
elide: Text.ElideRight
}
Widgets.Separator {}
Text {
text: "now"
font.pixelSize: 14
color: ShellSettings.settings.colors["on_surface_variant"]
Layout.alignment: Qt.AlignVCenter
}
}
// Notification bodies
ColumnLayout {
Layout.fillWidth: true
spacing: 2
Repeater {
model: summaryGroup.modelData.notifications
delegate: ColumnLayout {
id: bodyDelegate
required property var modelData
required property int index
Layout.fillWidth: true
spacing: 0
Text {
Layout.fillWidth: true
text: bodyDelegate.modelData.body
font.pixelSize: 14
color: ShellSettings.settings.colors["on_surface_variant"]
wrapMode: Text.WordWrap
maximumLineCount: 4
elide: Text.ElideRight
lineHeight: 1.3
visible: bodyDelegate.modelData.body != ""
}
}
}
}
}
}
}
}
}
}
}
}
}
}
// HyprlanFocusGrab {
// id: grab
// windows: [notificationPanel]
// onCleared: {
// ipc.hide();
// }
// }
}
}
function init() {
}
}

View file

@ -0,0 +1,23 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Services.Notifications
Singleton {
property alias notificationServer: notifServer
NotificationServer {
id: notifServer
actionsSupported: true
persistenceSupported: true
}
Connections {
target: notifServer
function onNotification(notification) {
notification.tracked = true;
}
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M24,0V24H0V0Z" fill="none"/>
<path d="M20,10V8.33A1.34,1.34,0,0,0,18.67,7H3.34A1.34,1.34,0,0,0,2,8.33v7.33A1.34,1.34,0,0,0,3.33,17H18.67A1.34,1.34,0,0,0,20,15.67V14h2V10Zm-8.5,3v2L4,11H9.5V9L17,13Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 448 B

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M20,15.67V14h2V10H20V8.33A1.34,1.34,0,0,0,18.67,7H3.34A1.34,1.34,0,0,0,2,8.33v7.33A1.34,1.34,0,0,0,3.33,17H18.67A1.34,1.34,0,0,0,20,15.67Z"/>
<path d="M20,15.67V14h2V10H20V8.33A1.34,1.34,0,0,0,18.67,7H3.34A1.34,1.34,0,0,0,2,8.33v7.33A1.34,1.34,0,0,0,3.33,17H18.67A1.34,1.34,0,0,0,20,15.67Z" fill="white"/>
<path d="M24,0V24H0V0Z" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 581 B

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg height="64px" viewBox="0 0 16 16" width="64px" xmlns="http://www.w3.org/2000/svg">
<g fill="white">
<path d="m 4.550781 1 c -1.9375 0 -3.5 1.5625 -3.5 3.5 s 1.5625 3.5 3.5 3.5 h 7 c 1.941407 0 3.5 -1.5625 3.5 -3.5 s -1.558593 -3.5 -3.5 -3.5 z m 7 1 c 1.386719 0 2.5 1.113281 2.5 2.5 c 0 1.382812 -1.113281 2.5 -2.5 2.5 c -1.382812 0 -2.5 -1.117188 -2.5 -2.5 c 0 -1.386719 1.117188 -2.5 2.5 -2.5 z m 0 0"/>
<path d="m 4.550781 9 c -1.9375 0 -3.5 1.5625 -3.5 3.5 s 1.5625 3.5 3.5 3.5 h 7 c 1.941407 0 3.5 -1.5625 3.5 -3.5 s -1.558593 -3.5 -3.5 -3.5 z m 0 1 c 1.386719 0 2.5 1.113281 2.5 2.5 c 0 1.382812 -1.113281 2.5 -2.5 2.5 c -1.382812 0 -2.5 -1.117188 -2.5 -2.5 c 0 -1.386719 1.117188 -2.5 2.5 -2.5 z m 0 0" fill="white"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 805 B

Some files were not shown because too many files have changed in this diff Show more