mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
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:
@@ -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 Wi‑Fi, then Unpair, then main CTA)
|
||||
RowLayout {
|
||||
spacing: Style.marginS
|
||||
|
||||
// Info for connected device (placed before the CTA for consistency with Wi‑Fi)
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1365,4 +1365,4 @@ Singleton {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user