Update BluetoothPanel.qml

Update BluetoothDevicesList.qml

Update BluetoothSubTab.qml

No toast on discoverable

Update BluetoothService.qml

Delete BluetoothDevicesList.qml

Update BluetoothService.qml

Update BluetoothSubTab.qml

fmt
This commit is contained in:
Turann_
2026-02-09 19:18:56 +03:00
parent d00638d382
commit be0b3798b1
6 changed files with 463 additions and 737 deletions
@@ -1,476 +0,0 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Wayland
import qs.Commons
import qs.Services.Hardware
import qs.Services.Networking
import qs.Services.UI
import qs.Widgets
NBox {
id: root
property string label: ""
property string tooltipText: ""
property var model: {}
// Header control mode: "layout" (default) shows grid/list toggle; "filter" shows unnamed-devices filter toggle
property string headerMode: "layout"
// Per-list expanded details (by device key)
property string expandedDeviceKey: ""
// Local layout toggle for details: true = grid (2 cols), false = rows (1 col)
// Persisted under Settings.data.network.bluetoothDetailsViewMode
property bool detailsGrid: (Settings.data && Settings.data.ui && Settings.data.network.bluetoothDetailsViewMode !== undefined) ? (Settings.data.network.bluetoothDetailsViewMode === "grid") : true
Layout.fillWidth: true
Layout.preferredHeight: column.implicitHeight + Style.marginXL
ColumnLayout {
id: column
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
RowLayout {
Layout.fillWidth: true
visible: root.model.length > 0
Layout.leftMargin: Style.marginM
spacing: Style.marginS
NText {
text: root.label
pointSize: Style.fontSizeS
color: Color.mSecondary
font.weight: Style.fontWeightBold
Layout.fillWidth: true
}
// (moved) details view toggle is now inside the expanded info box
// Filter toggle (for Available devices): hide unnamed devices
NIconButton {
visible: root.headerMode === "filter"
// Option A: filter/filter-off
// Off (show all): filter; On (hide unnamed): filter-off
icon: (Settings.data && Settings.data.ui && Settings.data.network.bluetoothHideUnnamedDevices) ? "filter-off" : "filter"
tooltipText: (Settings.data && Settings.data.ui && Settings.data.network.bluetoothHideUnnamedDevices) ? I18n.tr("tooltips.hide-unnamed-devices") : I18n.tr("tooltips.show-all-devices")
onClicked: {
if (Settings.data && Settings.data.ui) {
Settings.data.network.bluetoothHideUnnamedDevices = !(Settings.data.network.bluetoothHideUnnamedDevices);
}
}
}
}
Repeater {
id: deviceList
Layout.fillWidth: true
model: root.model
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
NBox {
id: device
readonly property bool canConnect: BluetoothService.canConnect(modelData)
readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData)
readonly property bool canPair: BluetoothService.canPair(modelData)
readonly property bool isBusy: BluetoothService.isDeviceBusy(modelData)
readonly property bool isExpanded: root.expandedDeviceKey === BluetoothService.deviceKey(modelData)
function getContentColor(defaultColor = Color.mOnSurface) {
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary;
if (modelData.blocked || modelData.state === BluetoothDeviceState.Disconnecting)
return Color.mError;
return defaultColor;
}
Layout.fillWidth: true
Layout.preferredHeight: deviceColumn.implicitHeight + (Style.marginXL)
radius: Style.radiusM
clip: true
color: (modelData.connected && modelData.state !== BluetoothDeviceState.Disconnecting) ? Qt.alpha(Color.mPrimary, 0.15) : Color.mSurface
// Content column so expanded details are laid out inside the card
ColumnLayout {
id: deviceColumn
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginS
RowLayout {
id: deviceLayout
Layout.fillWidth: true
spacing: Style.marginM
Layout.alignment: Qt.AlignVCenter
// One device BT icon
NIcon {
icon: BluetoothService.getDeviceIcon(modelData)
pointSize: Style.fontSizeXXL
color: modelData.connected ? Color.mPrimary : getContentColor(Color.mOnSurface)
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS
// Device name
NText {
text: modelData.name || modelData.deviceName
pointSize: Style.fontSizeM
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
elide: Text.ElideRight
color: getContentColor(Color.mOnSurface)
Layout.fillWidth: true
}
// Status
NText {
text: {
const k = BluetoothService.getStatusKey(modelData);
if (k === "pairing")
return I18n.tr("common.pairing");
if (k === "blocked")
return I18n.tr("bluetooth.panel.blocked");
if (k === "connecting")
return I18n.tr("common.connecting");
if (k === "disconnecting")
return I18n.tr("common.disconnecting");
return "";
}
visible: text !== ""
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurfaceVariant)
}
// Signal strength: show only in the expanded info panel (hidden in compact row)
RowLayout {
visible: false
Layout.fillWidth: true
spacing: Style.marginXS
}
// Battery (icon + percent)
RowLayout {
visible: modelData.batteryAvailable
spacing: Style.marginXS
NIcon {
icon: {
var b = BluetoothService.getBatteryPercent(modelData);
return BatteryService.getIcon(b !== null ? b : 0, false, false, b !== null);
}
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurface)
}
NText {
text: {
var b = BluetoothService.getBatteryPercent(modelData);
return b === null ? "-" : (b + "%");
}
pointSize: Style.fontSizeXS
color: getContentColor(Color.mOnSurfaceVariant)
}
}
}
// Spacer to push actions to the right
Item {
Layout.fillWidth: true
}
// Actions (Info on the left to match WiFi, then Unpair, then main CTA)
RowLayout {
spacing: Style.marginS
// Info for connected device (placed before the CTA for consistency with WiFi)
NIconButton {
visible: modelData.connected
icon: "info"
tooltipText: I18n.tr("common.info")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
const key = BluetoothService.deviceKey(modelData);
root.expandedDeviceKey = (root.expandedDeviceKey === key) ? "" : key;
}
}
// Unpair for saved devices when not connected
NIconButton {
visible: (modelData.paired || modelData.trusted) && !modelData.connected && !isBusy && !modelData.blocked
icon: "trash"
tooltipText: I18n.tr("common.unpair")
baseSize: Style.baseWidgetSize * 0.8
onClicked: BluetoothService.unpairDevice(modelData)
}
// Main Call to action
NButton {
id: button
visible: (modelData.state !== BluetoothDeviceState.Connecting)
enabled: (canConnect || canDisconnect || canPair) && !isBusy
outlined: !button.hovered
fontSize: Style.fontSizeS
tooltipText: root.tooltipText
backgroundColor: modelData.connected ? Color.mError : Color.mPrimary
text: {
if (modelData.pairing) {
return I18n.tr("common.pairing");
}
if (modelData.blocked) {
return I18n.tr("bluetooth.panel.blocked");
}
if (modelData.connected) {
return I18n.tr("common.disconnect");
}
if (device.canPair) {
return I18n.tr("common.pair");
}
return I18n.tr("common.connect");
}
icon: (isBusy ? "busy" : null)
onClicked: {
if (modelData.connected) {
BluetoothService.disconnectDevice(modelData);
} else {
if (device.canPair) {
BluetoothService.pairDevice(modelData);
} else {
BluetoothService.connectDeviceWithTrust(modelData);
}
}
}
}
}
}
// Expanded info section
Rectangle {
visible: device.isExpanded
Layout.fillWidth: true
implicitHeight: infoColumn.implicitHeight + Style.marginS * 2
radius: Style.radiusS
color: Color.mSurfaceVariant
border.width: Style.borderS
border.color: Color.mOutline
clip: true
onVisibleChanged: {
if (visible && infoColumn && infoColumn.forceLayout) {
Qt.callLater(function () {
infoColumn.forceLayout();
});
}
}
// Grid/List toggle moved here to the top-right corner of the info box
NIconButton {
id: detailsToggle
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginS
// Use Tabler layout icons; "grid" alone doesn't exist in our font
icon: root.detailsGrid ? "layout-list" : "layout-grid"
tooltipText: root.detailsGrid ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.detailsGrid = !root.detailsGrid;
if (Settings.data && Settings.data.ui) {
Settings.data.network.bluetoothDetailsViewMode = root.detailsGrid ? "grid" : "list";
}
}
z: 1
}
GridLayout {
id: infoColumn
anchors.fill: parent
anchors.margins: Style.marginS
// Layout toggle based on local state
columns: root.detailsGrid ? 2 : 1
columnSpacing: Style.marginM
rowSpacing: Style.marginXS
// Ensure proper relayout when switching grid/list while open
onColumnsChanged: {
if (infoColumn.forceLayout) {
Qt.callLater(function () {
infoColumn.forceLayout();
});
}
}
// Icons only; labels shown as tooltips on hover
// Row 1: Signal | Battery
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
spacing: Style.marginXS
NIcon {
icon: BluetoothService.getSignalIcon(modelData)
pointSize: Style.fontSizeXS
color: Color.mOnSurface
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, I18n.tr("common.signal"))
onExited: TooltipService.hide()
}
}
NText {
text: BluetoothService.getSignalStrength(modelData)
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
// Wrap only when needed to avoid extra spacing
wrapMode: implicitWidth > width ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap
elide: Text.ElideNone
maximumLineCount: 4
clip: true
}
}
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
spacing: Style.marginXS
NIcon {
icon: {
var b = BluetoothService.getBatteryPercent(modelData);
return BatteryService.getIcon(b !== null ? b : 0, false, false, b !== null);
}
pointSize: Style.fontSizeXS
color: Color.mOnSurface
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, I18n.tr("common.battery"))
onExited: TooltipService.hide()
}
}
NText {
text: {
var b = BluetoothService.getBatteryPercent(modelData);
return b === null ? "-" : (b + "%");
}
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: implicitWidth > width ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap
elide: Text.ElideNone
maximumLineCount: 4
clip: true
}
}
// Row 2: Paired | Trusted
RowLayout {
Layout.fillWidth: true
spacing: Style.marginXS
NIcon {
icon: "link"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, I18n.tr("common.paired"))
onExited: TooltipService.hide()
}
}
NText {
text: modelData.paired ? I18n.tr("common.yes") : I18n.tr("common.no")
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: implicitWidth > width ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap
elide: Text.ElideNone
maximumLineCount: 2
clip: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginXS
NIcon {
icon: "shield-check"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, I18n.tr("common.trusted"))
onExited: TooltipService.hide()
}
}
NText {
text: modelData.trusted ? I18n.tr("common.yes") : I18n.tr("common.no")
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: implicitWidth > width ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap
elide: Text.ElideNone
maximumLineCount: 2
clip: true
}
}
// Row 3: Address (single row; spans two columns when grid)
RowLayout {
Layout.fillWidth: true
Layout.columnSpan: infoColumn.columns === 2 ? 2 : 1
spacing: Style.marginXS
NIcon {
icon: "hash"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, I18n.tr("bluetooth.panel.device-address"))
onExited: TooltipService.hide()
}
}
NText {
id: macAddressText
text: modelData.address || "-"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
// MAC addresses usually fit; wrap only if necessary
wrapMode: implicitWidth > width ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap
elide: Text.ElideNone
maximumLineCount: 2
clip: true
// Click-to-copy MAC address
MouseArea {
anchors.fill: parent
enabled: (modelData.address && modelData.address.length > 0)
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address"))
onExited: TooltipService.hide()
onClicked: {
const addr = modelData.address || "";
if (addr.length > 0) {
// Copy to clipboard via wl-copy (runtime dependency)
Quickshell.execDetached(["wl-copy", addr]);
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.address-copied"), "bluetooth");
}
}
}
}
}
}
}
}
}
}
}
}
+10 -129
View File
@@ -3,10 +3,11 @@ import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import "../Settings/Tabs/Connections" as BluetoothPrefs
import qs.Commons
import qs.Modules.MainScreen
import qs.Modules.Panels.Settings // For SettingsPanel
import qs.Modules.Panels.Settings
import qs.Services.Hardware
import qs.Services.Networking
import qs.Services.UI
import qs.Widgets
@@ -93,7 +94,6 @@ SmartPanel {
Layout.fillWidth: true
Layout.preferredHeight: disabledColumn.implicitHeight + Style.marginXL
// Center the content within this rectangle
ColumnLayout {
id: disabledColumn
anchors.fill: parent
@@ -139,12 +139,8 @@ SmartPanel {
visible: {
if (!(BluetoothService.adapter && BluetoothService.adapter.enabled && BluetoothService.adapter.devices))
return false;
// Check for connected or paired/trusted devices
var knownCount = BluetoothService.adapter.devices.values.filter(dev => {
return dev && !dev.blocked && (dev.connected || dev.paired || dev.trusted);
}).length;
return (knownCount === 0);
// Pulling pairedDevices count from the source component
return (btSource.pairedDevices.length === 0 && btSource.connectedDevices.length === 0);
}
Layout.fillWidth: true
Layout.preferredHeight: emptyColumn.implicitHeight + Style.marginXL
@@ -182,133 +178,18 @@ SmartPanel {
root.close();
}
}
Item {
Layout.fillHeight: true
}
}
}
// Connected devices
BluetoothDevicesList {
label: I18n.tr("bluetooth.panel.connected-devices")
headerMode: "layout"
property var items: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return [];
var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && dev.connected);
filtered = BluetoothService.dedupeDevices(filtered);
return BluetoothService.sortDevices(filtered);
}
model: items
visible: items.length > 0 && BluetoothService.adapter && BluetoothService.adapter.enabled
// Pull connected/paired lists from BluetoothSubTab
BluetoothPrefs.BluetoothSubTab {
id: btSource
Layout.fillWidth: true
}
// Paired devices
BluetoothDevicesList {
label: I18n.tr("bluetooth.panel.paired-devices")
headerMode: "layout"
property var items: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return [];
var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted));
filtered = BluetoothService.dedupeDevices(filtered);
return BluetoothService.sortDevices(filtered);
}
model: items
visible: items.length > 0 && BluetoothService.adapter && BluetoothService.adapter.enabled
Layout.fillWidth: true
}
}
}
}
// PIN Authentication Overlay
Rectangle {
id: pinOverlay
anchors.fill: parent
color: Color.mSurface
visible: BluetoothService.pinRequired
// Trap all input
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.AllButtons
onClicked: mouse => mouse.accepted = true
onWheel: wheel => wheel.accepted = true
}
ColumnLayout {
anchors.centerIn: parent
width: parent.width * 0.85
spacing: Style.marginL
NIcon {
icon: "lock"
pointSize: 48
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("common.authentication-required")
pointSize: Style.fontSizeXL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter
}
NText {
text: I18n.tr("bluetooth.panel.pin-instructions")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
}
NTextInput {
id: pinInput
Layout.fillWidth: true
placeholderText: "123456"
inputIconName: "key"
// Clear text when overlay appears
onVisibleChanged: {
if (visible) {
text = "";
inputItem.forceActiveFocus();
}
}
// Submit on Enter
inputItem.onAccepted: {
if (text.length > 0) {
BluetoothService.submitPin(text);
text = "";
}
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginM
NButton {
text: I18n.tr("common.cancel")
icon: "x"
onClicked: BluetoothService.cancelPairing()
}
NButton {
text: I18n.tr("common.confirm")
icon: "check"
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
enabled: pinInput.text.length > 0
onClicked: {
BluetoothService.submitPin(pinInput.text);
pinInput.text = "";
}
showOnlyLists: true
visible: !disabledBox.visible && !emptyBox.visible
}
}
}
@@ -6,20 +6,79 @@ import Quickshell
import Quickshell.Bluetooth
import qs.Commons
import qs.Services.Hardware
import qs.Services.Networking
import qs.Services.System
import qs.Services.UI
import qs.Widgets
import qs.Modules.Panels.Bluetooth
Item {
id: btprefs
Layout.fillWidth: true
implicitHeight: mainLayout.implicitHeight // Do i hate locating qml Items? - Absolutely yes
implicitHeight: mainLayout.implicitHeight
// Configuration for shared use (e.g. by BluetoothPanel)
property bool showOnlyLists: false
property bool isScanningActive: BluetoothService.scanningActive
property bool isDiscoverable: BluetoothService.discoverable
// Device lists with local filtering logic
readonly property var connectedDevices: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return [];
var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && dev.connected);
filtered = BluetoothService.dedupeDevices(filtered);
return BluetoothService.sortDevices(filtered);
}
readonly property var pairedDevices: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return [];
var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted));
filtered = BluetoothService.dedupeDevices(filtered);
return BluetoothService.sortDevices(filtered);
}
readonly property var availableDevices: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return [];
var raw = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted);
if (Settings.data && Settings.data.ui && Settings.data.network.bluetoothHideUnnamedDevices) {
raw = raw.filter(function (dev) {
var dn = dev.name || dev.deviceName || "";
var s = String(dn).trim();
if (s.length === 0)
return false;
var lower = s.toLowerCase();
if (lower === "unknown" || lower === "unnamed" || lower === "n/a" || lower === "na")
return false;
var addr = dev.address || dev.bdaddr || dev.mac || "";
if (addr.length > 0) {
var normName = s.toLowerCase().replace(/[^0-9a-z]/g, "");
var normAddr = String(addr).toLowerCase().replace(/[^0-9a-z]/g, "");
if (normName.length > 0 && normName === normAddr)
return false;
}
var macColonHex = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/;
var macHyphenHex = /^([0-9A-Fa-f]{2}-){5}[0-9A-Fa-f]{2}$/;
var macHyphenAny = /^([0-9A-Za-z]{2}-){5}[0-9A-Za-z]{2}$/;
var macDotted = /^[0-9A-Fa-f]{4}\.[0-9A-Fa-f]{4}\.[0-9A-Fa-f]{4}$/;
var macBare = /^[0-9A-Fa-f]{12}$/;
if (macColonHex.test(s) || macHyphenHex.test(s) || macHyphenAny.test(s) || macDotted.test(s) || macBare.test(s))
return false;
return true;
});
}
raw = BluetoothService.dedupeDevices(raw);
return BluetoothService.sortDevices(raw);
}
// For managing expanded device details
property string expandedDeviceKey: ""
property bool detailsGrid: (Settings.data && Settings.data.ui && Settings.data.network.bluetoothDetailsViewMode !== undefined) ? (Settings.data.network.bluetoothDetailsViewMode === "grid") : true
// Combined visibility check: tab must be visible AND the window must be visible
readonly property bool effectivelyVisible: btprefs.visible && Window.window && Window.window.visible
@@ -79,6 +138,7 @@ Item {
// Master Control Section
NBox {
visible: !btprefs.showOnlyLists
Layout.fillWidth: true
Layout.preferredHeight: masterControlCol.implicitHeight + Style.marginL * 2
implicitHeight: Layout.preferredHeight
@@ -114,7 +174,7 @@ Item {
Item {
Layout.fillWidth: true
} // Spacer to push toggle to the right
}
NToggle {
checked: BluetoothService.enabled
@@ -135,119 +195,126 @@ Item {
}
// Device List [1] (Connected)
BluetoothDevicesList {
id: connectedDevicesList
label: I18n.tr("bluetooth.panel.connected-devices")
headerMode: "layout"
property var connectedDevices: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return [];
var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && dev.connected);
filtered = BluetoothService.dedupeDevices(filtered);
return BluetoothService.sortDevices(filtered);
}
model: connectedDevices
visible: connectedDevices.length > 0 && BluetoothService.adapter && BluetoothService.adapter.enabled
NBox {
id: connectedDevicesBox
visible: btprefs.connectedDevices.length > 0 && BluetoothService.adapter && BluetoothService.adapter.enabled
Layout.fillWidth: true
Layout.preferredHeight: connectedDevicesCol.implicitHeight + Style.marginXL
ColumnLayout {
id: connectedDevicesCol
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
NText {
text: I18n.tr("bluetooth.panel.connected-devices")
pointSize: Style.fontSizeS
color: Color.mSecondary
font.weight: Style.fontWeightBold
Layout.fillWidth: true
Layout.leftMargin: Style.marginS
}
Repeater {
model: btprefs.connectedDevices
delegate: nbox_delegate
}
}
}
NDivider {
Layout.fillWidth: true
visible: connectedDevicesList.visible
visible: connectedDevicesBox.visible && !btprefs.showOnlyLists
}
// Devices List [2] (Paired)
BluetoothDevicesList {
id: pairedDevicesList
label: I18n.tr("bluetooth.panel.paired-devices")
headerMode: "layout"
property var pairedDevices: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return [];
var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && !dev.connected && (dev.paired || dev.trusted));
filtered = BluetoothService.dedupeDevices(filtered);
return BluetoothService.sortDevices(filtered);
}
model: pairedDevices
visible: pairedDevices.length > 0 && BluetoothService.adapter && BluetoothService.adapter.enabled
NBox {
id: pairedDevicesBox
visible: btprefs.pairedDevices.length > 0 && BluetoothService.adapter && BluetoothService.adapter.enabled
Layout.fillWidth: true
}
Layout.preferredHeight: pairedDevicesCol.implicitHeight + Style.marginXL
NDivider {
Layout.fillWidth: true
visible: pairedDevicesList.visible
}
ColumnLayout {
id: pairedDevicesCol
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
// Device List [3] (Ready to pair // discovered)
BluetoothDevicesList {
id: availableDevicesList
label: I18n.tr("bluetooth.panel.available-devices") + (BluetoothService.scanningActive ? " (" + I18n.tr("bluetooth.panel.scanning") + ")" : "") // I would prefered something animated here but as far as im aware there is no such thing.
headerMode: "filter"
property var availableDevices: {
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
return [];
var filtered = BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted);
// Optionally hide devices without a meaningful name when the filter is enabled
if (Settings.data && Settings.data.ui && Settings.data.network.bluetoothHideUnnamedDevices) {
filtered = filtered.filter(function (dev) {
// Extract device name
var dn = dev.name || dev.deviceName || "";
// 1) Hide empty or whitespace-only
var s = String(dn).trim();
if (s.length === 0)
return false;
// 2) Hide common placeholders
var lower = s.toLowerCase();
if (lower === "unknown" || lower === "unnamed" || lower === "n/a" || lower === "na") {
return false;
}
// 3) Hide if the name equals the device address (ignoring separators)
var addr = dev.address || dev.bdaddr || dev.mac || "";
if (addr.length > 0) {
var normName = s.toLowerCase().replace(/[^0-9a-z]/g, "");
var normAddr = String(addr).toLowerCase().replace(/[^0-9a-z]/g, "");
if (normName.length > 0 && normName === normAddr)
return false;
}
// 4) Hide address-like strings
// - Colon-separated hex: 00:11:22:33:44:55
var macColonHex = /^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$/;
// - Hyphen-separated hex: 00-11-22-33-44-55
var macHyphenHex = /^([0-9A-Fa-f]{2}-){5}[0-9A-Fa-f]{2}$/;
// - Hyphen-separated alnum pairs (to catch non-hex variants like AB-CD-EF-GH-01-23)
var macHyphenAny = /^([0-9A-Za-z]{2}-){5}[0-9A-Za-z]{2}$/;
// - Cisco dotted hex: 0011.2233.4455
var macDotted = /^[0-9A-Fa-f]{4}\.[0-9A-Fa-f]{4}\.[0-9A-Fa-f]{4}$/;
// - Bare hex: 001122334455
var macBare = /^[0-9A-Fa-f]{12}$/;
if (macColonHex.test(s) || macHyphenHex.test(s) || macHyphenAny.test(s) || macDotted.test(s) || macBare.test(s)) {
return false;
}
// Keep device otherwise (has a meaningful user-facing name)
return true;
});
NText {
text: I18n.tr("bluetooth.panel.paired-devices")
pointSize: Style.fontSizeS
color: Color.mSecondary
font.weight: Style.fontWeightBold
Layout.fillWidth: true
Layout.leftMargin: Style.marginS
}
Repeater {
model: btprefs.pairedDevices
delegate: nbox_delegate
}
filtered = BluetoothService.dedupeDevices(filtered);
return BluetoothService.sortDevices(filtered);
}
model: availableDevices
visible: availableDevices.length > 0 && BluetoothService.adapter && BluetoothService.adapter.enabled
Layout.fillWidth: true
}
NDivider {
Layout.fillWidth: true
visible: availableDevicesList.visible
visible: pairedDevicesBox.visible && !btprefs.showOnlyLists
}
// Device List [3] (Available)
NBox {
id: availableDevicesBox
visible: !btprefs.showOnlyLists && btprefs.availableDevices.length > 0 && BluetoothService.adapter && BluetoothService.adapter.enabled
Layout.fillWidth: true
Layout.preferredHeight: availableDevicesCol.implicitHeight + Style.marginXL
ColumnLayout {
id: availableDevicesCol
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: Style.marginS
spacing: Style.marginS
NText {
text: I18n.tr("bluetooth.panel.available-devices") + (BluetoothService.scanningActive ? " (" + I18n.tr("bluetooth.panel.scanning") + ")" : "")
pointSize: Style.fontSizeS
color: Color.mSecondary
font.weight: Style.fontWeightBold
Layout.fillWidth: true
}
NIconButton {
icon: (Settings.data && Settings.data.ui && Settings.data.network.bluetoothHideUnnamedDevices) ? "filter-off" : "filter"
tooltipText: (Settings.data && Settings.data.ui && Settings.data.network.bluetoothHideUnnamedDevices) ? I18n.tr("tooltips.hide-unnamed-devices") : I18n.tr("tooltips.show-all-devices")
onClicked: {
if (Settings.data && Settings.data.ui) {
Settings.data.network.bluetoothHideUnnamedDevices = !(Settings.data.network.bluetoothHideUnnamedDevices);
}
}
}
}
Repeater {
model: btprefs.availableDevices
delegate: nbox_delegate
}
}
}
NDivider {
Layout.fillWidth: true
visible: availableDevicesBox.visible && !btprefs.showOnlyLists
}
// RSSI Polling
NBox {
visible: !btprefs.showOnlyLists && BluetoothService.enabled
Layout.fillWidth: true
visible: BluetoothService.enabled
Layout.preferredHeight: rssiPollingColumn.implicitHeight + Style.marginL * 2
implicitHeight: Layout.preferredHeight
@@ -288,9 +355,294 @@ Item {
}
}
// Shared Delegate
Component {
id: nbox_delegate
NBox {
id: device
readonly property bool canConnect: BluetoothService.canConnect(modelData)
readonly property bool canDisconnect: BluetoothService.canDisconnect(modelData)
readonly property bool canPair: BluetoothService.canPair(modelData)
readonly property bool isBusy: BluetoothService.isDeviceBusy(modelData)
readonly property bool isExpanded: btprefs.expandedDeviceKey === BluetoothService.deviceKey(modelData)
function getContentColor(defaultColor = Color.mOnSurface) {
if (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting)
return Color.mPrimary;
if (modelData.blocked || modelData.state === BluetoothDeviceState.Disconnecting)
return Color.mError;
return defaultColor;
}
Layout.fillWidth: true
Layout.preferredHeight: deviceColumn.implicitHeight + (Style.marginXL)
radius: Style.radiusM
clip: true
color: (modelData.connected && modelData.state !== BluetoothDeviceState.Disconnecting) ? Qt.alpha(Color.mPrimary, 0.15) : Color.mSurface
ColumnLayout {
id: deviceColumn
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginS
RowLayout {
id: deviceLayout
Layout.fillWidth: true
spacing: Style.marginM
Layout.alignment: Qt.AlignVCenter
NIcon {
icon: BluetoothService.getDeviceIcon(modelData)
pointSize: Style.fontSizeXXL
color: modelData.connected ? Color.mPrimary : device.getContentColor(Color.mOnSurface)
Layout.alignment: Qt.AlignVCenter
}
ColumnLayout {
Layout.fillWidth: true
spacing: Style.marginXXS
NText {
text: modelData.name || modelData.deviceName
pointSize: Style.fontSizeM
font.weight: modelData.connected ? Style.fontWeightBold : Style.fontWeightMedium
elide: Text.ElideRight
color: device.getContentColor(Color.mOnSurface)
Layout.fillWidth: true
}
NText {
text: {
const k = BluetoothService.getStatusKey(modelData);
if (k === "pairing")
return I18n.tr("common.pairing");
if (k === "blocked")
return I18n.tr("bluetooth.panel.blocked");
if (k === "connecting")
return I18n.tr("common.connecting");
if (k === "disconnecting")
return I18n.tr("common.disconnecting");
return "";
}
visible: text !== ""
pointSize: Style.fontSizeXS
color: device.getContentColor(Color.mOnSurfaceVariant)
}
RowLayout {
visible: modelData.batteryAvailable
spacing: Style.marginS
NIcon {
icon: {
var b = BluetoothService.getBatteryPercent(modelData);
return BatteryService.getIcon(b !== null ? b : 0, false, false, b !== null);
}
pointSize: Style.fontSizeXS
color: device.getContentColor(Color.mOnSurface)
}
NText {
text: {
var b = BluetoothService.getBatteryPercent(modelData);
return b === null ? "-" : (b + "%");
}
pointSize: Style.fontSizeXS
color: device.getContentColor(Color.mOnSurfaceVariant)
}
}
}
Item {
Layout.fillWidth: true
}
RowLayout {
spacing: Style.marginS
NIconButton {
visible: modelData.connected
icon: "info"
tooltipText: I18n.tr("common.info")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
const key = BluetoothService.deviceKey(modelData);
btprefs.expandedDeviceKey = (btprefs.expandedDeviceKey === key) ? "" : key;
}
}
NIconButton {
visible: !btprefs.showOnlyLists && (modelData.paired || modelData.trusted) && !modelData.connected && !isBusy && !modelData.blocked
icon: "trash"
tooltipText: I18n.tr("common.unpair")
baseSize: Style.baseWidgetSize * 0.8
onClicked: BluetoothService.unpairDevice(modelData)
}
NButton {
id: button
visible: (modelData.state !== BluetoothDeviceState.Connecting)
enabled: (canConnect || canDisconnect || (btprefs.showOnlyLists ? false : canPair)) && !isBusy
outlined: !button.hovered
fontSize: Style.fontSizeS
backgroundColor: modelData.connected ? Color.mError : Color.mPrimary
text: {
if (modelData.pairing)
return I18n.tr("common.pairing");
if (modelData.blocked)
return I18n.tr("bluetooth.panel.blocked");
if (modelData.connected)
return I18n.tr("common.disconnect");
if (!btprefs.showOnlyLists && device.canPair)
return I18n.tr("common.pair");
return I18n.tr("common.connect");
}
icon: (isBusy ? "busy" : null)
onClicked: {
if (modelData.connected) {
BluetoothService.disconnectDevice(modelData);
} else {
if (!btprefs.showOnlyLists && device.canPair) {
BluetoothService.pairDevice(modelData);
} else {
BluetoothService.connectDeviceWithTrust(modelData);
}
}
}
}
}
}
// Expanded info section
Rectangle {
visible: device.isExpanded
Layout.fillWidth: true
implicitHeight: infoColumn.implicitHeight + Style.marginS * 2
radius: Style.radiusS
color: Color.mSurfaceVariant
border.width: Style.borderS
border.color: Color.mOutline
clip: true
NIconButton {
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginS
icon: btprefs.detailsGrid ? "layout-list" : "layout-grid"
tooltipText: btprefs.detailsGrid ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
btprefs.detailsGrid = !btprefs.detailsGrid;
if (Settings.data && Settings.data.ui) {
Settings.data.network.bluetoothDetailsViewMode = btprefs.detailsGrid ? "grid" : "list";
}
}
z: 1
}
GridLayout {
id: infoColumn
anchors.fill: parent
anchors.margins: Style.marginS
columns: btprefs.detailsGrid ? 2 : 1
columnSpacing: Style.marginM
rowSpacing: Style.marginXS
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
spacing: Style.marginXS
NIcon {
icon: BluetoothService.getSignalIcon(modelData)
pointSize: Style.fontSizeXS
color: Color.mOnSurface
}
NText {
text: BluetoothService.getSignalStrength(modelData)
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
spacing: Style.marginXS
NIcon {
icon: {
var b = BluetoothService.getBatteryPercent(modelData);
return BatteryService.getIcon(b !== null ? b : 0, false, false, b !== null);
}
pointSize: Style.fontSizeXS
color: Color.mOnSurface
}
NText {
text: {
var b = BluetoothService.getBatteryPercent(modelData);
return b === null ? "-" : (b + "%");
}
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginXS
NIcon {
icon: "link"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
}
NText {
text: modelData.paired ? I18n.tr("common.yes") : I18n.tr("common.no")
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginXS
NIcon {
icon: "shield-check"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
}
NText {
text: modelData.trusted ? I18n.tr("common.yes") : I18n.tr("common.no")
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
}
}
RowLayout {
Layout.fillWidth: true
Layout.columnSpan: infoColumn.columns === 2 ? 2 : 1
spacing: Style.marginXS
NIcon {
icon: "hash"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
}
NText {
text: modelData.address || "-"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
}
}
}
}
}
}
}
// PIN Authentication Overlay
Rectangle {
id: pinOverlay
visible: !btprefs.showOnlyLists && BluetoothService.pinRequired
anchors.centerIn: parent
width: Math.min(parent.width * 0.9, 400)
height: pinCol.implicitHeight + Style.marginL * 2
@@ -298,7 +650,6 @@ Item {
radius: Style.radiusM
border.color: Style.boxBorderColor
border.width: Style.borderS
visible: BluetoothService.pinRequired
z: 1000
MouseArea {
@@ -320,25 +671,22 @@ Item {
color: Color.mPrimary
Layout.alignment: Qt.AlignHCenter
}
NText {
text: I18n.tr("common.authentication-required") // TODO: missing: i18n
text: I18n.tr("common.authentication-required")
pointSize: Style.fontSizeXL
font.weight: Style.fontWeightBold
color: Color.mOnSurface
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
}
NText {
text: I18n.tr("bluetooth.panel.pin-instructions") // TODO: missing: i18n
text: I18n.tr("bluetooth.panel.pin-instructions")
pointSize: Style.fontSizeM
color: Color.mOnSurfaceVariant
wrapMode: Text.WordWrap
horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true
}
NTextInput {
id: pinInput
Layout.fillWidth: true
@@ -357,19 +705,16 @@ Item {
}
}
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
spacing: Style.marginM
NButton {
text: I18n.tr("common.cancel")
icon: "x"
onClicked: BluetoothService.cancelPairing()
}
NButton {
text: I18n.tr("common.confirm") // TODO: i18n
text: I18n.tr("common.confirm")
icon: "check"
backgroundColor: Color.mPrimary
textColor: Color.mOnPrimary
+1 -1
View File
@@ -2,9 +2,9 @@ import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import qs.Commons
import qs.Widgets
import qs.Modules.Panels.Settings.Tabs.Connections
import qs.Services.Networking
import qs.Widgets
ColumnLayout {
id: root
+2 -26
View File
@@ -258,9 +258,7 @@ Singleton {
}
var ms = text.match(/\bDiscovering:\s*(yes|no)\b/i);
if (ms && ms.length > 1) {
var discovering = (ms[1].toLowerCase() === "yes");
Logger.d("Bluetooth", "Parsed Discovering state from bluetoothctl: " + discovering + " (current ctlDiscovering: " + root.ctlDiscovering + ")");
root.ctlDiscovering = discovering;
root.ctlDiscovering = (ms[1].toLowerCase() === "yes");
}
} catch (e) {
Logger.d("Bluetooth", "Failed to parse bluetoothctl show output", e);
@@ -319,11 +317,6 @@ Singleton {
btExec(["bluetoothctl", "discoverable", state ? "on" : "off"]);
root.ctlDiscoverable = !!state; // optimistic
requestCtlPoll(ctlPollSoonMs);
if (state) {
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.discoverable-enabled"), "broadcast");
} else {
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.discoverable-disabled"), "broadcast-off");
}
Logger.i("Bluetooth", "Discoverable state set to:", state);
} catch (e) {
Logger.w("Bluetooth", "Failed to change discoverable state", e);
@@ -381,23 +374,6 @@ Singleton {
}
return device.connected && !device.pairing && !device.blocked;
}
// Status string for a device (translated)
function getStatusString(device) {
if (!device) {
return "";
}
try {
if (device.pairing)
return I18n.tr("common.pairing");
if (device.blocked)
return I18n.tr("bluetooth.panel.blocked");
if (device.state === BluetoothDevice.Connecting)
return I18n.tr("common.connecting");
if (device.state === BluetoothDevice.Disconnecting)
return I18n.tr("common.disconnecting");
} catch (_) {}
return "";
}
// Textual signal quality (translated)
function getSignalStrength(device) {
@@ -620,4 +596,4 @@ Singleton {
ToastService.showWarning(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.forget-failed"));
}
}
}
}
+1 -1
View File
@@ -1365,4 +1365,4 @@ Singleton {
}
}
}
}
}