bluetooth menu

This commit is contained in:
end-4
2025-08-30 07:51:18 +02:00
parent 59abffb1c1
commit a952ea02dc
10 changed files with 276 additions and 39 deletions
@@ -0,0 +1,23 @@
pragma Singleton
// From https://github.com/caelestia-dots/shell (GPLv3)
import Quickshell
Singleton {
id: root
function getBluetoothDeviceMaterialSymbol(systemIconName: string): string {
if (systemIconName.includes("headset") || systemIconName.includes("headphones"))
return "headphones";
if (systemIconName.includes("audio"))
return "speaker";
if (systemIconName.includes("phone"))
return "smartphone";
if (systemIconName.includes("mouse"))
return "mouse";
if (systemIconName.includes("keyboard"))
return "keyboard";
return "bluetooth";
}
}
@@ -20,6 +20,7 @@ RippleButton {
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
colBackgroundHover: Appearance.colors.colLayer3Hover
colRipple: Appearance.colors.colLayer3Active
property alias colText: buttonTextWidget.color
contentItem: StyledText {
id: buttonTextWidget
@@ -0,0 +1,25 @@
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick
RippleButton {
id: root
property bool active: false
horizontalPadding: Appearance.rounding.large
verticalPadding: 12
clip: true
pointingHandCursor: !active
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
implicitHeight: contentItem.implicitHeight + verticalPadding * 2
Behavior on implicitHeight {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
colBackgroundHover: active ? colBackground : Appearance.colors.colLayer3Hover
colRipple: Appearance.colors.colLayer3Active
buttonRadius: 0
}
@@ -12,6 +12,7 @@ Button {
id: root
property bool toggled
property string buttonText
property bool pointingHandCursor: true
property real buttonRadius: Appearance?.rounding?.small ?? 4
property real buttonRadiusPressed: buttonRadius
property real buttonEffectiveRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius
@@ -58,7 +59,7 @@ Button {
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
cursorShape: root.pointingHandCursor ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: (event) => {
if(event.button === Qt.RightButton) {
@@ -1,18 +1,15 @@
import qs
import qs.services
import qs.services.network
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "./quickToggles/"
import "./wifiNetworks/"
import "./bluetoothDevices/"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import Quickshell
import Quickshell.Wayland
import Quickshell.Bluetooth
import Quickshell.Hyprland
Item {
@@ -21,12 +18,14 @@ Item {
property int sidebarPadding: 12
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
property bool showWifiDialog: false
property bool showBluetoothDialog: false
Connections {
target: GlobalStates
function onSidebarRightOpenChanged() {
if (!GlobalStates.sidebarRightOpen) {
root.showWifiDialog = false;
root.showBluetoothDialog = false;
}
}
}
@@ -129,7 +128,13 @@ Item {
root.showWifiDialog = true;
}
}
BluetoothToggle {}
BluetoothToggle {
altAction: () => {
Bluetooth.defaultAdapter.enabled = true;
Bluetooth.defaultAdapter.discovering = true;
root.showBluetoothDialog = true;
}
}
NightLight {}
GameMode {}
IdleInhibitor {}
@@ -138,7 +143,6 @@ Item {
}
CenterWidgetGroup {
focus: sidebarRoot.visible
Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true
Layout.fillWidth: true
@@ -176,4 +180,28 @@ Item {
}
}
}
onShowBluetoothDialogChanged: if (showBluetoothDialog) bluetoothDialogLoader.active = true;
Loader {
id: bluetoothDialogLoader
anchors.fill: parent
active: root.showBluetoothDialog || item.visible
onActiveChanged: {
if (active) {
item.show = true;
item.forceActiveFocus();
}
}
sourceComponent: BluetoothDialog {
onDismiss: {
show = false
root.showBluetoothDialog = false
}
onVisibleChanged: {
if (!visible && !root.showBluetoothDialog) bluetoothDialogLoader.active = false;
}
}
}
}
@@ -0,0 +1,112 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
DialogListItem {
id: root
required property var device
property bool expanded: false
pointingHandCursor: !expanded
onClicked: expanded = !expanded
component ActionButton: DialogButton {
colBackground: Appearance.colors.colPrimary
colBackgroundHover: Appearance.colors.colPrimaryHover
colRipple: Appearance.colors.colPrimaryActive
colText: Appearance.colors.colOnPrimary
}
contentItem: ColumnLayout {
anchors {
fill: parent
topMargin: root.verticalPadding
leftMargin: root.horizontalPadding
rightMargin: root.horizontalPadding
}
spacing: 0
RowLayout {
// Name
spacing: 10
MaterialSymbol {
iconSize: Appearance.font.pixelSize.larger
text: Icons.getBluetoothDeviceMaterialSymbol(root.device?.icon || "")
color: Appearance.colors.colOnSurfaceVariant
}
ColumnLayout {
spacing: 2
Layout.fillWidth: true
StyledText {
Layout.fillWidth: true
color: Appearance.colors.colOnSurfaceVariant
elide: Text.ElideRight
text: root.device?.name
}
StyledText {
visible: root.device?.connected || root.device?.paired
Layout.fillWidth: true
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.colors.colSubtext
elide: Text.ElideRight
text: {
if (!root.device?.paired) return "";
let statusText = root.device?.connected ? Translation.tr("Connected") : Translation.tr("Paired");
if (!root.device?.batteryAvailable) return statusText;
statusText += ` ${root.device?.battery * 100}%`;
return statusText;
}
}
}
MaterialSymbol {
text: "keyboard_arrow_down"
iconSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colOnLayer3
rotation: root.expanded ? 180 : 0
Behavior on rotation {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
}
RowLayout {
visible: root.expanded
Layout.topMargin: 8
Item {
Layout.fillWidth: true
}
ActionButton {
buttonText: root.device?.connected ? Translation.tr("Disconnect") : Translation.tr("Connect")
onClicked: {
if (root.device?.connected) {
root.device.disconnect();
} else {
root.device.connect();
}
}
}
ActionButton {
visible: root.device?.paired
colBackground: Appearance.colors.colError
colBackgroundHover: Appearance.colors.colErrorHover
colRipple: Appearance.colors.colErrorActive
colText: Appearance.colors.colOnError
buttonText: Translation.tr("Forget")
onClicked: {
root.device?.forget();
}
}
}
Item {
Layout.fillHeight: true
}
}
}
@@ -0,0 +1,67 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import Quickshell.Bluetooth
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
WindowDialog {
id: root
WindowDialogTitle {
text: Translation.tr("Bluetooth devices")
}
// TODO: add indeterminate progress bar when scanning
WindowDialogSeparator {}
StyledListView {
Layout.fillHeight: true
Layout.fillWidth: true
Layout.topMargin: -15
Layout.bottomMargin: -16
Layout.leftMargin: -Appearance.rounding.large
Layout.rightMargin: -Appearance.rounding.large
clip: true
spacing: 0
animateAppearance: false
model: ScriptModel {
values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired))
}
delegate: BluetoothDeviceItem {
required property BluetoothDevice modelData
device: modelData
anchors {
left: parent?.left
right: parent?.right
}
}
}
WindowDialogSeparator {}
WindowDialogButtonRow {
DialogButton {
buttonText: Translation.tr("Details")
onClicked: {
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`]);
GlobalStates.sidebarRightOpen = false;
}
}
Item {
Layout.fillWidth: true
}
DialogButton {
buttonText: Translation.tr("Done")
onClicked: root.dismiss()
}
}
}
@@ -3,17 +3,9 @@ import qs.services
import qs.services.network
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "./quickToggles/"
import "./wifiNetworks/"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell.Io
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
WindowDialog {
id: root
@@ -44,7 +36,6 @@ WindowDialog {
return b.strength - a.strength;
})
}
// model: Network.wifiNetworks
delegate: WifiNetworkItem {
required property WifiAccessPoint modelData
wifiNetwork: modelData
@@ -1,37 +1,21 @@
import qs
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.services
import qs.services.network
import QtQuick
import QtQuick.Layouts
import Quickshell
RippleButton {
DialogListItem {
id: root
required property WifiAccessPoint wifiNetwork
horizontalPadding: Appearance.rounding.large
verticalPadding: 12
implicitWidth: mainLayout.implicitWidth + horizontalPadding * 2
implicitHeight: mainLayout.implicitHeight + verticalPadding * 2
Behavior on implicitHeight {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
clip: true
buttonRadius: 0
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
colBackgroundHover: (wifiNetwork?.askingPassword || wifiNetwork?.active) ? colBackground : Appearance.colors.colLayer3Hover
colRipple: Appearance.colors.colLayer3Active
active: (wifiNetwork?.askingPassword || wifiNetwork?.active)
onClicked: {
Network.connectToWifiNetwork(wifiNetwork);
}
contentItem: ColumnLayout {
id: mainLayout
anchors {
fill: parent
topMargin: root.verticalPadding
@@ -52,8 +36,9 @@ RippleButton {
}
StyledText {
Layout.fillWidth: true
text: root.wifiNetwork?.ssid ?? Translation.tr("Unknown")
color: Appearance.colors.colOnSurfaceVariant
elide: Text.ElideRight
text: root.wifiNetwork?.ssid ?? Translation.tr("Unknown")
}
MaterialSymbol {
visible: (root.wifiNetwork?.isSecure || root.wifiNetwork?.active) ?? false
@@ -65,8 +50,8 @@ RippleButton {
ColumnLayout { // Password
id: passwordPrompt
visible: root.wifiNetwork?.askingPassword ?? false
Layout.topMargin: 8
visible: root.wifiNetwork?.askingPassword ?? false
MaterialTextField {
id: passwordField
@@ -107,8 +92,8 @@ RippleButton {
ColumnLayout { // Public wifi login page
id: publicWifiPortal
visible: root.wifiNetwork?.active && (root.wifiNetwork?.security ?? "").trim().length === 0
Layout.topMargin: 8
visible: root.wifiNetwork?.active && (root.wifiNetwork?.security ?? "").trim().length === 0
RowLayout {
DialogButton {
@@ -124,5 +109,9 @@ RippleButton {
}
}
}
Item {
Layout.fillHeight: true
}
}
}
@@ -12,7 +12,7 @@ import QtQuick
Singleton {
id: root
readonly property bool enabled: Bluetooth.defaultAdapter?.enabled
readonly property bool enabled: Bluetooth.defaultAdapter?.enabled ?? false
readonly property BluetoothDevice firstActiveDevice: Bluetooth.defaultAdapter?.devices.values.find(device => device.connected) ?? null
readonly property int activeDeviceCount: Bluetooth.defaultAdapter?.devices.values.filter(device => device.connected).length ?? 0
readonly property bool connected: Bluetooth.devices.values.some(d => d.connected)