working mpris and bluetooth menu

This commit is contained in:
2026-06-26 17:34:25 +01:00
parent 2b49211315
commit 07ec781b68
12 changed files with 708 additions and 13 deletions

View File

@@ -89,6 +89,10 @@ Scope {
right: parent.right right: parent.right
rightMargin: 7 rightMargin: 7
} }
Right {
monitor: win.monitor
}
} }
} }
} }

View File

@@ -3,6 +3,7 @@ import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Services.Mpris
import Quickshell.Services.Notifications import Quickshell.Services.Notifications
import "../Color.js" as Colors import "../Color.js" as Colors
import "../Components/" import "../Components/"
@@ -49,13 +50,18 @@ Container {
PropertyChanges { PropertyChanges {
root.boxHeight: 35 root.boxHeight: 35
root.boxRadius: 15 root.boxRadius: 15
root.boxWidth: 110 root.boxWidth: MprisManager.getPlaying() == true ? 350 : 110
root.visibleTopMargin: 0 root.visibleTopMargin: 0
} }
StateChangeScript { StateChangeScript {
script: { 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; root.state == "hovered" ? root.state = "expanded" : undefined;
} }
Connections {
function onTrackChanged() {
if (Hyprland.focusedMonitor == root.monitor) {
musicToastTimer.start();
root.state = "hovered";
}
}
target: MprisManager.defaultPlayer
}
Timer { Timer {
id: notificationViewTimer id: notificationViewTimer
@@ -133,6 +150,18 @@ Container {
} }
} }
Timer {
id: musicToastTimer
interval: 2000
repeat: false
running: false
onTriggered: {
root.state = "";
}
}
Component { Component {
id: time 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 { Component {
id: fullTime id: fullTime

62
Bar/Center/Mpris.qml Normal file
View File

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

View File

@@ -19,6 +19,8 @@ Item {
required property double selectorWidth required property double selectorWidth
Item { Item {
id: newWorkspaceButton
implicitWidth: 20 implicitWidth: 20
opacity: root.extraHoveredWidth > 0 ? 1 : 0 opacity: root.extraHoveredWidth > 0 ? 1 : 0
@@ -45,7 +47,7 @@ Item {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: Colors.red color: Colors.green
radius: 4 radius: 4
StyledText { StyledText {

View File

@@ -79,8 +79,7 @@ Item {
fill: parent fill: parent
} }
StyledText { CenteredText {
anchors.centerIn: parent
font.pointSize: 10.5 font.pointSize: 10.5
text: root.workspace.id text: root.workspace.id
} }

21
Bar/Right.qml Normal file
View File

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

72
Bar/Right/Bluetooth.qml Normal file
View File

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

View File

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

View File

@@ -7,16 +7,17 @@ import Quickshell.Hyprland
import "../Color.js" as Colors import "../Color.js" as Colors
Item { Item {
id: containerRoot id: root
property double animOffset: 0
property color boxColor: Colors.surface0 property color boxColor: Colors.surface0
property double boxHeight: 28 property double boxHeight: 28
property double boxRadius: 9 property double boxRadius: 9
property double boxWidth: 100 property double boxWidth: 100
readonly property double calculatedTopMargin: { readonly property double calculatedTopMargin: {
if (containerRoot.exclusiveToScreen) { if (root.exclusiveToScreen) {
if (Hyprland.focusedMonitor != containerRoot.exclusiveMonitor) { if (Hyprland.focusedMonitor != root.exclusiveMonitor || forceHidden == true) {
return -4 - containerRoot.boxHeight - 5; return -4 - root.boxHeight - 5;
} else { } else {
return visibleTopMargin; return visibleTopMargin;
} }
@@ -27,6 +28,7 @@ Item {
property Component defaultItem property Component defaultItem
required property HyprlandMonitor exclusiveMonitor required property HyprlandMonitor exclusiveMonitor
property bool exclusiveToScreen: false property bool exclusiveToScreen: false
property bool forceHidden: false
property alias hover: hoverHandler property alias hover: hoverHandler
property alias rect: container property alias rect: container
property alias stack: containerContent property alias stack: containerContent
@@ -66,7 +68,7 @@ Item {
NumberAnimation { NumberAnimation {
id: defaultCurve id: defaultCurve
duration: 200 duration: 200 + root.animOffset
easing: Easing.InOutBack easing: Easing.InOutBack
} }
@@ -93,8 +95,8 @@ Item {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
color: containerRoot.boxColor color: root.boxColor
radius: containerRoot.boxRadius radius: root.boxRadius
StackView { StackView {
id: containerContent id: containerContent
@@ -125,7 +127,7 @@ Item {
} }
} }
Component.onCompleted: containerContent.push(containerRoot.defaultItem) Component.onCompleted: containerContent.push(root.defaultItem)
onCurrentItemChanged: { onCurrentItemChanged: {
if (currentItem) { if (currentItem) {
// Dynamically center the incoming child to the StackView // Dynamically center the incoming child to the StackView

View File

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

27
Services/MprisManager.qml Normal file
View File

@@ -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();
}
}

View File

@@ -2,3 +2,5 @@ module Services
singleton Time 1.0 Time.qml singleton Time 1.0 Time.qml
singleton NotificationManager 1.0 NotificationManager.qml singleton NotificationManager 1.0 NotificationManager.qml
singleton WorkspaceManager 1.0 WorkspaceManager.qml singleton WorkspaceManager 1.0 WorkspaceManager.qml
singleton BluetoothManager 1.0 BluetoothManager.qml
singleton MprisManager 1.0 MprisManager.qml