Add bluetooth panel (wip), add clock / clock+date setting
This commit is contained in:
parent
56ad08816e
commit
2fd20c69f9
9 changed files with 911 additions and 9 deletions
|
|
@ -96,6 +96,7 @@ Singleton {
|
||||||
property bool useFahrenheit: false
|
property bool useFahrenheit: false
|
||||||
property bool reverseDayMonth: false
|
property bool reverseDayMonth: false
|
||||||
property bool use12HourClock: false
|
property bool use12HourClock: false
|
||||||
|
property bool showDateWithClock: false
|
||||||
}
|
}
|
||||||
|
|
||||||
// screen recorder
|
// screen recorder
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,20 @@ Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property var date: new Date()
|
property var date: new Date()
|
||||||
property string time: Settings.data.location.use12HourClock ? Qt.formatDateTime(date, "h:mm AP") : Qt.formatDateTime(
|
property string time: {
|
||||||
date, "HH:mm")
|
let timeFormat = Settings.data.location.use12HourClock ? "h:mm AP" : "HH:mm"
|
||||||
|
let timeString = Qt.formatDateTime(date, timeFormat)
|
||||||
|
|
||||||
|
if (Settings.data.location.showDateWithClock) {
|
||||||
|
let dayName = date.toLocaleDateString(Qt.locale(), "ddd")
|
||||||
|
dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1)
|
||||||
|
let day = date.getDate()
|
||||||
|
let month = date.toLocaleDateString(Qt.locale(), "MMM")
|
||||||
|
return timeString + " - " + dayName + ", " + day + " " + month
|
||||||
|
}
|
||||||
|
|
||||||
|
return timeString
|
||||||
|
}
|
||||||
readonly property string dateString: {
|
readonly property string dateString: {
|
||||||
let now = date
|
let now = date
|
||||||
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
|
let dayName = now.toLocaleDateString(Qt.locale(), "ddd")
|
||||||
|
|
|
||||||
|
|
@ -109,9 +109,9 @@ Variants {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bluetooth {
|
Bluetooth {
|
||||||
// anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
// }
|
}
|
||||||
Battery {
|
Battery {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
|
||||||
58
Modules/Bar/Bluetooth.qml
Normal file
58
Modules/Bar/Bluetooth.qml
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property bool bluetoothEnabled: Settings.data.network.bluetoothEnabled
|
||||||
|
sizeMultiplier: 0.8
|
||||||
|
showBorder: false
|
||||||
|
visible: bluetoothEnabled
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log("[Bluetooth] Component loaded, bluetoothEnabled:", bluetoothEnabled)
|
||||||
|
console.log("[Bluetooth] BluetoothService available:", typeof BluetoothService !== 'undefined')
|
||||||
|
if (typeof BluetoothService !== 'undefined') {
|
||||||
|
console.log("[Bluetooth] Connected devices:", BluetoothService.connectedDevices.length)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
icon: {
|
||||||
|
// Show different icons based on connection status
|
||||||
|
if (BluetoothService.connectedDevices.length > 0) {
|
||||||
|
return "bluetooth_connected"
|
||||||
|
} else if (BluetoothService.isDiscovering) {
|
||||||
|
return "bluetooth_searching"
|
||||||
|
} else {
|
||||||
|
return "bluetooth"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tooltipText: "Bluetooth Devices"
|
||||||
|
onClicked: {
|
||||||
|
if (!bluetoothMenuLoader.active) {
|
||||||
|
bluetoothMenuLoader.isLoaded = true
|
||||||
|
}
|
||||||
|
if (bluetoothMenuLoader.item) {
|
||||||
|
if (bluetoothMenuLoader.item.visible) {
|
||||||
|
// Panel is visible, hide it with animation
|
||||||
|
if (bluetoothMenuLoader.item.hide) {
|
||||||
|
bluetoothMenuLoader.item.hide()
|
||||||
|
} else {
|
||||||
|
bluetoothMenuLoader.item.visible = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Panel is hidden, show it
|
||||||
|
bluetoothMenuLoader.item.visible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothMenu {
|
||||||
|
id: bluetoothMenuLoader
|
||||||
|
}
|
||||||
|
}
|
||||||
397
Modules/Bar/BluetoothMenu.qml
Normal file
397
Modules/Bar/BluetoothMenu.qml
Normal file
|
|
@ -0,0 +1,397 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import qs.Commons
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
// Loader for Bluetooth menu
|
||||||
|
NLoader {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
content: Component {
|
||||||
|
NPanel {
|
||||||
|
id: bluetoothPanel
|
||||||
|
|
||||||
|
function hide() {
|
||||||
|
bluetoothMenuRect.scaleValue = 0.8
|
||||||
|
bluetoothMenuRect.opacityValue = 0.0
|
||||||
|
hideTimer.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to NPanel's dismissed signal to handle external close events
|
||||||
|
Connections {
|
||||||
|
target: bluetoothPanel
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
function onDismissed() {
|
||||||
|
// Start hide animation
|
||||||
|
bluetoothMenuRect.scaleValue = 0.8
|
||||||
|
bluetoothMenuRect.opacityValue = 0.0
|
||||||
|
// Hide after animation completes
|
||||||
|
hideTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also handle visibility changes from external sources
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible && Settings.data.network.bluetoothEnabled) {
|
||||||
|
// Always refresh devices when menu opens to get fresh device objects
|
||||||
|
BluetoothService.refreshDevices()
|
||||||
|
} else if (bluetoothMenuRect.opacityValue > 0) {
|
||||||
|
// Start hide animation
|
||||||
|
bluetoothMenuRect.scaleValue = 0.8
|
||||||
|
bluetoothMenuRect.opacityValue = 0.0
|
||||||
|
// Hide after animation completes
|
||||||
|
hideTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer to hide panel after animation
|
||||||
|
Timer {
|
||||||
|
id: hideTimer
|
||||||
|
interval: Style.animationSlow
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
bluetoothPanel.visible = false
|
||||||
|
bluetoothPanel.dismissed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: bluetoothMenuRect
|
||||||
|
color: Colors.mSurface
|
||||||
|
radius: Style.radiusLarge * scaling
|
||||||
|
border.color: Colors.mOutlineVariant
|
||||||
|
border.width: Math.max(1, Style.borderThin * scaling)
|
||||||
|
width: 340 * scaling
|
||||||
|
height: 500 * scaling
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.topMargin: Style.marginTiny * scaling
|
||||||
|
anchors.rightMargin: Style.marginTiny * scaling
|
||||||
|
|
||||||
|
// Animation properties
|
||||||
|
property real scaleValue: 0.8
|
||||||
|
property real opacityValue: 0.0
|
||||||
|
|
||||||
|
scale: scaleValue
|
||||||
|
opacity: opacityValue
|
||||||
|
|
||||||
|
// Animate in when component is completed
|
||||||
|
Component.onCompleted: {
|
||||||
|
scaleValue = 1.0
|
||||||
|
opacityValue = 1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation behaviors
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationSlow
|
||||||
|
easing.type: Easing.OutExpo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Style.animationNormal
|
||||||
|
easing.type: Easing.OutQuad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginLarge * scaling
|
||||||
|
spacing: Style.marginMedium * scaling
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginMedium * scaling
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "bluetooth"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pointSize: Style.fontSizeXL * scaling
|
||||||
|
color: Colors.mPrimary
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "Bluetooth"
|
||||||
|
font.pointSize: Style.fontSizeLarge * scaling
|
||||||
|
font.bold: true
|
||||||
|
color: Colors.mOnSurface
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "refresh"
|
||||||
|
tooltipText: "Refresh Devices"
|
||||||
|
sizeMultiplier: 0.8
|
||||||
|
enabled: Settings.data.network.bluetoothEnabled && !BluetoothService.isDiscovering
|
||||||
|
onClicked: {
|
||||||
|
BluetoothService.refreshDevices()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NIconButton {
|
||||||
|
icon: "close"
|
||||||
|
tooltipText: "Close"
|
||||||
|
sizeMultiplier: 0.8
|
||||||
|
onClicked: {
|
||||||
|
bluetoothPanel.hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NDivider {}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
// Loading indicator
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: Settings.data.network.bluetoothEnabled && BluetoothService.isDiscovering
|
||||||
|
spacing: Style.marginMedium * scaling
|
||||||
|
|
||||||
|
NBusyIndicator {
|
||||||
|
running: BluetoothService.isDiscovering
|
||||||
|
color: Colors.mPrimary
|
||||||
|
size: Style.baseWidgetSize * scaling
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "Scanning for devices..."
|
||||||
|
font.pointSize: Style.fontSizeNormal * scaling
|
||||||
|
color: Colors.mOnSurfaceVariant
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bluetooth disabled message
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: !Settings.data.network.bluetoothEnabled
|
||||||
|
spacing: Style.marginMedium * scaling
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "bluetooth_disabled"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pointSize: Style.fontSizeXXL * scaling
|
||||||
|
color: Colors.mOnSurfaceVariant
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "Bluetooth is disabled"
|
||||||
|
font.pointSize: Style.fontSizeLarge * scaling
|
||||||
|
color: Colors.mOnSurfaceVariant
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "Enable Bluetooth to see available devices"
|
||||||
|
font.pointSize: Style.fontSizeNormal * scaling
|
||||||
|
color: Colors.mOnSurfaceVariant
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Device list
|
||||||
|
ListView {
|
||||||
|
id: deviceList
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: Settings.data.network.bluetoothEnabled && !BluetoothService.isDiscovering
|
||||||
|
model: []
|
||||||
|
spacing: Style.marginMedium * scaling
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
// Combine all devices into a single list for the ListView
|
||||||
|
property var allDevices: {
|
||||||
|
const devices = []
|
||||||
|
|
||||||
|
// Add connected devices first
|
||||||
|
for (const device of BluetoothService.connectedDevices) {
|
||||||
|
devices.push({
|
||||||
|
device: device,
|
||||||
|
type: 'connected',
|
||||||
|
section: 'Connected Devices'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add paired devices
|
||||||
|
for (const device of BluetoothService.pairedDevices) {
|
||||||
|
devices.push({
|
||||||
|
device: device,
|
||||||
|
type: 'paired',
|
||||||
|
section: 'Paired Devices'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add available devices
|
||||||
|
for (const device of BluetoothService.availableDevices) {
|
||||||
|
devices.push({
|
||||||
|
device: device,
|
||||||
|
type: 'available',
|
||||||
|
section: 'Available Devices'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update model when devices change
|
||||||
|
onAllDevicesChanged: {
|
||||||
|
deviceList.model = allDevices
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also watch for changes in the service arrays
|
||||||
|
Connections {
|
||||||
|
target: BluetoothService
|
||||||
|
function onConnectedDevicesChanged() {
|
||||||
|
deviceList.model = deviceList.allDevices
|
||||||
|
}
|
||||||
|
function onPairedDevicesChanged() {
|
||||||
|
deviceList.model = deviceList.allDevices
|
||||||
|
}
|
||||||
|
function onAvailableDevicesChanged() {
|
||||||
|
deviceList.model = deviceList.allDevices
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Item {
|
||||||
|
width: parent ? parent.width : 0
|
||||||
|
height: Style.baseWidgetSize * 1.5 * scaling
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Style.radiusMedium * scaling
|
||||||
|
color: modelData.device.connected ? Colors.mPrimary : (deviceMouseArea.containsMouse ? Colors.mTertiary : "transparent")
|
||||||
|
|
||||||
|
RowLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: Style.marginSmall * scaling
|
||||||
|
spacing: Style.marginSmall * scaling
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: BluetoothService.getDeviceIcon(modelData.device)
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pointSize: Style.fontSizeXL * scaling
|
||||||
|
color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface)
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
spacing: Style.marginTiny * scaling
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: modelData.device.name || modelData.device.deviceName || "Unknown Device"
|
||||||
|
font.pointSize: Style.fontSizeNormal * scaling
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.fillWidth: true
|
||||||
|
color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface)
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: {
|
||||||
|
if (modelData.device.connected) {
|
||||||
|
return "Connected"
|
||||||
|
} else if (modelData.device.paired) {
|
||||||
|
return "Paired"
|
||||||
|
} else {
|
||||||
|
return "Available"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
font.pointSize: Style.fontSizeSmall * scaling
|
||||||
|
color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurfaceVariant)
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: BluetoothService.getBatteryText(modelData.device)
|
||||||
|
font.pointSize: Style.fontSizeSmall * scaling
|
||||||
|
color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurfaceVariant)
|
||||||
|
visible: modelData.device.batteryAvailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
Layout.preferredWidth: Style.baseWidgetSize * 0.7 * scaling
|
||||||
|
Layout.preferredHeight: Style.baseWidgetSize * 0.7 * scaling
|
||||||
|
visible: modelData.device.pairing || modelData.device.state === 2 // Connecting state
|
||||||
|
|
||||||
|
NBusyIndicator {
|
||||||
|
visible: modelData.device.pairing || modelData.device.state === 2
|
||||||
|
running: modelData.device.pairing || modelData.device.state === 2
|
||||||
|
color: Colors.mPrimary
|
||||||
|
anchors.centerIn: parent
|
||||||
|
size: Style.baseWidgetSize * 0.7 * scaling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
visible: modelData.device.connected
|
||||||
|
text: "connected"
|
||||||
|
font.pointSize: Style.fontSizeSmall * scaling
|
||||||
|
color: modelData.device.connected ? Colors.mSurface : (deviceMouseArea.containsMouse ? Colors.mSurface : Colors.mOnSurface)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: {
|
||||||
|
if (modelData.device.connected) {
|
||||||
|
BluetoothService.disconnectDevice(modelData.device)
|
||||||
|
} else if (modelData.device.paired) {
|
||||||
|
BluetoothService.connectDevice(modelData.device)
|
||||||
|
} else {
|
||||||
|
BluetoothService.pairDevice(modelData.device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty state when no devices found
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
visible: Settings.data.network.bluetoothEnabled &&
|
||||||
|
!BluetoothService.isDiscovering &&
|
||||||
|
deviceList.count === 0
|
||||||
|
spacing: Style.marginMedium * scaling
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "bluetooth_disabled"
|
||||||
|
font.family: "Material Symbols Outlined"
|
||||||
|
font.pointSize: Style.fontSizeXXL * scaling
|
||||||
|
color: Colors.mOnSurfaceVariant
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "No Bluetooth devices"
|
||||||
|
font.pointSize: Style.fontSizeLarge * scaling
|
||||||
|
color: Colors.mOnSurfaceVariant
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
NText {
|
||||||
|
text: "Click the refresh button to discover devices"
|
||||||
|
font.pointSize: Style.fontSizeNormal * scaling
|
||||||
|
color: Colors.mOnSurfaceVariant
|
||||||
|
Layout.alignment: Qt.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -58,6 +58,7 @@ ColumnLayout {
|
||||||
checked: Settings.data.network.bluetoothEnabled
|
checked: Settings.data.network.bluetoothEnabled
|
||||||
onToggled: checked => {
|
onToggled: checked => {
|
||||||
Settings.data.network.bluetoothEnabled = checked
|
Settings.data.network.bluetoothEnabled = checked
|
||||||
|
BluetoothService.setBluetoothEnabled(checked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,15 @@ ColumnLayout {
|
||||||
Settings.data.location.reverseDayMonth = checked
|
Settings.data.location.reverseDayMonth = checked
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NToggle {
|
||||||
|
label: "Show Date with Clock"
|
||||||
|
description: "Display date alongside time (e.g., 18:12 - Sat, 23 Aug)"
|
||||||
|
checked: Settings.data.location.showDateWithClock
|
||||||
|
onToggled: checked => {
|
||||||
|
Settings.data.location.showDateWithClock = checked
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NDivider {
|
NDivider {
|
||||||
|
|
|
||||||
424
Services/BluetoothService.qml
Normal file
424
Services/BluetoothService.qml
Normal file
|
|
@ -0,0 +1,424 @@
|
||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Bluetooth
|
||||||
|
import qs.Commons
|
||||||
|
|
||||||
|
Singleton {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
// Bluetooth state properties
|
||||||
|
property bool isEnabled: Settings.data.network.bluetoothEnabled
|
||||||
|
property bool isDiscovering: false
|
||||||
|
property var connectedDevices: []
|
||||||
|
property var pairedDevices: []
|
||||||
|
property var availableDevices: []
|
||||||
|
property string lastConnectedDevice: ""
|
||||||
|
property string connectStatus: ""
|
||||||
|
property string connectStatusDevice: ""
|
||||||
|
property string connectError: ""
|
||||||
|
|
||||||
|
// Timer for refreshing device lists
|
||||||
|
property Timer refreshTimer: Timer {
|
||||||
|
interval: 5000 // Refresh every 5 seconds when discovery is active
|
||||||
|
repeat: true
|
||||||
|
running: root.isEnabled && Bluetooth.defaultAdapter && Bluetooth.defaultAdapter.discovering
|
||||||
|
onTriggered: root.refreshDevices()
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
console.log("[Bluetooth] Service started")
|
||||||
|
|
||||||
|
if (isEnabled && Bluetooth.defaultAdapter) {
|
||||||
|
// Ensure adapter is enabled
|
||||||
|
if (!Bluetooth.defaultAdapter.enabled) {
|
||||||
|
Bluetooth.defaultAdapter.enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start discovery to find devices
|
||||||
|
if (!Bluetooth.defaultAdapter.discovering) {
|
||||||
|
Bluetooth.defaultAdapter.discovering = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh devices after a short delay to allow discovery to start
|
||||||
|
Qt.callLater(function() {
|
||||||
|
refreshDevices()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to enable/disable Bluetooth
|
||||||
|
function setBluetoothEnabled(enabled) {
|
||||||
|
|
||||||
|
if (enabled) {
|
||||||
|
// Store the currently connected devices before enabling
|
||||||
|
for (const device of connectedDevices) {
|
||||||
|
if (device.connected) {
|
||||||
|
lastConnectedDevice = device.name || device.deviceName
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable Bluetooth
|
||||||
|
if (Bluetooth.defaultAdapter) {
|
||||||
|
Bluetooth.defaultAdapter.enabled = true
|
||||||
|
|
||||||
|
// Start discovery to find devices
|
||||||
|
if (!Bluetooth.defaultAdapter.discovering) {
|
||||||
|
Bluetooth.defaultAdapter.discovering = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh devices after enabling
|
||||||
|
Qt.callLater(refreshDevices)
|
||||||
|
} else {
|
||||||
|
console.warn("[Bluetooth] No Bluetooth adapter found!")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Disconnect from current devices before disabling
|
||||||
|
for (const device of connectedDevices) {
|
||||||
|
if (device.connected) {
|
||||||
|
device.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable Bluetooth
|
||||||
|
if (Bluetooth.defaultAdapter) {
|
||||||
|
console.log("[Bluetooth] Disabling Bluetooth adapter")
|
||||||
|
Bluetooth.defaultAdapter.enabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Settings.data.network.bluetoothEnabled = enabled
|
||||||
|
isEnabled = enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to refresh device lists
|
||||||
|
function refreshDevices() {
|
||||||
|
if (!isEnabled || !Bluetooth.defaultAdapter) {
|
||||||
|
connectedDevices = []
|
||||||
|
pairedDevices = []
|
||||||
|
availableDevices = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicate check since we already did it above
|
||||||
|
|
||||||
|
const connected = []
|
||||||
|
const paired = []
|
||||||
|
const available = []
|
||||||
|
|
||||||
|
let devices = null
|
||||||
|
|
||||||
|
// Try adapter devices first
|
||||||
|
if (Bluetooth.defaultAdapter.enabled && Bluetooth.defaultAdapter.devices) {
|
||||||
|
devices = Bluetooth.defaultAdapter.devices
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to global devices list
|
||||||
|
if (!devices && Bluetooth.devices) {
|
||||||
|
devices = Bluetooth.devices
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!devices) {
|
||||||
|
connectedDevices = []
|
||||||
|
pairedDevices = []
|
||||||
|
availableDevices = []
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use Qt model methods to iterate through the ObjectModel
|
||||||
|
let deviceFound = false
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get the row count using the Qt model method
|
||||||
|
const rowCount = devices.rowCount()
|
||||||
|
|
||||||
|
if (rowCount > 0) {
|
||||||
|
// Iterate through each row using the Qt model data() method
|
||||||
|
for (let i = 0; i < rowCount; i++) {
|
||||||
|
try {
|
||||||
|
// Create a model index for this row
|
||||||
|
const modelIndex = devices.index(i, 0)
|
||||||
|
if (!modelIndex.valid) continue
|
||||||
|
|
||||||
|
// Get the device object using the Qt.UserRole (typically 256)
|
||||||
|
const device = devices.data(modelIndex, 256) // Qt.UserRole
|
||||||
|
if (!device) {
|
||||||
|
// Try alternative role values
|
||||||
|
const deviceAlt = devices.data(modelIndex, 0) // Qt.DisplayRole
|
||||||
|
if (deviceAlt) {
|
||||||
|
device = deviceAlt
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceFound = true
|
||||||
|
|
||||||
|
if (device.connected) {
|
||||||
|
connected.push(device)
|
||||||
|
} else if (device.paired) {
|
||||||
|
paired.push(device)
|
||||||
|
} else {
|
||||||
|
available.push(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
// Silent error handling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alternative method: try the values property if available
|
||||||
|
if (!deviceFound && devices.values) {
|
||||||
|
try {
|
||||||
|
const values = devices.values
|
||||||
|
if (values && typeof values === 'object') {
|
||||||
|
// Try to iterate through values if it's iterable
|
||||||
|
if (values.length !== undefined) {
|
||||||
|
for (let i = 0; i < values.length; i++) {
|
||||||
|
const device = values[i]
|
||||||
|
if (device) {
|
||||||
|
deviceFound = true
|
||||||
|
if (device.connected) {
|
||||||
|
connected.push(device)
|
||||||
|
} else if (device.paired) {
|
||||||
|
paired.push(device)
|
||||||
|
} else {
|
||||||
|
available.push(device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Silent error handling
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[Bluetooth] Error accessing device model:", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!deviceFound) {
|
||||||
|
console.log("[Bluetooth] No devices found")
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedDevices = connected
|
||||||
|
pairedDevices = paired
|
||||||
|
availableDevices = available
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to start discovery
|
||||||
|
function startDiscovery() {
|
||||||
|
if (!isEnabled || !Bluetooth.defaultAdapter) return
|
||||||
|
|
||||||
|
isDiscovering = true
|
||||||
|
Bluetooth.defaultAdapter.discovering = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to stop discovery
|
||||||
|
function stopDiscovery() {
|
||||||
|
if (!Bluetooth.defaultAdapter) return
|
||||||
|
|
||||||
|
isDiscovering = false
|
||||||
|
Bluetooth.defaultAdapter.discovering = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to connect to a device
|
||||||
|
function connectDevice(device) {
|
||||||
|
if (!device) return
|
||||||
|
|
||||||
|
// Check if device is still valid (not stale from previous session)
|
||||||
|
if (!device.connect || typeof device.connect !== 'function') {
|
||||||
|
console.warn("[Bluetooth] Device object is stale, refreshing devices")
|
||||||
|
refreshDevices()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
connectStatus = "connecting"
|
||||||
|
connectStatusDevice = device.name || device.deviceName
|
||||||
|
connectError = ""
|
||||||
|
|
||||||
|
try {
|
||||||
|
device.connect()
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Bluetooth] Error connecting to device:", error)
|
||||||
|
connectStatus = "error"
|
||||||
|
connectError = error.toString()
|
||||||
|
Qt.callLater(refreshDevices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to disconnect from a device
|
||||||
|
function disconnectDevice(device) {
|
||||||
|
if (!device) return
|
||||||
|
|
||||||
|
// Check if device is still valid (not stale from previous session)
|
||||||
|
if (!device.disconnect || typeof device.disconnect !== 'function') {
|
||||||
|
console.warn("[Bluetooth] Device object is stale, refreshing devices")
|
||||||
|
refreshDevices()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
device.disconnect()
|
||||||
|
// Clear connection status
|
||||||
|
connectStatus = ""
|
||||||
|
connectStatusDevice = ""
|
||||||
|
connectError = ""
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[Bluetooth] Error disconnecting device:", error)
|
||||||
|
Qt.callLater(refreshDevices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to pair with a device
|
||||||
|
function pairDevice(device) {
|
||||||
|
if (!device) return
|
||||||
|
|
||||||
|
// Check if device is still valid (not stale from previous session)
|
||||||
|
if (!device.pair || typeof device.pair !== 'function') {
|
||||||
|
console.warn("[Bluetooth] Device object is stale, refreshing devices")
|
||||||
|
refreshDevices()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
device.pair()
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[Bluetooth] Error pairing device:", error)
|
||||||
|
Qt.callLater(refreshDevices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to forget a device
|
||||||
|
function forgetDevice(device) {
|
||||||
|
if (!device) return
|
||||||
|
|
||||||
|
// Check if device is still valid (not stale from previous session)
|
||||||
|
if (!device.forget || typeof device.forget !== 'function') {
|
||||||
|
console.warn("[Bluetooth] Device object is stale, refreshing devices")
|
||||||
|
refreshDevices()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store device info before forgetting (in case device object becomes invalid)
|
||||||
|
const deviceName = device.name || device.deviceName || "Unknown Device"
|
||||||
|
|
||||||
|
try {
|
||||||
|
device.forget()
|
||||||
|
|
||||||
|
// Clear any connection status that might be related to this device
|
||||||
|
if (connectStatusDevice === deviceName) {
|
||||||
|
connectStatus = ""
|
||||||
|
connectStatusDevice = ""
|
||||||
|
connectError = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh devices after a delay to ensure the forget operation is complete
|
||||||
|
Qt.callLater(refreshDevices, 1000)
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[Bluetooth] Error forgetting device:", error)
|
||||||
|
Qt.callLater(refreshDevices, 500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get device icon
|
||||||
|
function getDeviceIcon(device) {
|
||||||
|
if (!device) return "bluetooth"
|
||||||
|
|
||||||
|
// Use device icon if available, otherwise fall back to device type
|
||||||
|
if (device.icon) {
|
||||||
|
return device.icon
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback icons based on common device types
|
||||||
|
const name = (device.name || device.deviceName || "").toLowerCase()
|
||||||
|
if (name.includes("headphone") || name.includes("earbud") || name.includes("airpods")) {
|
||||||
|
return "headphones"
|
||||||
|
} else if (name.includes("speaker")) {
|
||||||
|
return "speaker"
|
||||||
|
} else if (name.includes("keyboard")) {
|
||||||
|
return "keyboard"
|
||||||
|
} else if (name.includes("mouse")) {
|
||||||
|
return "mouse"
|
||||||
|
} else if (name.includes("phone") || name.includes("mobile")) {
|
||||||
|
return "smartphone"
|
||||||
|
} else if (name.includes("laptop") || name.includes("computer")) {
|
||||||
|
return "laptop"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "bluetooth"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get device status text
|
||||||
|
function getDeviceStatus(device) {
|
||||||
|
if (!device) return ""
|
||||||
|
|
||||||
|
if (device.connected) {
|
||||||
|
return "Connected"
|
||||||
|
} else if (device.pairing) {
|
||||||
|
return "Pairing..."
|
||||||
|
} else if (device.paired) {
|
||||||
|
return "Paired"
|
||||||
|
} else {
|
||||||
|
return "Available"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to get battery level text
|
||||||
|
function getBatteryText(device) {
|
||||||
|
if (!device || !device.batteryAvailable) return ""
|
||||||
|
|
||||||
|
const percentage = Math.round(device.battery * 100)
|
||||||
|
return `${percentage}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for Bluetooth adapter changes
|
||||||
|
Connections {
|
||||||
|
target: Bluetooth.defaultAdapter
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
|
||||||
|
function onEnabledChanged() {
|
||||||
|
root.isEnabled = Bluetooth.defaultAdapter.enabled
|
||||||
|
Settings.data.network.bluetoothEnabled = root.isEnabled
|
||||||
|
if (root.isEnabled) {
|
||||||
|
Qt.callLater(refreshDevices)
|
||||||
|
} else {
|
||||||
|
connectedDevices = []
|
||||||
|
pairedDevices = []
|
||||||
|
availableDevices = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDiscoveringChanged() {
|
||||||
|
root.isDiscovering = Bluetooth.defaultAdapter.discovering
|
||||||
|
if (Bluetooth.defaultAdapter.discovering) {
|
||||||
|
Qt.callLater(refreshDevices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStateChanged() {
|
||||||
|
if (Bluetooth.defaultAdapter.state >= 4) {
|
||||||
|
Qt.callLater(refreshDevices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDevicesChanged() {
|
||||||
|
Qt.callLater(refreshDevices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for global device changes
|
||||||
|
Connections {
|
||||||
|
target: Bluetooth
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
|
||||||
|
function onDevicesChanged() {
|
||||||
|
Qt.callLater(refreshDevices)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -156,13 +156,13 @@ Singleton {
|
||||||
readonly property Process initProc: Process {
|
readonly property Process initProc: Process {
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
console.log("[BrightnessService] Raw brightness data for", monitor.modelData.name + ":", text.trim())
|
console.log("[Brightness] Raw brightness data for", monitor.modelData.name + ":", text.trim())
|
||||||
|
|
||||||
if (monitor.isAppleDisplay) {
|
if (monitor.isAppleDisplay) {
|
||||||
var val = parseInt(text.trim())
|
var val = parseInt(text.trim())
|
||||||
if (!isNaN(val)) {
|
if (!isNaN(val)) {
|
||||||
monitor.brightness = val / 101
|
monitor.brightness = val / 101
|
||||||
console.log("[BrightnessService] Apple display brightness:", monitor.brightness)
|
console.log("[Brightness] Apple display brightness:", monitor.brightness)
|
||||||
}
|
}
|
||||||
} else if (monitor.isDdc) {
|
} else if (monitor.isDdc) {
|
||||||
var parts = text.trim().split(" ")
|
var parts = text.trim().split(" ")
|
||||||
|
|
@ -171,7 +171,7 @@ Singleton {
|
||||||
var max = parseInt(parts[1])
|
var max = parseInt(parts[1])
|
||||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||||
monitor.brightness = current / max
|
monitor.brightness = current / max
|
||||||
console.log("[BrightnessService] DDC brightness:", current + "/" + max + " =", monitor.brightness)
|
console.log("[Brightness] DDC brightness:", current + "/" + max + " =", monitor.brightness)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -182,7 +182,7 @@ Singleton {
|
||||||
var max = parseInt(parts[1])
|
var max = parseInt(parts[1])
|
||||||
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
if (!isNaN(current) && !isNaN(max) && max > 0) {
|
||||||
monitor.brightness = current / max
|
monitor.brightness = current / max
|
||||||
console.log("[BrightnessService] Internal brightness:", current + "/" + max + " =", monitor.brightness)
|
console.log("[Brightness] Internal brightness:", current + "/" + max + " =", monitor.brightness)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue