diff --git a/Bar.qml b/Bar.qml index 21dbcfc..45a8464 100644 --- a/Bar.qml +++ b/Bar.qml @@ -89,6 +89,10 @@ Scope { right: parent.right rightMargin: 7 } + + Right { + monitor: win.monitor + } } } } diff --git a/Bar/Center.qml b/Bar/Center.qml index 7c3e41a..4fb86b0 100644 --- a/Bar/Center.qml +++ b/Bar/Center.qml @@ -3,6 +3,7 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Hyprland +import Quickshell.Services.Mpris import Quickshell.Services.Notifications import "../Color.js" as Colors import "../Components/" @@ -49,13 +50,18 @@ Container { PropertyChanges { root.boxHeight: 35 root.boxRadius: 15 - root.boxWidth: 110 + root.boxWidth: MprisManager.getPlaying() == true ? 350 : 110 root.visibleTopMargin: 0 } StateChangeScript { script: { - root.previousState = "hovered"; + if (MprisManager.getPlaying()) { + root.previousState = "mpris"; + root.stack.replace(mpris); + } else { + root.previousState = "hovered"; + } } } }, @@ -121,6 +127,17 @@ Container { root.state == "hovered" ? root.state = "expanded" : undefined; } + Connections { + function onTrackChanged() { + if (Hyprland.focusedMonitor == root.monitor) { + musicToastTimer.start(); + root.state = "hovered"; + } + } + + target: MprisManager.defaultPlayer + } + Timer { id: notificationViewTimer @@ -133,6 +150,18 @@ Container { } } + Timer { + id: musicToastTimer + + interval: 2000 + repeat: false + running: false + + onTriggered: { + root.state = ""; + } + } + Component { id: time @@ -143,6 +172,28 @@ Container { } } + Component { + id: mpris + + Mpris { + TapHandler { + onTapped: root.state = "expanded" + } + + TapHandler { + acceptedButtons: Qt.RightButton + + onTapped: MprisManager.skip() + } + + TapHandler { + acceptedButtons: Qt.MiddleButton + + onTapped: MprisManager.prev() + } + } + } + Component { id: fullTime diff --git a/Bar/Center/Mpris.qml b/Bar/Center/Mpris.qml new file mode 100644 index 0000000..51adfe2 --- /dev/null +++ b/Bar/Center/Mpris.qml @@ -0,0 +1,62 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.Mpris +import "../../Color.js" as Colors +import "../../Components/" +import "../../Services/" + +Item { + id: root + + property MprisPlayer player: MprisManager.defaultPlayer + + Rectangle { + color: Colors.red + radius: 5 + + anchors { + bottomMargin: 15 + fill: parent + leftMargin: 10 + rightMargin: parent.width - ((parent.width * MprisManager.pos) - 10) + topMargin: 15 + } + } + + RowLayout { + anchors { + bottomMargin: 2 + fill: parent + leftMargin: 7 + rightMargin: 7 + topMargin: 2 + } + + Item { + Layout.fillHeight: true + Layout.preferredWidth: height + + Image { + anchors.fill: parent + fillMode: Image.PreserveAspectFit + source: MprisManager.defaultPlayer.trackArtUrl + } + } + + Item { + Layout.fillHeight: true + Layout.fillWidth: true + + StyledText { + elide: Qt.ElideRight + horizontalAlignment: Qt.AlignHCenter + text: MprisManager.defaultPlayer.trackArtist + " - " + MprisManager.defaultPlayer.trackTitle + verticalAlignment: Qt.AlignVCenter + + anchors { + fill: parent + } + } + } + } +} diff --git a/Bar/Left/WorkspaceContainer.qml b/Bar/Left/WorkspaceContainer.qml index 5f5f0ef..65ccc38 100644 --- a/Bar/Left/WorkspaceContainer.qml +++ b/Bar/Left/WorkspaceContainer.qml @@ -19,6 +19,8 @@ Item { required property double selectorWidth Item { + id: newWorkspaceButton + implicitWidth: 20 opacity: root.extraHoveredWidth > 0 ? 1 : 0 @@ -45,7 +47,7 @@ Item { Rectangle { anchors.fill: parent - color: Colors.red + color: Colors.green radius: 4 StyledText { diff --git a/Bar/Left/WorkspaceSelector.qml b/Bar/Left/WorkspaceSelector.qml index abc556f..3111d51 100644 --- a/Bar/Left/WorkspaceSelector.qml +++ b/Bar/Left/WorkspaceSelector.qml @@ -79,8 +79,7 @@ Item { fill: parent } - StyledText { - anchors.centerIn: parent + CenteredText { font.pointSize: 10.5 text: root.workspace.id } diff --git a/Bar/Right.qml b/Bar/Right.qml new file mode 100644 index 0000000..a4608d4 --- /dev/null +++ b/Bar/Right.qml @@ -0,0 +1,21 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland +import Quickshell.Services.Notifications +import "../Color.js" as Colors +import "../Components/" +import "../Services/" +import "./Center" +import "./Right/" + +Row { + id: root + + required property HyprlandMonitor monitor + + Bluetooth { + exclusiveMonitor: root.monitor + } +} diff --git a/Bar/Right/Bluetooth.qml b/Bar/Right/Bluetooth.qml new file mode 100644 index 0000000..875949c --- /dev/null +++ b/Bar/Right/Bluetooth.qml @@ -0,0 +1,72 @@ +pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Quickshell.Bluetooth +import "../../Color.js" as Colors +import "../../Components/" +import "../../Services/" +import "Bluetooth" + +Container { + id: root + + animOffset: 200 + boxColor: Colors.peach + boxHeight: 25 + boxWidth: 30 + defaultItem: icon + exclusiveToScreen: true + forceHidden: BluetoothManager.getConnected() + state: "" + + states: [ + State { + name: "" + + PropertyChanges { + root.boxHeight: 25 + root.boxWidth: 30 + } + + StateChangeScript { + script: { + root.stack.replace(icon); + } + } + }, + State { + name: "expanded" + + PropertyChanges { + root.boxHeight: 250 + root.boxRadius: 20 + root.boxWidth: 350 + } + + StateChangeScript { + script: { + root.stack.replace(expanded); + } + } + } + ] + + hover.onHoveredChanged: hover.hovered == false ? state = "" : undefined + tap.onTapped: state = "expanded" + + Component { + id: icon + + Item { + CenteredText { + text: "󰂱" + } + } + } + + Component { + id: expanded + + Expanded {} + } +} diff --git a/Bar/Right/Bluetooth/Expanded.qml b/Bar/Right/Bluetooth/Expanded.qml new file mode 100644 index 0000000..71b4f61 --- /dev/null +++ b/Bar/Right/Bluetooth/Expanded.qml @@ -0,0 +1,384 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Bluetooth +import "../../../Color.js" as Colors +import "../../../Components/" +import "../../../Services/" + +Item { + id: root + + property double topBarHeight: 35 + + Rectangle { + color: Colors.surface0 + radius: 17 + + anchors { + bottomMargin: 5 + fill: parent + leftMargin: 5 + rightMargin: 5 + topMargin: 5 + } + + Rectangle { + id: topBar + + property double topBarHeight: root.topBarHeight + + color: Colors.peach + implicitHeight: topBarHeight + radius: 9 + + anchors { + left: parent.left + leftMargin: 5 + right: parent.right + rightMargin: 5 + top: parent.top + topMargin: 5 + } + + RowLayout { + id: topBarRow + + property double topBarRowPadding: 4 + + spacing: 3 + + anchors { + left: parent.left + leftMargin: topBarRowPadding + right: parent.right + rightMargin: topBarRowPadding + top: parent.top + topMargin: topBarRowPadding + } + + Rectangle { + Layout.fillWidth: true + // color: Qt.lighter(Colors.peach, 1.2) + color: Colors.surface1 + implicitHeight: topBar.topBarHeight - (topBarRow.topBarRowPadding * 2) + radius: 6 + + StyledText { + anchors.verticalCenter: parent.verticalCenter + color: Colors.text + horizontalAlignment: Qt.AlignLeft + text: " " + BluetoothManager.defaultAdapterName + } + } + + Rectangle { + id: scanButton + + color: Qt.lighter(Colors.peach, 1.15) + implicitHeight: topBar.topBarHeight - (topBarRow.topBarRowPadding * 2) + implicitWidth: topBar.topBarHeight - (topBarRow.topBarRowPadding * 2) + radius: 6 + + Behavior on color { + ColorAnimation { + duration: 100 + } + } + states: [ + State { + name: "hovered" + + PropertyChanges { + scanButton.color: Colors.green + } + } + ] + + border { + color: Colors.surface0 + width: 2 + } + + HoverHandler { + onHoveredChanged: hovered == true ? scanButton.state = "hovered" : scanButton.state = "" + } + + TapHandler { + onTapped: { + BluetoothManager.toggleDiscover(); + } + } + + CenteredText { + id: scanButtonText + + text: "" + + NumberAnimation { + id: animateRotation + + duration: 1000 + from: 0 + loops: Animation.Infinite + properties: "rotation" + running: BluetoothManager.defaultAdapter.discovering + target: scanButtonText + to: 360 + + onStopped: { + scanButton.rotation = 0; + } + } + } + } + } + } + + ListView { + boundsBehavior: Flickable.StopAtBounds + clip: true + model: BluetoothManager.getDevicesList() + spacing: 5 + + delegate: Item { + id: deviceRoot + + required property real index + required property BluetoothDevice modelData + + height: 55 + width: ListView.view.width + + Rectangle { + id: containerBox + + anchors.fill: parent + clip: true + color: Colors.peach + radius: 7 + + Rectangle { + anchors.centerIn: parent + color: Colors.green + implicitHeight: parent.height - 10 + implicitWidth: parent.width * (deviceRoot.modelData.batteryAvailable == true + ? deviceRoot.modelData.battery : 0) - 10 + opacity: 0.7 + radius: 7 + visible: deviceRoot.modelData.batteryAvailable == true + } + + RowLayout { + anchors { + bottomMargin: 4 + fill: parent + leftMargin: 4 + rightMargin: 4 + topMargin: 4 + } + + Rectangle { + Layout.fillHeight: true + Layout.preferredWidth: height + color: deviceRoot.modelData.connected ? Colors.green : Qt.lighter(Colors.peach, 1.25) + radius: 5 + + CenteredText { + color: Colors.surface0 + font.pointSize: 30 + text: BluetoothManager.getIcon(deviceRoot.modelData.icon) + } + } + + ColumnLayout { + Layout.fillWidth: true + + Rectangle { + Layout.fillHeight: true + Layout.fillWidth: true + color: Qt.lighter(Colors.peach, 1.15) + radius: 4 + + StyledText { + anchors.fill: parent + anchors.verticalCenter: parent.verticalCenter + elide: Qt.ElideRight + text: deviceRoot.modelData.name + } + } + + Rectangle { + Layout.fillHeight: true + Layout.fillWidth: true + color: "transparent" + + StyledText { + anchors.fill: parent + anchors.verticalCenter: parent.verticalCenter + color: Colors.surface2 + font.weight: 300 + text: deviceRoot.modelData.address + } + } + } + + Item { + Layout.fillHeight: true + Layout.preferredWidth: height + + RowLayout { + anchors.fill: parent + + Rectangle { + id: itemEntry + + Layout.fillHeight: true + Layout.fillWidth: true + color: Colors.red + radius: 3 + + Loader { + id: deleteButtonLoader + + anchors.fill: parent + sourceComponent: forgetButton + + states: [ + State { + name: "" + when: deviceRoot.modelData.connected == false + + PropertyChanges { + deleteButtonLoader.sourceComponent: forgetButton + } + }, + State { + name: "connected" + when: deviceRoot.modelData.connected == true + + PropertyChanges { + deleteButtonLoader.sourceComponent: disconnectButton + } + } + ] + } + + Component { + id: disconnectButton + + Item { + anchors.fill: parent + + TapHandler { + onTapped: deviceRoot.modelData.disconnect() + } + + CenteredText { + text: "" + } + } + } + + Component { + id: forgetButton + + Item { + anchors.fill: parent + + TapHandler { + onTapped: deviceRoot.modelData.forget() + } + + CenteredText { + text: "󰩹" + } + } + } + } + + Rectangle { + id: greenButton + + property Component loadedItem: connectButton + + Layout.fillHeight: true + Layout.fillWidth: true + color: Colors.green + radius: 3 + + states: [ + State { + name: "" + when: deviceRoot.modelData.paired == true + + PropertyChanges { + greenButton.color: Colors.green + greenButton.loadedItem: connectButton + } + }, + State { + name: "unpaired" + when: deviceRoot.modelData.paired == false + + PropertyChanges { + greenButton.color: Colors.blue + greenButton.loadedItem: pairButton + } + } + ] + + Loader { + anchors.fill: parent + sourceComponent: greenButton.loadedItem + } + + Component { + id: connectButton + + Item { + anchors.fill: parent + + CenteredText { + text: "󱘖" + } + + TapHandler { + onTapped: deviceRoot.modelData.connect() + } + } + } + + Component { + id: pairButton + + Item { + anchors.fill: parent + + CenteredText { + text: "" + } + + TapHandler { + onTapped: { + deviceRoot.modelData.pair(); + } + } + } + } + } + } + } + } + } + } + + anchors { + bottomMargin: 5 + fill: parent + leftMargin: 5 + rightMargin: 5 + topMargin: 10 + root.topBarHeight + } + } + } +} diff --git a/Components/Container.qml b/Components/Container.qml index d3fd96c..d906226 100644 --- a/Components/Container.qml +++ b/Components/Container.qml @@ -7,16 +7,17 @@ import Quickshell.Hyprland import "../Color.js" as Colors Item { - id: containerRoot + id: root + property double animOffset: 0 property color boxColor: Colors.surface0 property double boxHeight: 28 property double boxRadius: 9 property double boxWidth: 100 readonly property double calculatedTopMargin: { - if (containerRoot.exclusiveToScreen) { - if (Hyprland.focusedMonitor != containerRoot.exclusiveMonitor) { - return -4 - containerRoot.boxHeight - 5; + if (root.exclusiveToScreen) { + if (Hyprland.focusedMonitor != root.exclusiveMonitor || forceHidden == true) { + return -4 - root.boxHeight - 5; } else { return visibleTopMargin; } @@ -27,6 +28,7 @@ Item { property Component defaultItem required property HyprlandMonitor exclusiveMonitor property bool exclusiveToScreen: false + property bool forceHidden: false property alias hover: hoverHandler property alias rect: container property alias stack: containerContent @@ -66,7 +68,7 @@ Item { NumberAnimation { id: defaultCurve - duration: 200 + duration: 200 + root.animOffset easing: Easing.InOutBack } @@ -93,8 +95,8 @@ Item { anchors.fill: parent clip: true - color: containerRoot.boxColor - radius: containerRoot.boxRadius + color: root.boxColor + radius: root.boxRadius StackView { id: containerContent @@ -125,7 +127,7 @@ Item { } } - Component.onCompleted: containerContent.push(containerRoot.defaultItem) + Component.onCompleted: containerContent.push(root.defaultItem) onCurrentItemChanged: { if (currentItem) { // Dynamically center the incoming child to the StackView diff --git a/Services/BluetoothManager.qml b/Services/BluetoothManager.qml new file mode 100644 index 0000000..86f3611 --- /dev/null +++ b/Services/BluetoothManager.qml @@ -0,0 +1,69 @@ +pragma Singleton +import QtQuick +import Quickshell +import Quickshell.Bluetooth + +Singleton { + id: root + + property BluetoothAdapter defaultAdapter: Bluetooth.defaultAdapter + property string defaultAdapterName: Bluetooth.defaultAdapter.adapterId + + function getConnected() { + let deviceList = getDevicesList(); + for (let device in deviceList) { + let currentDevice = deviceList[device]; + if (currentDevice.connected == true) { + return false; + } + } + return true; + } + + function getDevicesList() { + return root.defaultAdapter.devices.values; + } + + function getIcon(name) { + const icons = { + "audio-card": "󰓃", + "audio-input-microphone": "", + "audio-headphones": "󰋋", + "audio-headset": "󰋋", + "battery": "󰂀", + "camera-photo": "󰻛", + "computer": "", + "input-keyboard": "󰌌", + "input-mouse": "󰍽", + "input-gaming": "󰊴", + "phone": "󰏲" + }; + + return icons[name] || "󰾰"; + } + + function pairTrustConnect(device) { + device.pair(); + device.trusted = true; + device.connect(); + } + + function toggleDiscover() { + if (defaultAdapter.discovering == true) { + defaultAdapter.discovering = false; + } else { + defaultAdapter.discovering = true; + discoveringTimeout.running = true; + } + } + + Timer { + id: discoveringTimeout + + interval: 15000 + repeat: false + running: false + + onTriggered: defaultAdapter.discovering = false + } +} diff --git a/Services/MprisManager.qml b/Services/MprisManager.qml new file mode 100644 index 0000000..95cf458 --- /dev/null +++ b/Services/MprisManager.qml @@ -0,0 +1,27 @@ +pragma Singleton +import QtQuick +import Quickshell +import Quickshell.Services.Mpris + +Singleton { + id: root + + property MprisPlayer defaultPlayer: Mpris.players.values[0] + property real pos: defaultPlayer.position / defaultPlayer.length + + function getPlaying() { + if (!defaultPlayer) { + return false; + } else { + return defaultPlayer.playbackState == MprisPlaybackState.Playing; + } + } + + function prev() { + defaultPlayer.previous(); + } + + function skip() { + defaultPlayer.next(); + } +} diff --git a/Services/qmldir b/Services/qmldir index 586fa0f..b6984ea 100644 --- a/Services/qmldir +++ b/Services/qmldir @@ -2,3 +2,5 @@ module Services singleton Time 1.0 Time.qml singleton NotificationManager 1.0 NotificationManager.qml singleton WorkspaceManager 1.0 WorkspaceManager.qml +singleton BluetoothManager 1.0 BluetoothManager.qml +singleton MprisManager 1.0 MprisManager.qml