feat: bluetooth widget

This commit is contained in:
kossLAN 2025-11-04 19:31:38 -05:00
parent 83d7dc47c4
commit 92316b3ca9
Signed by: kossLAN
SSH key fingerprint: SHA256:bdV0x+wdQHGJ6LgmstH3KV8OpWY+OOFmJcPcB0wQPV8
6 changed files with 300 additions and 5 deletions

View file

@ -4,6 +4,7 @@ import Quickshell
import "power" import "power"
import "volume" import "volume"
import "systray" import "systray"
import "bluetooth"
// import qs.widgets // import qs.widgets
import qs import qs
@ -79,6 +80,12 @@ Variants {
Layout.fillHeight: true Layout.fillHeight: true
} }
BluetoothMenu {
bar: root
Layout.preferredWidth: this.height
Layout.fillHeight: true
}
PowerMenu { PowerMenu {
bar: root bar: root
Layout.fillHeight: true Layout.fillHeight: true

View file

@ -0,0 +1,98 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Widgets
import qs.widgets
import qs
Item {
id: root
required property BluetoothDevice device
RowLayout {
spacing: 2
anchors.fill: parent
IconImage {
source: Quickshell.iconPath(root.device.icon)
Layout.preferredWidth: this.height
Layout.fillHeight: true
Layout.margins: 6
}
ColumnLayout {
spacing: 0
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
Text {
text: root.device.name
color: ShellSettings.colors.active
elide: Text.ElideRight
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
}
Text {
text: root.device.connected ? "Connected" : "Disconnected"
color: ShellSettings.colors.active.darker(1.5)
elide: Text.ElideRight
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
}
}
RowLayout {
spacing: 2
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: 4
StyledMouseArea {
Layout.preferredWidth: this.height
Layout.fillHeight: true
onClicked: {
if (root.device.connected) {
root.device.disconnect();
} else {
root.device.connect();
}
}
IconImage {
source: {
if (root.device.connected) {
return "image://icon/network-disconnect-symbolic";
} else {
return "image://icon/network-connect-symbolic";
}
}
anchors {
fill: parent
margins: 2
}
}
}
StyledMouseArea {
onClicked: root.device.forget()
Layout.preferredWidth: this.height
Layout.fillHeight: true
IconImage {
source: "image://icon/albumfolder-user-trash"
anchors {
fill: parent
margins: 2
}
}
}
}
}
}

View file

@ -0,0 +1,187 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell.Widgets
import Quickshell.Bluetooth
import qs.widgets
import qs.bar
import qs
StyledMouseArea {
id: root
onClicked: showMenu = !showMenu
required property var bar
property bool showMenu: false
IconImage {
anchors.fill: parent
source: {
if (Bluetooth.defaultAdapter.enabled) {
return "image://icon/bluetooth-online";
} else {
return "image://icon/bluetooth-offline";
}
}
}
property PopupItem menu: PopupItem {
id: menu
owner: root
popup: root.bar.popup
show: root.showMenu
onClosed: root.showMenu = false
implicitWidth: 300
implicitHeight: container.implicitHeight + (2 * container.anchors.margins)
property var entryHeight: 35
ColumnLayout {
id: container
spacing: 2
anchors {
fill: parent
margins: 4
}
// Adapter
RowLayout {
spacing: 2
Layout.fillWidth: true
Layout.preferredHeight: menu.entryHeight
IconImage {
Layout.preferredWidth: this.height
Layout.fillHeight: true
// Layout.margins: 5
source: {
if (Bluetooth.defaultAdapter.enabled) {
return "image://icon/bluetooth-online";
} else {
return "image://icon/bluetooth-offline";
}
}
}
ColumnLayout {
spacing: 0
Layout.fillWidth: true
Layout.fillHeight: true
Layout.alignment: Qt.AlignVCenter
Text {
text: `Bluetooth(${Bluetooth.defaultAdapter.adapterId})`
color: ShellSettings.colors.active
elide: Text.ElideRight
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
}
Text {
text: Bluetooth.defaultAdapter.enabled ? "Enabled" : "Disabled"
color: ShellSettings.colors.active.darker(1.5)
elide: Text.ElideRight
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
}
}
RowLayout {
spacing: 2
Layout.fillWidth: true
Layout.fillHeight: true
Layout.margins: 4
StyledMouseArea {
Layout.preferredWidth: this.height
Layout.fillHeight: true
onClicked: {
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled;
}
IconImage {
source: {
if (Bluetooth.defaultAdapter.enabled) {
return "image://icon/bluetooth-offline";
} else {
return "image://icon/bluetooth-online";
}
}
anchors {
fill: parent
margins: 2
}
}
}
StyledMouseArea {
Layout.preferredWidth: this.height
Layout.fillHeight: true
onClicked: {
Bluetooth.defaultAdapter.discovering = !Bluetooth.defaultAdapter.discovering;
}
IconImage {
id: searchIcon
transformOrigin: Item.Center
source: {
if (Bluetooth.defaultAdapter.discovering) {
return "image://icon/reload";
} else {
return "image://icon/cm_search";
}
}
anchors {
fill: parent
margins: 2
}
NumberAnimation on rotation {
from: 0
to: 360
duration: 900
loops: Animation.Infinite
running: Bluetooth.defaultAdapter.discovering
onRunningChanged: {
if (!running)
searchIcon.rotation = 0;
}
}
}
}
}
}
// Devices
StyledListView {
id: appList
spacing: 2
model: Bluetooth.devices
clip: true
Layout.fillWidth: true
Layout.preferredHeight: {
const entryHeight = Math.min(8, Bluetooth.devices.values.length);
return entryHeight * (menu.entryHeight + appList.spacing);
}
delegate: BluetoothCard {
device: modelData
width: ListView.view.width
height: menu.entryHeight
required property BluetoothDevice modelData
}
}
}
}
}

View file

@ -47,6 +47,8 @@ Loader {
} }
StyledSlider { StyledSlider {
implicitHeight: 7
handleHeight: 12
value: root.node.audio.volume ?? 0 value: root.node.audio.volume ?? 0
onValueChanged: { onValueChanged: {

View file

@ -38,9 +38,9 @@ StyledMouseArea {
onClosed: root.showMenu = false onClosed: root.showMenu = false
implicitWidth: 275 implicitWidth: 275
implicitHeight: container.implicitHeight + (2 * 8) implicitHeight: container.implicitHeight + (2 * container.anchors.margins)
property real entryHeight: 40 property real entryHeight: 38
ColumnLayout { ColumnLayout {
id: container id: container
@ -91,7 +91,7 @@ StyledMouseArea {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: { Layout.preferredHeight: {
const entryHeight = Math.min(5, linkTracker.linkGroups.length); const entryHeight = Math.min(6, linkTracker.linkGroups.length);
return entryHeight * (menu.entryHeight + appList.spacing); return entryHeight * (menu.entryHeight + appList.spacing);
} }

View file

@ -10,6 +10,7 @@ Slider {
implicitHeight: 7 implicitHeight: 7
property var accentColor: ShellSettings.colors.active property var accentColor: ShellSettings.colors.active
property real handleHeight: 16
background: Rectangle { background: Rectangle {
id: sliderContainer id: sliderContainer
@ -50,8 +51,8 @@ Slider {
id: handleRect id: handleRect
x: slider.visualPosition * (slider.availableWidth - width) x: slider.visualPosition * (slider.availableWidth - width)
y: slider.topPadding + slider.availableHeight / 2 - height / 2 y: slider.topPadding + slider.availableHeight / 2 - height / 2
width: 16 width: slider.handleHeight
height: 16 height: slider.handleHeight
radius: width / 2 radius: width / 2
color: slider.pressed ? Qt.color(slider.accentColor ?? "purple").darker(1.5) : slider.accentColor ?? "purple" color: slider.pressed ? Qt.color(slider.accentColor ?? "purple").darker(1.5) : slider.accentColor ?? "purple"
} }