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

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
}
}
}
}
}