commit 05cd51b54e72418159c2b87cab3de55112e3ff4a Author: kossLAN Date: Sat Jun 7 04:01:14 2025 -0400 Initial commit diff --git a/.stfolder/syncthing-folder-13b209.txt b/.stfolder/syncthing-folder-13b209.txt new file mode 100644 index 0000000..876cb95 --- /dev/null +++ b/.stfolder/syncthing-folder-13b209.txt @@ -0,0 +1,5 @@ +# This directory is a Syncthing folder marker. +# Do not delete. + +folderID: quickshell +created: 2024-12-23T02:09:06-05:00 diff --git a/.stversions/PriorityScreens~20250509-203451.qml b/.stversions/PriorityScreens~20250509-203451.qml new file mode 100644 index 0000000..ce2f7c0 --- /dev/null +++ b/.stversions/PriorityScreens~20250509-203451.qml @@ -0,0 +1,8 @@ +pragma Singleton + +import QtQuick +import Quickshell + +Singleton { + property var screens: ["LG ULTRAGEAR+", "NE135A1M-NY1"] +} diff --git a/.stversions/ReloadPopup~20250307-175400.qml b/.stversions/ReloadPopup~20250307-175400.qml new file mode 100644 index 0000000..ff71c60 --- /dev/null +++ b/.stversions/ReloadPopup~20250307-175400.qml @@ -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() + } + } + } +} diff --git a/.stversions/ShellGlobals~20250307-175400.qml b/.stversions/ShellGlobals~20250307-175400.qml new file mode 100644 index 0000000..825d474 --- /dev/null +++ b/.stversions/ShellGlobals~20250307-175400.qml @@ -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"; + } +} diff --git a/.stversions/ShellGlobals~20250509-203451.qml b/.stversions/ShellGlobals~20250509-203451.qml new file mode 100644 index 0000000..222120a --- /dev/null +++ b/.stversions/ShellGlobals~20250509-203451.qml @@ -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" + } +} diff --git a/.stversions/bar/ActiveWindow~20250107-005428.qml b/.stversions/bar/ActiveWindow~20250107-005428.qml new file mode 100644 index 0000000..47f58ec --- /dev/null +++ b/.stversions/bar/ActiveWindow~20250107-005428.qml @@ -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]; + } + } + } + } +} diff --git a/.stversions/bar/ActiveWindow~20250107-005503.qml b/.stversions/bar/ActiveWindow~20250107-005503.qml new file mode 100644 index 0000000..b8377ec --- /dev/null +++ b/.stversions/bar/ActiveWindow~20250107-005503.qml @@ -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]; + } + } + } + } +} diff --git a/.stversions/bar/ActiveWindow~20250107-005542.qml b/.stversions/bar/ActiveWindow~20250107-005542.qml new file mode 100644 index 0000000..4039fea --- /dev/null +++ b/.stversions/bar/ActiveWindow~20250107-005542.qml @@ -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]; + } + } + } + } +} diff --git a/.stversions/bar/ActiveWindow~20250107-005825.qml b/.stversions/bar/ActiveWindow~20250107-005825.qml new file mode 100644 index 0000000..0fab3be --- /dev/null +++ b/.stversions/bar/ActiveWindow~20250107-005825.qml @@ -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]; + } + } + } + } +} diff --git a/.stversions/bar/ActiveWindow~20250307-175400.qml b/.stversions/bar/ActiveWindow~20250307-175400.qml new file mode 100644 index 0000000..56141f6 --- /dev/null +++ b/.stversions/bar/ActiveWindow~20250307-175400.qml @@ -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]; + } + } + } +} diff --git a/.stversions/bar/Bar~20250107-010815.qml b/.stversions/bar/Bar~20250107-010815.qml new file mode 100644 index 0000000..6578e3f --- /dev/null +++ b/.stversions/bar/Bar~20250107-010815.qml @@ -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; + } + } +} diff --git a/.stversions/bar/Bar~20250107-010845.qml b/.stversions/bar/Bar~20250107-010845.qml new file mode 100644 index 0000000..e1e8667 --- /dev/null +++ b/.stversions/bar/Bar~20250107-010845.qml @@ -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; + } + } +} diff --git a/.stversions/bar/Bar~20250107-010938.qml b/.stversions/bar/Bar~20250107-010938.qml new file mode 100644 index 0000000..538e846 --- /dev/null +++ b/.stversions/bar/Bar~20250107-010938.qml @@ -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; + } + } +} diff --git a/.stversions/bar/Bar~20250307-175400.qml b/.stversions/bar/Bar~20250307-175400.qml new file mode 100644 index 0000000..74135b9 --- /dev/null +++ b/.stversions/bar/Bar~20250307-175400.qml @@ -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; + } + } +} diff --git a/.stversions/bar/Bar~20250509-203449.qml b/.stversions/bar/Bar~20250509-203449.qml new file mode 100644 index 0000000..1266ec5 --- /dev/null +++ b/.stversions/bar/Bar~20250509-203449.qml @@ -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 + } + } +} diff --git a/.stversions/bar/BatteryIndicator~20250307-175400.qml b/.stversions/bar/BatteryIndicator~20250307-175400.qml new file mode 100644 index 0000000..bbba594 --- /dev/null +++ b/.stversions/bar/BatteryIndicator~20250307-175400.qml @@ -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; + } +} diff --git a/.stversions/bar/Clock~20250307-175400.qml b/.stversions/bar/Clock~20250307-175400.qml new file mode 100644 index 0000000..65a03c2 --- /dev/null +++ b/.stversions/bar/Clock~20250307-175400.qml @@ -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; +} diff --git a/.stversions/bar/HyprWorkspaces~20250509-203448.qml b/.stversions/bar/HyprWorkspaces~20250509-203448.qml new file mode 100644 index 0000000..36df04c --- /dev/null +++ b/.stversions/bar/HyprWorkspaces~20250509-203448.qml @@ -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}`) + } + } + } +} diff --git a/.stversions/bar/Separator~20250307-175400.qml b/.stversions/bar/Separator~20250307-175400.qml new file mode 100644 index 0000000..d2b8288 --- /dev/null +++ b/.stversions/bar/Separator~20250307-175400.qml @@ -0,0 +1,9 @@ +import QtQuick +import ".." + +Rectangle { + color: ShellGlobals.colors.highlight; + radius: 5; + width: 7.5; + height: 7.5; +} diff --git a/.stversions/bar/SwayWorkspaces~20250509-203451.qml b/.stversions/bar/SwayWorkspaces~20250509-203451.qml new file mode 100644 index 0000000..623d877 --- /dev/null +++ b/.stversions/bar/SwayWorkspaces~20250509-203451.qml @@ -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; + } +} diff --git a/.stversions/bar/SysTray~20250307-175400.qml b/.stversions/bar/SysTray~20250307-175400.qml new file mode 100644 index 0000000..6e17989 --- /dev/null +++ b/.stversions/bar/SysTray~20250307-175400.qml @@ -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); + } + + } +} diff --git a/.stversions/bar/SysTray~20250509-203448.qml b/.stversions/bar/SysTray~20250509-203448.qml new file mode 100644 index 0000000..9ba2a1f --- /dev/null +++ b/.stversions/bar/SysTray~20250509-203448.qml @@ -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() + } + } +} diff --git a/.stversions/bar/Workspaces~20250307-175400.qml b/.stversions/bar/Workspaces~20250307-175400.qml new file mode 100644 index 0000000..c71e4c9 --- /dev/null +++ b/.stversions/bar/Workspaces~20250307-175400.qml @@ -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); + } + } + } +} diff --git a/.stversions/bar/control/ControlButton~20250307-175400.qml b/.stversions/bar/control/ControlButton~20250307-175400.qml new file mode 100644 index 0000000..0dcf480 --- /dev/null +++ b/.stversions/bar/control/ControlButton~20250307-175400.qml @@ -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(); + } + } +} diff --git a/.stversions/bar/control/ControlPanel~20250307-175400.qml b/.stversions/bar/control/ControlPanel~20250307-175400.qml new file mode 100644 index 0000000..d3913cf --- /dev/null +++ b/.stversions/bar/control/ControlPanel~20250307-175400.qml @@ -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; + } + } + } + } +} + diff --git a/.stversions/bar/control/ControlSlider~20250307-175400.qml b/.stversions/bar/control/ControlSlider~20250307-175400.qml new file mode 100644 index 0000000..ed88b42 --- /dev/null +++ b/.stversions/bar/control/ControlSlider~20250307-175400.qml @@ -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 { } +} diff --git a/.stversions/bar/control/ControlVSlider~20250307-175400.qml b/.stversions/bar/control/ControlVSlider~20250307-175400.qml new file mode 100644 index 0000000..bb60c01 --- /dev/null +++ b/.stversions/bar/control/ControlVSlider~20250307-175400.qml @@ -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 { } +} diff --git a/.stversions/bar/control/Control~20250307-175400.qml b/.stversions/bar/control/Control~20250307-175400.qml new file mode 100644 index 0000000..ecfd822 --- /dev/null +++ b/.stversions/bar/control/Control~20250307-175400.qml @@ -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; + } + } +} + diff --git a/.stversions/bar/mpris/Card~20250509-203448.qml b/.stversions/bar/mpris/Card~20250509-203448.qml new file mode 100644 index 0000000..5d367eb --- /dev/null +++ b/.stversions/bar/mpris/Card~20250509-203448.qml @@ -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')}`; + } +} diff --git a/.stversions/bar/mpris/MediaInfo~20250107-004337.qml b/.stversions/bar/mpris/MediaInfo~20250107-004337.qml new file mode 100644 index 0000000..bdc7027 --- /dev/null +++ b/.stversions/bar/mpris/MediaInfo~20250107-004337.qml @@ -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 + } +} diff --git a/.stversions/bar/mpris/MediaInfo~20250107-004401.qml b/.stversions/bar/mpris/MediaInfo~20250107-004401.qml new file mode 100644 index 0000000..b6b21f4 --- /dev/null +++ b/.stversions/bar/mpris/MediaInfo~20250107-004401.qml @@ -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 + } +} diff --git a/.stversions/bar/mpris/MediaInfo~20250107-004431.qml b/.stversions/bar/mpris/MediaInfo~20250107-004431.qml new file mode 100644 index 0000000..84eabb9 --- /dev/null +++ b/.stversions/bar/mpris/MediaInfo~20250107-004431.qml @@ -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 + } +} diff --git a/.stversions/bar/mpris/MediaInfo~20250107-004532.qml b/.stversions/bar/mpris/MediaInfo~20250107-004532.qml new file mode 100644 index 0000000..1cca802 --- /dev/null +++ b/.stversions/bar/mpris/MediaInfo~20250107-004532.qml @@ -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 + } +} diff --git a/.stversions/bar/mpris/MediaInfo~20250307-175400.qml b/.stversions/bar/mpris/MediaInfo~20250307-175400.qml new file mode 100644 index 0000000..5748758 --- /dev/null +++ b/.stversions/bar/mpris/MediaInfo~20250307-175400.qml @@ -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; + } + } + } +} diff --git a/.stversions/bar/mpris/MediaSwitcher.qml~20250307-175400.bak b/.stversions/bar/mpris/MediaSwitcher.qml~20250307-175400.bak new file mode 100644 index 0000000..0d99bcb --- /dev/null +++ b/.stversions/bar/mpris/MediaSwitcher.qml~20250307-175400.bak @@ -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 + } +} diff --git a/.stversions/bar/mpris/MediaSwitcher~20250107-004917.qml b/.stversions/bar/mpris/MediaSwitcher~20250107-004917.qml new file mode 100644 index 0000000..c6062ba --- /dev/null +++ b/.stversions/bar/mpris/MediaSwitcher~20250107-004917.qml @@ -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 + } +} + diff --git a/.stversions/bar/mpris/MediaSwitcher~20250107-004927.qml b/.stversions/bar/mpris/MediaSwitcher~20250107-004927.qml new file mode 100644 index 0000000..25ddb5a --- /dev/null +++ b/.stversions/bar/mpris/MediaSwitcher~20250107-004927.qml @@ -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 + } +} + diff --git a/.stversions/bar/mpris/MediaSwitcher~20250107-004947.qml b/.stversions/bar/mpris/MediaSwitcher~20250107-004947.qml new file mode 100644 index 0000000..1e9b2b1 --- /dev/null +++ b/.stversions/bar/mpris/MediaSwitcher~20250107-004947.qml @@ -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 + } +} + diff --git a/.stversions/bar/mpris/MediaSwitcher~20250107-005025.qml b/.stversions/bar/mpris/MediaSwitcher~20250107-005025.qml new file mode 100644 index 0000000..0346d4a --- /dev/null +++ b/.stversions/bar/mpris/MediaSwitcher~20250107-005025.qml @@ -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 + } +} + diff --git a/.stversions/bar/mpris/MediaSwitcher~20250307-175400.qml b/.stversions/bar/mpris/MediaSwitcher~20250307-175400.qml new file mode 100644 index 0000000..cbc747c --- /dev/null +++ b/.stversions/bar/mpris/MediaSwitcher~20250307-175400.qml @@ -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(); + } + } + } + } + } + } + } +} + diff --git a/.stversions/bar/mpris/Media~20250307-175400.qml b/.stversions/bar/mpris/Media~20250307-175400.qml new file mode 100644 index 0000000..2c9a0cb --- /dev/null +++ b/.stversions/bar/mpris/Media~20250307-175400.qml @@ -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; + } + } + } +} diff --git a/.stversions/bar/mpris/Player~20250509-203448.qml b/.stversions/bar/mpris/Player~20250509-203448.qml new file mode 100644 index 0000000..2d9dd49 --- /dev/null +++ b/.stversions/bar/mpris/Player~20250509-203448.qml @@ -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 + } + } + } + } +} diff --git a/.stversions/bar/mpris/Status~20250509-203451.qml b/.stversions/bar/mpris/Status~20250509-203451.qml new file mode 100644 index 0000000..1d31b29 --- /dev/null +++ b/.stversions/bar/mpris/Status~20250509-203451.qml @@ -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 + } + } + } +} diff --git a/.stversions/bar/notifications/Notifications~20241229-025012.qml b/.stversions/bar/notifications/Notifications~20241229-025012.qml new file mode 100644 index 0000000..72eda3e --- /dev/null +++ b/.stversions/bar/notifications/Notifications~20241229-025012.qml @@ -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; + } + } + } +} diff --git a/.stversions/bar/notifications/Notifications~20250307-175400.qml b/.stversions/bar/notifications/Notifications~20250307-175400.qml new file mode 100644 index 0000000..5d6a69b --- /dev/null +++ b/.stversions/bar/notifications/Notifications~20250307-175400.qml @@ -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; + } + } + } + } + } +} diff --git a/.stversions/bar/notifications/Toast~20241229-025203.qml b/.stversions/bar/notifications/Toast~20241229-025203.qml new file mode 100644 index 0000000..e9779b1 --- /dev/null +++ b/.stversions/bar/notifications/Toast~20241229-025203.qml @@ -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; + } + } +} + diff --git a/.stversions/bar/notifications/Toast~20241229-183725.qml b/.stversions/bar/notifications/Toast~20241229-183725.qml new file mode 100644 index 0000000..abf22fd --- /dev/null +++ b/.stversions/bar/notifications/Toast~20241229-183725.qml @@ -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; + } + } +} + diff --git a/.stversions/bar/notifications/Toast~20241229-183824.qml b/.stversions/bar/notifications/Toast~20241229-183824.qml new file mode 100644 index 0000000..c0e6076 --- /dev/null +++ b/.stversions/bar/notifications/Toast~20241229-183824.qml @@ -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; + } + } +} + diff --git a/.stversions/bar/notifications/Toast~20241229-184059.qml b/.stversions/bar/notifications/Toast~20241229-184059.qml new file mode 100644 index 0000000..63cff72 --- /dev/null +++ b/.stversions/bar/notifications/Toast~20241229-184059.qml @@ -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; + } + } +} + diff --git a/.stversions/bar/notifications/Toast~20250307-175400.qml b/.stversions/bar/notifications/Toast~20250307-175400.qml new file mode 100644 index 0000000..c1a69b7 --- /dev/null +++ b/.stversions/bar/notifications/Toast~20250307-175400.qml @@ -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; + } + } + } + } + } +} + diff --git a/.stversions/bar/volume/Button~20250509-203451.qml b/.stversions/bar/volume/Button~20250509-203451.qml new file mode 100644 index 0000000..2d0d7da --- /dev/null +++ b/.stversions/bar/volume/Button~20250509-203451.qml @@ -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); + } + } + } +} diff --git a/.stversions/bar/volume/Card~20250509-203451.qml b/.stversions/bar/volume/Card~20250509-203451.qml new file mode 100644 index 0000000..569f2c4 --- /dev/null +++ b/.stversions/bar/volume/Card~20250509-203451.qml @@ -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 + } + } + } +} diff --git a/.stversions/bar/volume/ControlPanel~20250509-203451.qml b/.stversions/bar/volume/ControlPanel~20250509-203451.qml new file mode 100644 index 0000000..0488048 --- /dev/null +++ b/.stversions/bar/volume/ControlPanel~20250509-203451.qml @@ -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 + } + } + } + } + } + } +} diff --git a/.stversions/launcher/Controller~20250307-175400.qml b/.stversions/launcher/Controller~20250307-175400.qml new file mode 100644 index 0000000..a7a1402 --- /dev/null +++ b/.stversions/launcher/Controller~20250307-175400.qml @@ -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() {} +} diff --git a/.stversions/launcher/Controller~20250509-203448.qml b/.stversions/launcher/Controller~20250509-203448.qml new file mode 100644 index 0000000..67abd62 --- /dev/null +++ b/.stversions/launcher/Controller~20250509-203448.qml @@ -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() { + } +} diff --git a/.stversions/notifications/Notifications~20250509-203451.qml b/.stversions/notifications/Notifications~20250509-203451.qml new file mode 100644 index 0000000..5916c43 --- /dev/null +++ b/.stversions/notifications/Notifications~20250509-203451.qml @@ -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 + } + } + } + } + } +} diff --git a/.stversions/notifications/Notifications~20250605-105246.qml b/.stversions/notifications/Notifications~20250605-105246.qml new file mode 100644 index 0000000..e12d7ed --- /dev/null +++ b/.stversions/notifications/Notifications~20250605-105246.qml @@ -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; + } + } +} diff --git a/.stversions/notifications/Toast~20250509-203448.qml b/.stversions/notifications/Toast~20250509-203448.qml new file mode 100644 index 0000000..f2176be --- /dev/null +++ b/.stversions/notifications/Toast~20250509-203448.qml @@ -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 + } + } + } + } + } +} diff --git a/.stversions/resources/battery/battery-100~20241228-024204.svg b/.stversions/resources/battery/battery-100~20241228-024204.svg new file mode 100644 index 0000000..e1e66cb --- /dev/null +++ b/.stversions/resources/battery/battery-100~20241228-024204.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/.stversions/resources/battery/battery-100~20250509-203448.svg b/.stversions/resources/battery/battery-100~20250509-203448.svg new file mode 100644 index 0000000..66b337d --- /dev/null +++ b/.stversions/resources/battery/battery-100~20250509-203448.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/.stversions/resources/battery/battery-charging~20241228-024225.svg b/.stversions/resources/battery/battery-charging~20241228-024225.svg new file mode 100644 index 0000000..481509a --- /dev/null +++ b/.stversions/resources/battery/battery-charging~20241228-024225.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/.stversions/resources/battery/battery-charging~20250509-203448.svg b/.stversions/resources/battery/battery-charging~20250509-203448.svg new file mode 100644 index 0000000..39e9d3f --- /dev/null +++ b/.stversions/resources/battery/battery-charging~20250509-203448.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/.stversions/shell~20250307-175400.qml b/.stversions/shell~20250307-175400.qml new file mode 100644 index 0000000..228c802 --- /dev/null +++ b/.stversions/shell~20250307-175400.qml @@ -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 {} +} diff --git a/.stversions/shell~20250509-203449.qml b/.stversions/shell~20250509-203449.qml new file mode 100644 index 0000000..7a99639 --- /dev/null +++ b/.stversions/shell~20250509-203449.qml @@ -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 {} +} diff --git a/.stversions/widgets/IconButton~20250307-175400.qml b/.stversions/widgets/IconButton~20250307-175400.qml new file mode 100644 index 0000000..547e13d --- /dev/null +++ b/.stversions/widgets/IconButton~20250307-175400.qml @@ -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(); + } +} diff --git a/.stversions/widgets/IconButton~20250509-203449.qml b/.stversions/widgets/IconButton~20250509-203449.qml new file mode 100644 index 0000000..d7a2bf5 --- /dev/null +++ b/.stversions/widgets/IconButton~20250509-203449.qml @@ -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() + } +} diff --git a/.stversions/widgets/RoundSlider~20250509-203448.qml b/.stversions/widgets/RoundSlider~20250509-203448.qml new file mode 100644 index 0000000..0cfa5a6 --- /dev/null +++ b/.stversions/widgets/RoundSlider~20250509-203448.qml @@ -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 {} +} diff --git a/.stversions/widgets/Separator~20250509-203449.qml b/.stversions/widgets/Separator~20250509-203449.qml new file mode 100644 index 0000000..fda1023 --- /dev/null +++ b/.stversions/widgets/Separator~20250509-203449.qml @@ -0,0 +1,9 @@ +import QtQuick +import ".." + +Rectangle { + color: ShellGlobals.colors.accent + radius: 5 + width: 7.5 + height: 7.5 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/ReloadPopup.qml b/ReloadPopup.qml new file mode 100644 index 0000000..90376e3 --- /dev/null +++ b/ReloadPopup.qml @@ -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() + } + } + } +} diff --git a/ShellSettings.qml b/ShellSettings.qml new file mode 100644 index 0000000..93bade9 --- /dev/null +++ b/ShellSettings.qml @@ -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" + } + } + } +} diff --git a/bar/ActiveWindow.qml b/bar/ActiveWindow.qml new file mode 100644 index 0000000..849433f --- /dev/null +++ b/bar/ActiveWindow.qml @@ -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 +} diff --git a/bar/Bar.qml b/bar/Bar.qml new file mode 100644 index 0000000..7a93719 --- /dev/null +++ b/bar/Bar.qml @@ -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"] + } + } +} diff --git a/bar/BatteryIndicator.qml b/bar/BatteryIndicator.qml new file mode 100644 index 0000000..4269186 --- /dev/null +++ b/bar/BatteryIndicator.qml @@ -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 + } + } +} diff --git a/bar/Clock.qml b/bar/Clock.qml new file mode 100644 index 0000000..eee72e8 --- /dev/null +++ b/bar/Clock.qml @@ -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 +} diff --git a/bar/HyprWorkspaces.qml b/bar/HyprWorkspaces.qml new file mode 100644 index 0000000..c8df3b9 --- /dev/null +++ b/bar/HyprWorkspaces.qml @@ -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}`) + } + } + } +} diff --git a/bar/control/Button.qml b/bar/control/Button.qml new file mode 100644 index 0000000..65cd859 --- /dev/null +++ b/bar/control/Button.qml @@ -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); + } + } + } +} diff --git a/bar/control/ControlPanel.qml b/bar/control/ControlPanel.qml new file mode 100644 index 0000000..e7182e8 --- /dev/null +++ b/bar/control/ControlPanel.qml @@ -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 + } + } + } +} diff --git a/bar/control/MediaPlayer.qml b/bar/control/MediaPlayer.qml new file mode 100644 index 0000000..4580283 --- /dev/null +++ b/bar/control/MediaPlayer.qml @@ -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')}`; + } +} diff --git a/bar/control/ProfileImage.qml b/bar/control/ProfileImage.qml new file mode 100644 index 0000000..b7f33d1 --- /dev/null +++ b/bar/control/ProfileImage.qml @@ -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" + } + } + } +} diff --git a/bar/control/volume/Card.qml b/bar/control/volume/Card.qml new file mode 100644 index 0000000..2af17e1 --- /dev/null +++ b/bar/control/volume/Card.qml @@ -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; + } + } + } +} diff --git a/bar/control/volume/Mixer.qml b/bar/control/volume/Mixer.qml new file mode 100644 index 0000000..0b48931 --- /dev/null +++ b/bar/control/volume/Mixer.qml @@ -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 + } + } + } + } + } +} diff --git a/bar/mpris/Card.qml b/bar/mpris/Card.qml new file mode 100644 index 0000000..a075de5 --- /dev/null +++ b/bar/mpris/Card.qml @@ -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')}`; + } +} diff --git a/bar/mpris/Media.qml b/bar/mpris/Media.qml new file mode 100644 index 0000000..98ff93a --- /dev/null +++ b/bar/mpris/Media.qml @@ -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; + } + } + } +} diff --git a/bar/mpris/Player.qml b/bar/mpris/Player.qml new file mode 100644 index 0000000..e6c7837 --- /dev/null +++ b/bar/mpris/Player.qml @@ -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 + } + } + } + } +} diff --git a/bar/mpris/Status.qml b/bar/mpris/Status.qml new file mode 100644 index 0000000..9d96f13 --- /dev/null +++ b/bar/mpris/Status.qml @@ -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 + } + } + } +} diff --git a/bar/notifications/NotificationButton.qml b/bar/notifications/NotificationButton.qml new file mode 100644 index 0000000..f2c0972 --- /dev/null +++ b/bar/notifications/NotificationButton.qml @@ -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 +} diff --git a/bar/popups/MenuWindow.qml b/bar/popups/MenuWindow.qml new file mode 100644 index 0000000..0ffde8b --- /dev/null +++ b/bar/popups/MenuWindow.qml @@ -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 + // } + // } + } +} diff --git a/bar/systray/SubTrayMenu.qml b/bar/systray/SubTrayMenu.qml new file mode 100644 index 0000000..c6dca25 --- /dev/null +++ b/bar/systray/SubTrayMenu.qml @@ -0,0 +1,9 @@ +import Quickshell +import QtQuick + +Rectangle { + id: root + visible: false + implicitWidth: 20 + implicitHeight: 20 +} diff --git a/bar/systray/SysTray.qml b/bar/systray/SysTray.qml new file mode 100644 index 0000000..93f066b --- /dev/null +++ b/bar/systray/SysTray.qml @@ -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 + } + } + } + } + } +} diff --git a/bar/systray/TrayMenu.qml b/bar/systray/TrayMenu.qml new file mode 100644 index 0000000..0e0c8eb --- /dev/null +++ b/bar/systray/TrayMenu.qml @@ -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 + } +} diff --git a/bar/systray/TrayMenuEntry.qml b/bar/systray/TrayMenuEntry.qml new file mode 100644 index 0000000..58d5ae0 --- /dev/null +++ b/bar/systray/TrayMenuEntry.qml @@ -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 + } + } + } + } +} diff --git a/launcher/Controller.qml b/launcher/Controller.qml new file mode 100644 index 0000000..aff6a2c --- /dev/null +++ b/launcher/Controller.qml @@ -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() { + } +} diff --git a/notifications/ActiveToast.qml b/notifications/ActiveToast.qml new file mode 100644 index 0000000..3eaea01 --- /dev/null +++ b/notifications/ActiveToast.qml @@ -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 + } + } + } + } + } +} diff --git a/notifications/Controller.qml b/notifications/Controller.qml new file mode 100644 index 0000000..f325439 --- /dev/null +++ b/notifications/Controller.qml @@ -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(); + } + } + } + } + } + } + } +} diff --git a/notifications/NotificationCenter.qml b/notifications/NotificationCenter.qml new file mode 100644 index 0000000..bd254ca --- /dev/null +++ b/notifications/NotificationCenter.qml @@ -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() { + } +} diff --git a/notifications/Notifications.qml b/notifications/Notifications.qml new file mode 100644 index 0000000..a76b632 --- /dev/null +++ b/notifications/Notifications.qml @@ -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; + } + } +} diff --git a/resources/battery/battery-charge.svg b/resources/battery/battery-charge.svg new file mode 100644 index 0000000..39e9d3f --- /dev/null +++ b/resources/battery/battery-charge.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/battery/battery.svg b/resources/battery/battery.svg new file mode 100644 index 0000000..66b337d --- /dev/null +++ b/resources/battery/battery.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/resources/control/controls-button.svg b/resources/control/controls-button.svg new file mode 100644 index 0000000..1d50a9e --- /dev/null +++ b/resources/control/controls-button.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/resources/control/lock.svg b/resources/control/lock.svg new file mode 100644 index 0000000..db842ce --- /dev/null +++ b/resources/control/lock.svg @@ -0,0 +1,2 @@ + + diff --git a/resources/control/shutdown.svg b/resources/control/shutdown.svg new file mode 100644 index 0000000..6d84ad9 --- /dev/null +++ b/resources/control/shutdown.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/control/sleep.svg b/resources/control/sleep.svg new file mode 100644 index 0000000..7206f9f --- /dev/null +++ b/resources/control/sleep.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/general/down-arrow.svg b/resources/general/down-arrow.svg new file mode 100644 index 0000000..6470d1a --- /dev/null +++ b/resources/general/down-arrow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/general/nixos.svg b/resources/general/nixos.svg new file mode 100644 index 0000000..34e9baa --- /dev/null +++ b/resources/general/nixos.svg @@ -0,0 +1 @@ + diff --git a/resources/general/notification.svg b/resources/general/notification.svg new file mode 100644 index 0000000..21a0f2f --- /dev/null +++ b/resources/general/notification.svg @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/resources/general/pfp.png b/resources/general/pfp.png new file mode 100644 index 0000000..113ad36 Binary files /dev/null and b/resources/general/pfp.png differ diff --git a/resources/general/placeholder.svg b/resources/general/placeholder.svg new file mode 100644 index 0000000..0fae206 --- /dev/null +++ b/resources/general/placeholder.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/general/right-arrow.svg b/resources/general/right-arrow.svg new file mode 100644 index 0000000..0b46dfb --- /dev/null +++ b/resources/general/right-arrow.svg @@ -0,0 +1,4 @@ + + + + diff --git a/resources/mask.png b/resources/mask.png new file mode 100644 index 0000000..e6cac94 Binary files /dev/null and b/resources/mask.png differ diff --git a/resources/mpris/next.svg b/resources/mpris/next.svg new file mode 100644 index 0000000..706815d --- /dev/null +++ b/resources/mpris/next.svg @@ -0,0 +1,19 @@ + + + + + next [#998] + Created with Sketch. + + + + + + + + + + + + + diff --git a/resources/mpris/pause.svg b/resources/mpris/pause.svg new file mode 100644 index 0000000..f19b075 --- /dev/null +++ b/resources/mpris/pause.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/resources/mpris/play.svg b/resources/mpris/play.svg new file mode 100644 index 0000000..849fa14 --- /dev/null +++ b/resources/mpris/play.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/resources/mpris/previous.svg b/resources/mpris/previous.svg new file mode 100644 index 0000000..895139f --- /dev/null +++ b/resources/mpris/previous.svg @@ -0,0 +1,19 @@ + + + + + previous [#999] + Created with Sketch. + + + + + + + + + + + + + diff --git a/resources/mpris/shuffle.svg b/resources/mpris/shuffle.svg new file mode 100644 index 0000000..4df19c7 --- /dev/null +++ b/resources/mpris/shuffle.svg @@ -0,0 +1,6 @@ + + + +shuffle + + diff --git a/resources/mpris/stop.svg b/resources/mpris/stop.svg new file mode 100644 index 0000000..6d2870a --- /dev/null +++ b/resources/mpris/stop.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/resources/volume/microphone-full.svg b/resources/volume/microphone-full.svg new file mode 100644 index 0000000..8d1a116 --- /dev/null +++ b/resources/volume/microphone-full.svg @@ -0,0 +1,2 @@ + + diff --git a/resources/volume/microphone-mute.svg b/resources/volume/microphone-mute.svg new file mode 100644 index 0000000..8c2d3b5 --- /dev/null +++ b/resources/volume/microphone-mute.svg @@ -0,0 +1,2 @@ + + diff --git a/resources/volume/volume-full.svg b/resources/volume/volume-full.svg new file mode 100644 index 0000000..5126fca --- /dev/null +++ b/resources/volume/volume-full.svg @@ -0,0 +1,15 @@ + + + + volume-up-solid + + + + + + + + + + + diff --git a/resources/volume/volume-mute.svg b/resources/volume/volume-mute.svg new file mode 100644 index 0000000..3604983 --- /dev/null +++ b/resources/volume/volume-mute.svg @@ -0,0 +1,16 @@ + + + + volume-off-solid + + + + + + + + + + + + diff --git a/resources/wallpapers/pixelart0.jpg b/resources/wallpapers/pixelart0.jpg new file mode 100644 index 0000000..7da7dba Binary files /dev/null and b/resources/wallpapers/pixelart0.jpg differ diff --git a/resources/wallpapers/wallhaven-0w3ej7.jpg b/resources/wallpapers/wallhaven-0w3ej7.jpg new file mode 100644 index 0000000..990cc34 Binary files /dev/null and b/resources/wallpapers/wallhaven-0w3ej7.jpg differ diff --git a/resources/wallpapers/wallhaven-2yp6gg.png b/resources/wallpapers/wallhaven-2yp6gg.png new file mode 100644 index 0000000..8c65a14 Binary files /dev/null and b/resources/wallpapers/wallhaven-2yp6gg.png differ diff --git a/resources/wallpapers/wallhaven-5g22q5.png b/resources/wallpapers/wallhaven-5g22q5.png new file mode 100644 index 0000000..a405652 Binary files /dev/null and b/resources/wallpapers/wallhaven-5g22q5.png differ diff --git a/resources/wallpapers/wallhaven-5w9em7.jpg b/resources/wallpapers/wallhaven-5w9em7.jpg new file mode 100644 index 0000000..5b9f714 Binary files /dev/null and b/resources/wallpapers/wallhaven-5w9em7.jpg differ diff --git a/resources/wallpapers/wallhaven-96y9qk.jpg b/resources/wallpapers/wallhaven-96y9qk.jpg new file mode 100644 index 0000000..80a03c4 Binary files /dev/null and b/resources/wallpapers/wallhaven-96y9qk.jpg differ diff --git a/resources/wallpapers/wallhaven-od2lwm.jpg b/resources/wallpapers/wallhaven-od2lwm.jpg new file mode 100644 index 0000000..93e1865 Binary files /dev/null and b/resources/wallpapers/wallhaven-od2lwm.jpg differ diff --git a/resources/wallpapers/wallhaven-zywwky.jpg b/resources/wallpapers/wallhaven-zywwky.jpg new file mode 100644 index 0000000..1c0d406 Binary files /dev/null and b/resources/wallpapers/wallhaven-zywwky.jpg differ diff --git a/settings/Controller.qml b/settings/Controller.qml new file mode 100644 index 0000000..23a0b37 --- /dev/null +++ b/settings/Controller.qml @@ -0,0 +1,309 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Qt.labs.folderlistmodel +import "../" + +Singleton { + PersistentProperties { + id: persist + property bool windowOpen: false + } + + IpcHandler { + target: "settings" + + function open(): void { + persist.windowOpen = true; + } + + function close(): void { + persist.windowOpen = false; + } + + function toggle(): void { + persist.windowOpen = !persist.windowOpen; + } + } + + LazyLoader { + id: loader + activeAsync: persist.windowOpen + + FloatingWindow { + color: ShellSettings.settings.colors["surface"] + implicitWidth: 840 + implicitHeight: 845 + + // onWidthChanged: { + // console.log("height: " + height); + // console.log("width: " + width); + // } + + maximumSize { + width: 840 + height: 845 + } + + minimumSize { + width: 840 + height: 845 + } + + onVisibleChanged: { + if (!visible) { + persist.windowOpen = false; + } + } + + ColumnLayout { + id: container + spacing: 5 + + anchors { + fill: parent + margins: 10 + } + + ClippingRectangle { + radius: 20 + Layout.preferredWidth: 464 + Layout.preferredHeight: 261 + Layout.alignment: Qt.AlignCenter + Layout.margins: 20 + + Image { + id: wallpaperImage + source: ShellSettings.settings.wallpaperUrl + fillMode: Image.PreserveAspectFit + + anchors { + fill: parent + } + } + } + + Rectangle { + color: ShellSettings.settings.colors["surface_container"] + radius: 20 + Layout.fillWidth: true + Layout.fillHeight: true + + ColumnLayout { + anchors.fill: parent + + ListView { + id: horizontalList + orientation: ListView.Horizontal + model: ["scheme-content", "scheme-expressive", "scheme-fidelity", "scheme-fruit-salad", "scheme-monochrome", "scheme-neutral", "scheme-rainbow", "scheme-tonal-spot", "scheme-vibrant"] + spacing: 10 + clip: true + + Layout.fillWidth: true + Layout.preferredHeight: 100 + Layout.margins: 10 + + delegate: Rectangle { + id: paletteCell + required property string modelData + property string matugenConf: Qt.resolvedUrl("root:wallpaper/matugen.toml").toString().replace("file://", "") + property var colors: { + "primary": "white", + "secondary": "gray", + "tertiary": "lightgrey", + "container": "black" + } + + width: 100 + height: 100 + color: paletteSelect.containsMouse ? ShellSettings.settings.colors["surface_container_highest"] : ShellSettings.settings.colors["surface_container_high"] + radius: 20 + + MouseArea { + id: paletteSelect + hoverEnabled: true + anchors.fill: parent + onPressed: { + ShellSettings.settings.colorScheme = paletteCell.modelData; + } + } + + Item { + id: paletteContainer + width: 80 + height: 80 + anchors.centerIn: parent + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: paletteContainer.width + height: paletteContainer.height + radius: 20 + } + } + + Rectangle { + id: topLeft + color: paletteCell.colors["primary"] ?? "white" + width: parent.width / 2 + height: parent.height / 2 + } + + Rectangle { + id: topRight + color: paletteCell.colors["secondary"] ?? "gray" + width: parent.width / 2 + height: parent.height / 2 + anchors.left: topLeft.right + } + + Rectangle { + id: bottomLeft + color: paletteCell.colors["tertiary"] ?? "lightgrey" + width: parent.width / 2 + height: parent.height / 2 + anchors.top: topLeft.bottom + } + + Rectangle { + id: bottomRight + color: paletteCell.colors["surface"] ?? "black" + width: parent.width / 2 + height: parent.height / 2 + anchors { + top: topRight.bottom + left: bottomLeft.right + } + } + } + + Connections { + target: ShellSettings.settings + function onWallpaperUrlChanged() { + matugen.running = true; + } + } + + Process { + id: matugen + running: true + command: ["matugen", "image", ShellSettings.settings.wallpaperUrl.replace("file://", ""), "--type", paletteCell.modelData, "--json", "hex", "--config", paletteCell.matugenConf, "--dry-run"] + + stdout: SplitParser { + onRead: data => { + try { + paletteCell.colors = JSON.parse(data)['colors']['dark']; + } catch (e) { + console.error("Error parsing JSON:", e); + } + } + } + + stderr: SplitParser { + onRead: data => console.log(`line read: ${data}`) + } + } + } + } + + Rectangle { + color: ShellSettings.settings.colors["surface_container_high"] + Layout.fillWidth: true + Layout.preferredHeight: 1 + } + + GridView { + id: wallpaperGrid + cellWidth: 200 + cellHeight: 200 + clip: true + Layout.fillWidth: true + Layout.fillHeight: true + Layout.margins: 10 + + model: FolderListModel { + id: folderModel + folder: Qt.resolvedUrl("root:resources/wallpapers") + nameFilters: ["*.jpg", "*.png"] + } + + delegate: Rectangle { + id: cell + required property var modelData + width: 200 + height: 200 + color: "transparent" + + Item { + anchors.fill: parent + + Rectangle { + id: border + visible: mouseArea.containsMouse + color: "transparent" + radius: 20 + + border { + color: ShellSettings.settings.colors["primary"] + width: 2 + } + + anchors { + fill: parent + margins: 1 + } + } + + Image { + id: image + source: cell.modelData.fileUrl + fillMode: Image.PreserveAspectCrop + asynchronous: true + + sourceSize { + height: image.height + width: image.width + } + + anchors { + fill: parent + margins: 5 + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: cell.width + height: cell.height + radius: 20 + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + onClicked: { + ShellSettings.settings.wallpaperUrl = cell.modelData.fileUrl; + } + } + } + } + } + } + } + } + } + + function init() { + } +} diff --git a/shaders/mask.frag b/shaders/mask.frag new file mode 100644 index 0000000..3f93a1a --- /dev/null +++ b/shaders/mask.frag @@ -0,0 +1,25 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(binding = 1) uniform sampler2D source; +layout(binding = 2) uniform sampler2D mask; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; +}; + +void main() { + vec4 sourceColor = texture(source, qt_TexCoord0); + vec4 maskColor = texture(mask, qt_TexCoord0); + + // Use the mask's luminance to determine opacity + float maskValue = dot(maskColor.rgb, vec3(0.299, 0.587, 0.114)); + + // Black areas of mask = transparent, white areas = opaque + sourceColor.a *= (1.0 - maskValue) * qt_Opacity; + + fragColor = sourceColor; +} diff --git a/shaders/mask.frag.qsb b/shaders/mask.frag.qsb new file mode 100644 index 0000000..5a3382a Binary files /dev/null and b/shaders/mask.frag.qsb differ diff --git a/shaders/vertexgradient.frag b/shaders/vertexgradient.frag new file mode 100644 index 0000000..0b82457 --- /dev/null +++ b/shaders/vertexgradient.frag @@ -0,0 +1,18 @@ +#version 440 + +layout(location = 0) in vec2 coord; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + vec4 topLeftColor; + vec4 topRightColor; + vec4 bottomLeftColor; + vec4 bottomRightColor; +} ubuf; + +void main() { + vec4 topColor = mix(ubuf.topLeftColor, ubuf.topRightColor, coord.x); + vec4 bottomColor = mix(ubuf.bottomLeftColor, ubuf.bottomRightColor, coord.x); + fragColor = mix(topColor, bottomColor, coord.y); +} diff --git a/shaders/vertexgradient.frag.qsb b/shaders/vertexgradient.frag.qsb new file mode 100644 index 0000000..4781cf9 Binary files /dev/null and b/shaders/vertexgradient.frag.qsb differ diff --git a/shaders/vertexgradient.vert b/shaders/vertexgradient.vert new file mode 100644 index 0000000..f91120b --- /dev/null +++ b/shaders/vertexgradient.vert @@ -0,0 +1,21 @@ +#version 440 + +layout(location = 0) in vec4 qt_Vertex; +layout(location = 1) in vec2 qt_MultiTexCoord0; + +layout(location = 0) out vec2 coord; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + vec4 topLeftColor; + vec4 topRightColor; + vec4 bottomLeftColor; + vec4 bottomRightColor; +} ubuf; + +out gl_PerVertex { vec4 gl_Position; }; + +void main() { + coord = qt_MultiTexCoord0; + gl_Position = ubuf.qt_Matrix * qt_Vertex; +} diff --git a/shaders/vertexgradient.vert.qsb b/shaders/vertexgradient.vert.qsb new file mode 100644 index 0000000..70a12d3 Binary files /dev/null and b/shaders/vertexgradient.vert.qsb differ diff --git a/shaders/wallpapertransition.frag b/shaders/wallpapertransition.frag new file mode 100644 index 0000000..8982ba7 --- /dev/null +++ b/shaders/wallpapertransition.frag @@ -0,0 +1,33 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float progress; + vec2 aspectRatio; + vec2 origin; +}; + +layout(binding = 1) uniform sampler2D fromImage; +layout(binding = 2) uniform sampler2D toImage; + +void main() { + vec2 uv = qt_TexCoord0; + + vec2 scaledUV = (uv - origin) * aspectRatio; + float distance = length(scaledUV); + + vec2 maxVec = max(origin, vec2(1.0) - origin) * aspectRatio; + float maxDistance = length(maxVec); + + float threshold = progress * maxDistance; + + if (distance < threshold) { + fragColor = texture(toImage, uv) * qt_Opacity; + } else { + fragColor = texture(fromImage, uv) * qt_Opacity; + } +} diff --git a/shell.qml b/shell.qml new file mode 100644 index 0000000..f2e6544 --- /dev/null +++ b/shell.qml @@ -0,0 +1,31 @@ +//@ pragma UseQApplication +//@ pragma IconTheme Papirus-Dark +import Quickshell +import QtQuick +import "bar" as Bar +import "notifications" as Notifications +import "volume-osd" as VolumeOSD +import "settings" as Settings +import "launcher" as Launcher +import "wallpaper" as Wallpaper + +ShellRoot { + Component.onCompleted: [Launcher.Controller.init(), Settings.Controller.init(), Notifications.NotificationCenter.init()] + + Variants { + model: Quickshell.screens + + Scope { + id: scope + property var modelData + + Bar.Bar { + screen: scope.modelData + } + } + } + + Notifications.Controller {} + VolumeOSD.Controller {} + Wallpaper.Controller {} +} diff --git a/volume-osd/Controller.qml b/volume-osd/Controller.qml new file mode 100644 index 0000000..b693a57 --- /dev/null +++ b/volume-osd/Controller.qml @@ -0,0 +1,119 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Qt5Compat.GraphicalEffects +import Quickshell +import Quickshell.Services.Pipewire +import "../widgets" as Widgets +import ".." + +Scope { + id: root + + // Bind the pipewire node so its volume will be tracked + PwObjectTracker { + objects: [Pipewire.defaultAudioSink] + } + + Connections { + target: Pipewire.defaultAudioSink?.audio + + function onVolumeChanged() { + root.shouldShowOsd = true; + hideTimer.restart(); + } + } + + property bool shouldShowOsd: false + + Timer { + id: hideTimer + interval: 1000 + onTriggered: root.shouldShowOsd = false + } + + // The OSD window will be created and destroyed based on shouldShowOsd. + // PanelWindow.visible could be set instead of using a loader, but using + // a loader will reduce the memory overhead when the window isn't open. + LazyLoader { + active: root.shouldShowOsd + + PanelWindow { + // Since the panel's screen is unset, it will be picked by the compositor + // when the window is created. Most compositors pick the current active monitor. + + anchors.bottom: true + margins.bottom: 300 + + implicitWidth: 400 + implicitHeight: 50 + color: "transparent" + + // An empty click mask prevents the window from blocking mouse events. + mask: Region {} + + Rectangle { + anchors.fill: parent + radius: 8 + color: { + let color = ShellSettings.settings.colors["surface"]; + return Qt.rgba(color.r, color.g, color.b, 0.8); + } + + RowLayout { + anchors { + fill: parent + leftMargin: 10 + rightMargin: 15 + } + + Widgets.ColoredIcon { + implicitSize: 30 + source: "root:resources/volume/volume-full.svg" + color: ShellSettings.settings.colors["inverse_surface"] + } + + Rectangle { + id: sliderBackground + Layout.fillWidth: true + implicitHeight: 10 + radius: 20 + color: { + let color = ShellSettings.settings.colors["inverse_surface"]; + return Qt.rgba(color.r, color.g, color.b, 0.5); + } + + layer.enabled: true + layer.effect: OpacityMask { + source: Rectangle { + width: sliderBackground.width + height: sliderBackground.height + radius: sliderBackground.radius + color: "white" + } + + maskSource: Rectangle { + width: sliderBackground.width + height: sliderBackground.height + radius: sliderBackground.radius + color: "black" + } + } + + Rectangle { + color: ShellSettings.settings.colors["primary"] + anchors { + left: parent.left + top: parent.top + bottom: parent.bottom + } + + implicitWidth: parent.width * (Pipewire.defaultAudioSink?.audio.volume ?? 0) + } + } + } + } + } + } +} diff --git a/wallpaper/Controller.qml b/wallpaper/Controller.qml new file mode 100644 index 0000000..21e25e4 --- /dev/null +++ b/wallpaper/Controller.qml @@ -0,0 +1,75 @@ +import Quickshell +import Quickshell.Io +import QtQuick +import ".." + +Scope { + id: root + required property var screen + property string matugenConf: Qt.resolvedUrl("matugen.toml").toString().replace("file://", "") + + LazyLoader { + loading: true + + Scope { + Variants { + model: Quickshell.screens + + PanelWindow { + required property var modelData + color: "black" + aboveWindows: false + screen: modelData + + anchors { + left: true + right: true + top: true + bottom: true + } + + Image { + source: ShellSettings.settings.wallpaperUrl + fillMode: Image.PreserveAspectCrop + anchors.fill: parent + } + } + } + + Connections { + target: ShellSettings.settings + + function onWallpaperUrlChanged() { + console.log("Switching wallpaper: " + ShellSettings.settings.wallpaperUrl); + matugen.running = true; + } + + function onColorSchemeChanged() { + console.log("Switching color scheme: " + ShellSettings.settings.colorScheme); + matugen.running = true; + } + } + + Process { + id: matugen + running: false + + // Formatter is keeping me hostage frfr... + command: ["matugen", "image", ShellSettings.settings.wallpaperUrl.replace("file://", ""), "--type", ShellSettings.settings.colorScheme, "--json", "hex", "--config", root.matugenConf] + + stdout: SplitParser { + onRead: data => { + console.log(ShellSettings.settings.colorScheme); + try { + ShellSettings.settings.colors = JSON.parse(data)['colors']['dark']; + } catch (e) {} + } + } + + stderr: SplitParser { + onRead: data => console.log(`line read: ${data}`) + } + } + } + } +} diff --git a/wallpaper/matugen.toml b/wallpaper/matugen.toml new file mode 100644 index 0000000..271e822 --- /dev/null +++ b/wallpaper/matugen.toml @@ -0,0 +1,27 @@ +[config.custom_colors] + +[templates.kde] +input_path = "templates/BreezeDark.colors" +mode = "Dark" +output_path = "~/.config/kdeglobals" +# post_hook = "systemctl restart --user plasma-xdg-desktop-portal-kde.service" + +[templates.nvim] +input_path = "templates/nvim.json" +mode = "Dark" +output_path = "~/.local/share/nvim-colors.json" + +[templates.qt5ct] +input_path = "templates/qtct-colors.conf" +mode = "Dark" +output_path = "~/.config/qt5ct/colors/matugen.conf" + +[templates.qt6ct] +input_path = "templates/BreezeDark.colors" +mode = "Dark" +output_path = "~/.config/qt6ct/colors/BreezeDark.colors" + +[templates.hyprland] +input_path = 'templates/hyprland-colors.conf' +output_path = '~/.config/hypr/colors.conf' +post_hook = 'hyprctl reload' diff --git a/wallpaper/templates/BreezeDark.colors b/wallpaper/templates/BreezeDark.colors new file mode 100644 index 0000000..b7a5fa6 --- /dev/null +++ b/wallpaper/templates/BreezeDark.colors @@ -0,0 +1,211 @@ +# SPDX-FileCopyrightText: Andrew Lake +# SPDX-FileCopyrightText: Marco Martin +# SPDX-FileCopyrightText: Nate Graham +# SPDX-FileCopyrightText: Noah Davis +# SPDX-FileCopyrightText: Neal Gompa +# SPDX-FileCopyrightText: David Redondo +# SPDX-License-Identifier: LGPL-2.0-or-later + +[ColorEffects:Disabled] +Color={{colors.outline.default.red}},{{colors.outline.default.green}},{{colors.outline.default.blue}} +ColorAmount=0 +ColorEffect=0 +ContrastAmount=0.65 +ContrastEffect=1 +IntensityAmount=0.1 +IntensityEffect=2 + +[ColorEffects:Inactive] +ChangeSelectionColor=true +Color={{colors.outline_variant.default.red}},{{colors.outline_variant.default.green}},{{colors.outline_variant.default.blue}} +ColorAmount=0.025 +ColorEffect=2 +ContrastAmount=0.1 +ContrastEffect=2 +Enable=false +IntensityAmount=0 +IntensityEffect=0 + +[Colors:Button] +BackgroundAlternate={{colors.surface_container_high.default.red}},{{colors.surface_container_high.default.green}},{{colors.surface_container_high.default.blue}} +BackgroundNormal={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Colors:Complementary] +BackgroundAlternate={{colors.surface_container_high.default.red}},{{colors.surface_container_high.default.green}},{{colors.surface_container_high.default.blue}} +BackgroundNormal={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Colors:Header] +BackgroundAlternate={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}} +BackgroundNormal={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Colors:Header][Inactive] +BackgroundAlternate={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +BackgroundNormal={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Colors:Selection] +BackgroundAlternate={{colors.surface_container_high.default.red}},{{colors.surface_container_high.default.green}},{{colors.surface_container_high.default.blue}} +BackgroundNormal={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.background.default.red}},{{colors.background.default.green}},{{colors.background.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Colors:Tooltip] +BackgroundAlternate={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}} +BackgroundNormal={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Colors:View] +BackgroundAlternate={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}} +BackgroundNormal={{colors.surface.default.red}},{{colors.surface.default.green}},{{colors.surface.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Colors:Window] +BackgroundAlternate={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +BackgroundNormal={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}} +DecorationFocus={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +DecorationHover={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundActive={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundInactive={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +ForegroundLink={{colors.primary.default.red}},{{colors.primary.default.green}},{{colors.primary.default.blue}} +ForegroundNegative={{colors.error.default.red}},{{colors.error.default.green}},{{colors.error.default.blue}} +ForegroundNeutral={{colors.secondary.default.red}},{{colors.secondary.default.green}},{{colors.secondary.default.blue}} +ForegroundNormal={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +ForegroundPositive={{colors.tertiary.default.red}},{{colors.tertiary.default.green}},{{colors.tertiary.default.blue}} +ForegroundVisited={{colors.tertiary_container.default.red}},{{colors.tertiary_container.default.green}},{{colors.tertiary_container.default.blue}} + +[Icons] +Theme=breeze-dark + +[General] +ColorScheme=BreezeDark +Name=Breeze Dark +Name[ar]=نسيم داكن +Name[az]=Breeze - Tünd +Name[bg]=Breeze Тъмен +Name[bs]=Breeze tamna +Name[ca]=Brisa fosca +Name[ca@valencia]=Brisa fosca +Name[cs]=Breeze Tmavé +Name[da]=Breeze Dark +Name[de]=Breeze Dunkel +Name[el]=Breeze σκούρο +Name[en_GB]=Breeze Dark +Name[eo]=Breeze Dark +Name[es]=Brisa oscuro +Name[et]=Breeze tume +Name[eu]=Breeze iluna +Name[fi]=Tumma Breeze +Name[fr]=Brise sombre +Name[gl]=Brisa escura +Name[he]=בריזה כהה +Name[hi]=ब्रीज़ गहरा +Name[hu]=Breeze Dark +Name[ia]=Brisa obscure +Name[id]=Breeze Gelap +Name[is]=Breeze dökkt +Name[it]=Brezza scuro +Name[ja]=Breeze ダーク +Name[ka]=Breeze მუქი +Name[ko]=어두운 Breeze +Name[lt]=Breeze tamsus +Name[lv]=Breeze Dark +Name[nb]=Breeze mørk +Name[nl]=Breeze Dark +Name[nn]=Breeze mørk +Name[pa]=ਬਰੀਜ਼ ਗੂੜ੍ਹਾ +Name[pl]=Ciemna Bryza +Name[pt]=Brisa Escura +Name[pt_BR]=Breeze Dark +Name[ro]=Briză, întunecat +Name[ru]=Breeze, тёмный вариант +Name[sa]=वायुः अन्धकारः +Name[sk]=Tmavý vánok +Name[sl]=Sapica, temna +Name[sr]=Поветарац тамни +Name[sr@ijekavian]=Поветарац тамни +Name[sr@ijekavianlatin]=Povetarac tamni +Name[sr@latin]=Povetarac tamni +Name[sv]=Breeze mörk +Name[tg]=Насими торик +Name[tr]=Esinti Koyu +Name[uk]=Темна Breeze +Name[x-test]=xxBreeze Darkxx +Name[zh_CN]=Breeze 微风深色 +Name[zh_TW]=Breeze Dark +shadeSortColumn=true + +[KDE] +contrast=4 + +[WM] +activeBackground={{colors.surface_container.default.red}},{{colors.surface_container.default.green}},{{colors.surface_container.default.blue}} +activeBlend={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +activeForeground={{colors.on_surface.default.red}},{{colors.on_surface.default.green}},{{colors.on_surface.default.blue}} +inactiveBackground={{colors.surface_container_low.default.red}},{{colors.surface_container_low.default.green}},{{colors.surface_container_low.default.blue}} +inactiveBlend={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} +inactiveForeground={{colors.on_surface_variant.default.red}},{{colors.on_surface_variant.default.green}},{{colors.on_surface_variant.default.blue}} diff --git a/wallpaper/templates/hyprland-colors.conf b/wallpaper/templates/hyprland-colors.conf new file mode 100644 index 0000000..d7e6821 --- /dev/null +++ b/wallpaper/templates/hyprland-colors.conf @@ -0,0 +1,4 @@ +<* for name, value in colors *> +$image = {{image}} +${{name}} = rgba({{value.default.hex_stripped}}ff) +<* endfor *> diff --git a/wallpaper/templates/nvim.json b/wallpaper/templates/nvim.json new file mode 100644 index 0000000..d0bcfa7 --- /dev/null +++ b/wallpaper/templates/nvim.json @@ -0,0 +1,20 @@ +{ + "colors": { + "base00": "{{colors.surface.dark.hex}}", + "base01": "{{colors.surface_container.dark.hex}}", + "base02": "{{colors.surface_container_high.dark.hex}}", + "base03": "{{colors.outline.dark.hex}}", + "base04": "{{colors.on_surface_variant.dark.hex}}", + "base05": "{{colors.on_surface.dark.hex}}", + "base06": "{{colors.inverse_surface.dark.hex}}", + "base07": "{{colors.inverse_on_surface.dark.hex}}", + "base08": "{{colors.primary_fixed.dark.hex}}", + "base09": "{{colors.tertiary.dark.hex}}", + "base0A": "{{colors.secondary.dark.hex}}", + "base0B": "{{colors.primary.dark.hex}}", + "base0C": "{{colors.tertiary.light.hex}}", + "base0D": "{{colors.primary.light.hex}}", + "base0E": "{{colors.secondary.light.hex}}", + "base0F": "{{colors.error.light.hex}}" + } +} diff --git a/wallpaper/templates/qtct-colors.conf b/wallpaper/templates/qtct-colors.conf new file mode 100644 index 0000000..848d620 --- /dev/null +++ b/wallpaper/templates/qtct-colors.conf @@ -0,0 +1,4 @@ +[ColorScheme] +active_colors={{colors.on_background.default.hex}}, {{colors.surface.default.hex}}, {{colors.surface_container_high.default.hex}}, #cacaca, #9f9f9f, #b8b8b8, {{colors.on_background.default.hex}}, #ffffff, {{colors.on_surface.default.hex}}, {{colors.background.default.hex}}, {{colors.background.default.hex}}, {{colors.shadow.default.hex}}, {{colors.primary_container.default.hex}}, {{colors.on_primary_container.default.hex}}, {{colors.secondary.default.hex}}, {{colors.primary.default.hex}}, {{colors.surface.default.hex}}, {{colors.scrim.default.hex}}, {{colors.surface.default.hex}}, {{colors.on_surface.default.hex}}, {{colors.secondary.default.hex}} +disabled_colors={{colors.on_background.default.hex}}, {{colors.surface.default.hex}}, {{colors.surface_container_high.default.hex}}, #cacaca, #9f9f9f, #b8b8b8, {{colors.on_background.default.hex}}, #ffffff, {{colors.on_surface.default.hex}}, {{colors.background.default.hex}}, {{colors.background.default.hex}}, {{colors.shadow.default.hex}}, {{colors.primary_container.default.hex}}, {{colors.on_primary_container.default.hex}}, {{colors.secondary.default.hex}}, {{colors.primary.default.hex}}, {{colors.surface.default.hex}}, {{colors.scrim.default.hex}}, {{colors.surface.default.hex}}, {{colors.on_surface.default.hex}}, {{colors.secondary.default.hex}} +inactive_colors={{colors.on_background.default.hex}}, {{colors.surface.default.hex}}, {{colors.surface_container_high.default.hex}}, #cacaca, #9f9f9f, #b8b8b8, {{colors.on_background.default.hex}}, #ffffff, {{colors.on_surface.default.hex}}, {{colors.background.default.hex}}, {{colors.background.default.hex}}, {{colors.shadow.default.hex}}, {{colors.primary_container.default.hex}}, {{colors.on_primary_container.default.hex}}, {{colors.secondary.default.hex}}, {{colors.primary.default.hex}}, {{colors.surface.default.hex}}, {{colors.scrim.default.hex}}, {{colors.surface.default.hex}}, {{colors.on_surface.default.hex}}, {{colors.secondary.default.hex}} diff --git a/widgets/ColoredIcon.qml b/widgets/ColoredIcon.qml new file mode 100644 index 0000000..480baa1 --- /dev/null +++ b/widgets/ColoredIcon.qml @@ -0,0 +1,30 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Qt5Compat.GraphicalEffects +import Quickshell.Widgets +import ".." + +Item { + id: root + required property var source + property var implicitSize: 0 + property var color: "white" + readonly property real actualSize: Math.min(root.width, root.height) + + implicitWidth: implicitSize + implicitHeight: implicitSize + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: IconImage { + implicitSize: root.actualSize + source: root.source + } + } + + Rectangle { + color: root.color + anchors.fill: parent + } +} diff --git a/widgets/IconButton.qml b/widgets/IconButton.qml new file mode 100644 index 0000000..110a026 --- /dev/null +++ b/widgets/IconButton.qml @@ -0,0 +1,52 @@ +import QtQuick +import Quickshell.Widgets +import Qt5Compat.GraphicalEffects +import ".." + +Item { + id: root + property string source + property var implicitSize: 24 + property var padding: 0 + property var radius: 20 + property var activeRectangle: true + property var color: ShellSettings.settings.colors["inverse_surface"] + property var activeColor: ShellSettings.settings.colors["inverse_primary"] + signal clicked + + implicitWidth: implicitSize + implicitHeight: implicitSize + + Rectangle { + id: iconBackground + color: ShellSettings.settings.colors["primary"] + radius: root.radius + visible: iconButton.containsMouse && root.activeRectangle + anchors.fill: parent + } + + // Figure out a way to color images better + IconImage { + id: iconImage + source: root.source + visible: true + // color: { + // if (!activeRectangle) + // return root.color; + // + // return iconButton.containsMouse ? root.activeColor : root.color; + // } + + anchors { + fill: parent + margins: root.padding + } + } + + MouseArea { + id: iconButton + hoverEnabled: true + anchors.fill: parent + onPressed: root.clicked() + } +} diff --git a/widgets/RoundSlider.qml b/widgets/RoundSlider.qml new file mode 100644 index 0000000..abdfdd9 --- /dev/null +++ b/widgets/RoundSlider.qml @@ -0,0 +1,55 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import Qt5Compat.GraphicalEffects +import ".." + +Slider { + id: slider + implicitHeight: 8 + property var accentColor: ShellSettings.settings.colors["primary"] + + background: Rectangle { + id: sliderContainer + width: slider.availableWidth + height: slider.implicitHeight + color: ShellSettings.settings.colors["inverse_surface"] + radius: 4 + anchors.verticalCenter: parent.verticalCenter + + 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: fill + width: slider.handle.width / 2 + slider.visualPosition * (sliderContainer.width - slider.handle.width) + height: sliderContainer.height + color: Qt.color(slider.accentColor ?? "purple").darker(1.2) + } + } + + handle: Rectangle { + id: handleRect + x: slider.visualPosition * (slider.availableWidth - width) + y: slider.topPadding + slider.availableHeight / 2 - height / 2 + width: 16 + height: 16 + radius: width / 2 + color: slider.pressed ? Qt.color(slider.accentColor ?? "purple").darker(1.5) : slider.accentColor ?? "purple" + } +} diff --git a/widgets/Separator.qml b/widgets/Separator.qml new file mode 100644 index 0000000..46519cd --- /dev/null +++ b/widgets/Separator.qml @@ -0,0 +1,9 @@ +import QtQuick +import ".." + +Rectangle { + color: ShellSettings.settings.colors["primary"] + radius: 5 + width: 7.5 + height: 7.5 +}