// Copyright (C) 2026 Danilo M. // // This program is free software; you can redistribute it and/or modify // it under the terms of the GNU General Public License version 2 as // published by the Free Software Foundation. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. import QtQuick import "../.." Column { id: page required property var bt signal back spacing: 14 // --- Adapter --- Item { width: page.width implicitHeight: Math.max(btLabel.implicitHeight, powerSwitch.implicitHeight) Text { id: btLabel anchors.verticalCenter: parent.verticalCenter text: "Bluetooth" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 1; bold: true } color: Theme.text } Switch { id: powerSwitch anchors { right: parent.right; verticalCenter: parent.verticalCenter } enabled: page.bt.adapter !== null checked: page.bt.adapter ? page.bt.adapter.enabled : false onToggled: if (page.bt.adapter) page.bt.adapter.enabled = checked } } Text { width: page.width visible: !(page.bt.adapter && page.bt.adapter.enabled) text: "Bluetooth off" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: Theme.subtext } Item { width: page.width visible: page.bt.adapter && page.bt.adapter.enabled implicitHeight: Math.max(visLabel.implicitHeight, visSwitch.implicitHeight) Text { id: visLabel anchors.verticalCenter: parent.verticalCenter text: "Visible" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: Theme.text } Switch { id: visSwitch anchors { right: parent.right; verticalCenter: parent.verticalCenter } enabled: page.bt.adapter !== null checked: page.bt.adapter ? page.bt.adapter.discoverable : false onToggled: if (page.bt.adapter) page.bt.adapter.discoverable = checked } } Row { width: page.width visible: page.bt.adapter && page.bt.adapter.enabled spacing: 8 Button { text: page.bt.adapter && page.bt.adapter.discovering ? "Stop scan" : "Scan" onClicked: if (page.bt.adapter) page.bt.adapter.discovering = !page.bt.adapter.discovering } // BlueZ discovery runs until stopped, and closing the drawer does not // stop it, so a forgotten scan keeps the radio busy for the session. // Bound to the property rather than started by the button: a manual // stop cancels the clock, and reopening the page re-arms a fresh 60s // on a scan still running from before. Timer { interval: 60000 running: page.bt.adapter ? page.bt.adapter.discovering : false onTriggered: if (page.bt.adapter) page.bt.adapter.discovering = false } } // --- Connected --- Text { width: page.width visible: page.bt.connectedDevices.length > 0 text: "Connected" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } color: Theme.subtext } Repeater { model: page.bt.connectedDevices BluetoothRow { required property var modelData bt: page.bt device: modelData width: page.width } } // --- Paired --- Text { width: page.width visible: page.bt.pairedDevices.length > 0 text: "Paired" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } color: Theme.subtext } Repeater { model: page.bt.pairedDevices BluetoothRow { required property var modelData bt: page.bt device: modelData width: page.width } } // --- Available (found while scanning, not yet paired) --- Text { width: page.width visible: page.bt.availableDevices.length > 0 text: "Available" font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2; bold: true } color: Theme.subtext } Repeater { model: page.bt.availableDevices Rectangle { required property var modelData width: page.width implicitHeight: availText.implicitHeight + 16 radius: 8 color: Qt.alpha(Theme.surface, 0.35) Text { id: availText anchors { left: parent.left; leftMargin: 10; verticalCenter: parent.verticalCenter } width: parent.width - pairBtn.width - 28 elide: Text.ElideRight text: page.bt.pairing.busy && page.bt.pairing.address === modelData.address ? "Pairing…" : (modelData.name || modelData.deviceName || modelData.address) font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 2 } color: Theme.text } Button { id: pairBtn anchors { right: parent.right; rightMargin: 8; verticalCenter: parent.verticalCenter } text: "Pair" enabled: !page.bt.pairing.busy onClicked: page.bt.pairing.pair(modelData.address) } } } // A failed pairing leaves the real bluetoothctl output here. No timeout: // it stays until dismissed, and the next failure sets it again, so an // identical error reappears rather than staying hidden. Dismissing clears // the source, not a local flag, so it survives this page unloading. The // desktop notification is sent separately and is not touched by this. Rectangle { width: page.width visible: page.bt.pairing.error !== "" implicitHeight: errText.implicitHeight + 16 radius: 8 color: Qt.alpha(Theme.red, 0.12) border.width: 1 border.color: Qt.alpha(Theme.red, 0.35) Text { id: errText anchors { left: parent.left; leftMargin: 10 right: errClose.left; rightMargin: 8 verticalCenter: parent.verticalCenter } wrapMode: Text.Wrap text: page.bt.pairing.error font { family: Theme.fontFamily; pixelSize: Theme.fontSize - 3 } color: Theme.red } Text { id: errClose anchors { right: parent.right; rightMargin: 8; verticalCenter: parent.verticalCenter } width: 20 height: 20 text: "\uf00d" horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font { family: Theme.iconFamily; pixelSize: 13 } color: errCloseArea.containsMouse ? Theme.red : Theme.subtext MouseArea { id: errCloseArea anchors.fill: parent anchors.margins: -6 hoverEnabled: true cursorShape: Qt.PointingHandCursor onClicked: page.bt.pairing.error = "" } } } }