mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge pull request #1743 from turannul/pr/bluetooth-refactor-pt1
Bluetooth Panel Rework pt1
This commit is contained in:
@@ -400,6 +400,7 @@
|
||||
"edit": "Edit",
|
||||
"enabled": "Enabled",
|
||||
"error": "Error",
|
||||
"ethernet": "Ethernet",
|
||||
"events": "Events",
|
||||
"execute": "Execute",
|
||||
"faithful": "Faithful",
|
||||
@@ -507,8 +508,11 @@
|
||||
"week": "Week",
|
||||
"widgets": "Widgets",
|
||||
"width": "Width",
|
||||
"wifi": "Wi-Fi",
|
||||
"windows": "Windows",
|
||||
"yes": "Yes"
|
||||
"yes": "Yes",
|
||||
"confirm": "Confirm",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"control-center": {
|
||||
"power-profile": {
|
||||
@@ -1235,12 +1239,23 @@
|
||||
"show-session-buttons-label": "Power controls",
|
||||
"title": "Lock Screen"
|
||||
},
|
||||
"network": {
|
||||
"connections": {
|
||||
"authentication-required": "Authentication required",
|
||||
"bluetooth-description": "Activate Bluetooth management.",
|
||||
"bluetooth-devices-unnamed": "Unnamed devices are not shown.",
|
||||
"bluetooth-discoverable": "This device is discoverable as <b> {hostName} </b> while this settings tab is open.",
|
||||
"bluetooth-rssi-polling-description": "Periodically sample RSSI for connected devices via bluetoothctl. May not be available for all devices; uses minimal resources when enabled.",
|
||||
"bluetooth-rssi-polling-interval-description": "Configure how often to update signal strength for connected devices.",
|
||||
"bluetooth-rssi-polling-label": "Bluetooth signal polling",
|
||||
"desc": "Manage Wi-Fi and Bluetooth connections.",
|
||||
"bluetooth-rssi-polling-interval-label": "Polling interval",
|
||||
"disable-discoverability-label": "Disable device visibility",
|
||||
"disable-discoverability-description": "Hide your device from nearby Bluetooth devices.",
|
||||
"hide-unnamed-devices-label": "Hide unnamed devices",
|
||||
"hide-unnamed-devices-description": "Hide devices that appear only as Bluetooth addresses.",
|
||||
"pin-instructions": "Please enter the PIN code displayed on your device.",
|
||||
"title": "Connections",
|
||||
"wifi-description": "Manage wireless networks (requires NetworkManager)."
|
||||
|
||||
},
|
||||
"notifications": {
|
||||
"duration-critical-urgency-description": "How long critical priority notifications stay visible.",
|
||||
@@ -1746,14 +1761,12 @@
|
||||
"forget-network": "Forget network",
|
||||
"grid-view": "Grid view",
|
||||
"hidden-files-hide": "Hidden files",
|
||||
"hide-unnamed-devices": "Hide unnamed devices",
|
||||
"home": "Home directory",
|
||||
"input-muted": "Toggle input mute",
|
||||
"keep-awake": "Keep Awake",
|
||||
"keyboard-layout": "{layout} keyboard layout",
|
||||
"list-view": "List view",
|
||||
"manage-vpn": "VPN connections",
|
||||
"manage-wifi": "Wi-Fi",
|
||||
"max-widgets-reached": "Maximum widgets reached",
|
||||
"microphone-volume-at": "Microphone volume: {volume}%",
|
||||
"move-to-section": "Move to {section}",
|
||||
|
||||
@@ -335,11 +335,13 @@
|
||||
},
|
||||
"network": {
|
||||
"wifiEnabled": true,
|
||||
"airplaneModeEnabled": false,
|
||||
"bluetoothRssiPollingEnabled": false,
|
||||
"bluetoothRssiPollIntervalMs": 10000,
|
||||
"bluetoothRssiPollIntervalMs": 60000,
|
||||
"wifiDetailsViewMode": "grid",
|
||||
"bluetoothDetailsViewMode": "grid",
|
||||
"bluetoothHideUnnamedDevices": false
|
||||
"bluetoothHideUnnamedDevices": false,
|
||||
"disableDiscoverability": false
|
||||
},
|
||||
"sessionMenu": {
|
||||
"enableCountdown": true,
|
||||
|
||||
@@ -1038,27 +1038,48 @@
|
||||
},
|
||||
{
|
||||
"labelKey": "actions.enable-wifi",
|
||||
"descriptionKey": "panels.network.wifi-description",
|
||||
"descriptionKey": "panels.connections.wifi-description",
|
||||
"widget": "NToggle",
|
||||
"tab": 15,
|
||||
"tabLabel": "common.network",
|
||||
"subTab": null
|
||||
"tabLabel": "panels.connections.title",
|
||||
"subTab": 0,
|
||||
"subTabLabel": "common.wifi"
|
||||
},
|
||||
{
|
||||
"labelKey": "actions.enable-bluetooth",
|
||||
"descriptionKey": "panels.network.bluetooth-description",
|
||||
"descriptionKey": "panels.connections.bluetooth-description",
|
||||
"widget": "NToggle",
|
||||
"tab": 15,
|
||||
"tabLabel": "common.network",
|
||||
"subTab": null
|
||||
"tabLabel": "panels.connections.title",
|
||||
"subTab": 1,
|
||||
"subTabLabel": "common.bluetooth"
|
||||
},
|
||||
{
|
||||
"labelKey": "panels.network.bluetooth-rssi-polling-label",
|
||||
"descriptionKey": "panels.network.bluetooth-rssi-polling-description",
|
||||
"labelKey": "panels.connections.hide-unnamed-devices-label",
|
||||
"descriptionKey": "panels.connections.hide-unnamed-devices-description",
|
||||
"widget": "NToggle",
|
||||
"tab": 15,
|
||||
"tabLabel": "common.network",
|
||||
"subTab": null
|
||||
"tabLabel": "panels.connections.title",
|
||||
"subTab": 1,
|
||||
"subTabLabel": "common.bluetooth"
|
||||
},
|
||||
{
|
||||
"labelKey": "panels.connections.disable-discoverability-label",
|
||||
"descriptionKey": "panels.connections.disable-discoverability-description",
|
||||
"widget": "NToggle",
|
||||
"tab": 15,
|
||||
"tabLabel": "panels.connections.title",
|
||||
"subTab": 1,
|
||||
"subTabLabel": "common.bluetooth"
|
||||
},
|
||||
{
|
||||
"labelKey": "panels.connections.bluetooth-rssi-polling-label",
|
||||
"descriptionKey": "panels.connections.bluetooth-rssi-polling-description",
|
||||
"widget": "NToggle",
|
||||
"tab": 15,
|
||||
"tabLabel": "panels.connections.title",
|
||||
"subTab": 1,
|
||||
"subTabLabel": "common.bluetooth"
|
||||
},
|
||||
{
|
||||
"labelKey": "panels.notifications.duration-respect-expire-label",
|
||||
|
||||
@@ -118,7 +118,7 @@ Singleton {
|
||||
"settings-launcher": "rocket",
|
||||
"settings-audio": "device-speaker",
|
||||
"settings-display": "device-desktop",
|
||||
"settings-network": "sitemap",
|
||||
"settings-network": "circles-relation",
|
||||
"settings-brightness": "brightness-up",
|
||||
"settings-location": "world-pin",
|
||||
"settings-color-scheme": "palette",
|
||||
|
||||
@@ -539,11 +539,13 @@ Singleton {
|
||||
// network
|
||||
property JsonObject network: JsonObject {
|
||||
property bool wifiEnabled: true
|
||||
property bool airplaneModeEnabled: false
|
||||
property bool bluetoothRssiPollingEnabled: false // Opt-in Bluetooth RSSI polling (uses bluetoothctl)
|
||||
property int bluetoothRssiPollIntervalMs: 10000 // Polling interval in milliseconds for RSSI queries
|
||||
property int bluetoothRssiPollIntervalMs: 60000 // Polling interval in milliseconds for RSSI queries
|
||||
property string wifiDetailsViewMode: "grid" // "grid" or "list"
|
||||
property string bluetoothDetailsViewMode: "grid" // "grid" or "list"
|
||||
property bool bluetoothHideUnnamedDevices: false
|
||||
property bool disableDiscoverability: false
|
||||
}
|
||||
|
||||
// session menu
|
||||
|
||||
@@ -3,6 +3,7 @@ import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Panels.Settings // For SettingsPanel
|
||||
import qs.Services.Networking
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
@@ -47,7 +48,13 @@ Item {
|
||||
{
|
||||
"label": BluetoothService.enabled ? I18n.tr("actions.disable-bluetooth") : I18n.tr("actions.enable-bluetooth"),
|
||||
"action": "toggle-bluetooth",
|
||||
"icon": BluetoothService.enabled ? "bluetooth-off" : "bluetooth"
|
||||
"icon": BluetoothService.enabled ? "bluetooth-off" : "bluetooth",
|
||||
"enabled": !Settings.data.network.airplaneModeEnabled && BluetoothService.bluetoothAvailable
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("common.bluetooth") + " " + I18n.tr("tooltips.open-settings"),
|
||||
"action": "bluetooth-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
@@ -62,6 +69,8 @@ Item {
|
||||
|
||||
if (action === "toggle-bluetooth") {
|
||||
BluetoothService.setBluetoothEnabled(!BluetoothService.enabled);
|
||||
} else if (action === "bluetooth-settings") {
|
||||
SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 1, screen);
|
||||
} else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Modules.Bar.Extras
|
||||
import qs.Modules.Panels.Settings // For SettingsPanel
|
||||
import qs.Services.Networking
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
@@ -47,7 +48,13 @@ Item {
|
||||
{
|
||||
"label": Settings.data.network.wifiEnabled ? I18n.tr("actions.disable-wifi") : I18n.tr("actions.enable-wifi"),
|
||||
"action": "toggle-wifi",
|
||||
"icon": Settings.data.network.wifiEnabled ? "wifi-off" : "wifi"
|
||||
"icon": Settings.data.network.wifiEnabled ? "wifi-off" : "wifi",
|
||||
"enabled": !Settings.data.network.airplaneModeEnabled && NetworkService.wifiAvailable
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("common.wifi") + " " + I18n.tr("tooltips.open-settings"),
|
||||
"action": "wifi-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
@@ -62,7 +69,9 @@ Item {
|
||||
|
||||
if (action === "toggle-wifi") {
|
||||
NetworkService.setWifiEnabled(!Settings.data.network.wifiEnabled);
|
||||
} else if (action === "widget-settings") {
|
||||
} else if (action === "wifi-settings") {
|
||||
SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 0, screen);
|
||||
}else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
@@ -133,7 +142,7 @@ Item {
|
||||
else if (NetworkService.activeEthernetIf && NetworkService.activeEthernetIf.length > 0)
|
||||
base = NetworkService.activeEthernetIf;
|
||||
else
|
||||
base = I18n.tr("control-center.wifi.label-ethernet");
|
||||
base = I18n.tr("common.ethernet");
|
||||
const speed = (d.speed && d.speed.length > 0) ? d.speed : "";
|
||||
return speed ? (base + " — " + speed) : base;
|
||||
}
|
||||
@@ -146,7 +155,7 @@ Item {
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
return I18n.tr("tooltips.manage-wifi");
|
||||
return I18n.tr("common.wifi");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,8 +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
|
||||
import qs.Services.Hardware
|
||||
import qs.Services.Networking
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
@@ -55,29 +58,11 @@ SmartPanel {
|
||||
NToggle {
|
||||
id: bluetoothSwitch
|
||||
checked: BluetoothService.enabled
|
||||
enabled: !Settings.data.network.airplaneModeEnabled && BluetoothService.bluetoothAvailable
|
||||
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
|
||||
baseSize: Style.baseWidgetSize * 0.65
|
||||
}
|
||||
|
||||
// Discoverability toggle (advertising)
|
||||
NIconButton {
|
||||
enabled: BluetoothService.enabled
|
||||
icon: BluetoothService.discoverable ? "broadcast" : "broadcast-off"
|
||||
tooltipText: I18n.tr("bluetooth.panel.discoverable")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: {
|
||||
BluetoothService.setDiscoverable(!BluetoothService.discoverable);
|
||||
}
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
enabled: BluetoothService.enabled
|
||||
icon: BluetoothService.scanningActive ? "stop" : "refresh"
|
||||
tooltipText: I18n.tr("tooltips.refresh-devices")
|
||||
baseSize: Style.baseWidgetSize * 0.8
|
||||
onClicked: BluetoothService.toggleDiscovery()
|
||||
}
|
||||
|
||||
NIconButton {
|
||||
icon: "close"
|
||||
tooltipText: I18n.tr("common.close")
|
||||
@@ -106,11 +91,10 @@ SmartPanel {
|
||||
// Adapter not available of disabled
|
||||
NBox {
|
||||
id: disabledBox
|
||||
visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled)
|
||||
visible: !BluetoothService.enabled
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: disabledColumn.implicitHeight + Style.marginXL
|
||||
|
||||
// Center the content within this rectangle
|
||||
ColumnLayout {
|
||||
id: disabledColumn
|
||||
anchors.fill: parent
|
||||
@@ -150,17 +134,14 @@ SmartPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state when no devices
|
||||
// Empty state when no paired devices
|
||||
NBox {
|
||||
id: emptyBox
|
||||
visible: {
|
||||
if (!(BluetoothService.adapter && BluetoothService.adapter.enabled && BluetoothService.adapter.devices) || BluetoothService.scanningActive)
|
||||
if (!BluetoothService.enabled || !BluetoothService.devices)
|
||||
return false;
|
||||
|
||||
var availableCount = BluetoothService.adapter.devices.values.filter(dev => {
|
||||
return dev && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
|
||||
}).length;
|
||||
return (availableCount === 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
|
||||
@@ -190,287 +171,26 @@ SmartPanel {
|
||||
}
|
||||
|
||||
NButton {
|
||||
text: I18n.tr("tooltips.refresh-devices")
|
||||
icon: "refresh"
|
||||
text: I18n.tr("common.settings")
|
||||
icon: "settings"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: {
|
||||
BluetoothService.toggleDiscovery();
|
||||
SettingsPanel.openToTab(SettingsPanel.Tab.Bluetooth);
|
||||
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
|
||||
}
|
||||
|
||||
// Available devices (for pairing)
|
||||
BluetoothDevicesList {
|
||||
label: I18n.tr("bluetooth.panel.available-devices")
|
||||
headerMode: "filter"
|
||||
property var items: {
|
||||
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 display name
|
||||
var dn = "";
|
||||
if (dev && dev.name)
|
||||
dn = dev.name;
|
||||
else if (dev && dev.deviceName)
|
||||
dn = dev.deviceName;
|
||||
else
|
||||
dn = "";
|
||||
if (dn === undefined || dn === null)
|
||||
dn = "";
|
||||
var s = String(dn).trim();
|
||||
|
||||
// 1) Hide empty or whitespace-only
|
||||
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 = "";
|
||||
if (dev && dev.address)
|
||||
addr = String(dev.address);
|
||||
else if (dev && dev.bdaddr)
|
||||
addr = String(dev.bdaddr);
|
||||
else if (dev && dev.mac)
|
||||
addr = String(dev.mac);
|
||||
if (addr && addr.length > 0) {
|
||||
var normName = s.toLowerCase().replace(/[^0-9a-z]/g, "");
|
||||
var normAddr = 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}$/;
|
||||
if (macColonHex.test(s))
|
||||
return false;
|
||||
// - Hyphen-separated hex: 00-11-22-33-44-55
|
||||
var macHyphenHex = /^([0-9A-Fa-f]{2}-){5}[0-9A-Fa-f]{2}$/;
|
||||
if (macHyphenHex.test(s))
|
||||
return false;
|
||||
// - 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}$/;
|
||||
if (macHyphenAny.test(s))
|
||||
return false;
|
||||
// - Cisco dotted hex: 0011.2233.4455
|
||||
var macDotted = /^[0-9A-Fa-f]{4}\.[0-9A-Fa-f]{4}\.[0-9A-Fa-f]{4}$/;
|
||||
if (macDotted.test(s))
|
||||
return false;
|
||||
// - Bare hex: 001122334455
|
||||
var macBare = /^[0-9A-Fa-f]{12}$/;
|
||||
if (macBare.test(s))
|
||||
return false;
|
||||
|
||||
// Keep device otherwise (has a meaningful user-facing name)
|
||||
return true;
|
||||
});
|
||||
}
|
||||
filtered = BluetoothService.dedupeDevices(filtered);
|
||||
return BluetoothService.sortDevices(filtered);
|
||||
}
|
||||
model: items
|
||||
visible: items.length > 0 && BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Fallback - No devices, scanning
|
||||
NBox {
|
||||
id: scanningBox
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: scanningColumn.implicitHeight + Style.marginXL
|
||||
visible: {
|
||||
if (!(BluetoothService.adapter && BluetoothService.adapter.enabled && BluetoothService.adapter.devices) || !BluetoothService.scanningActive) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var availableCount = BluetoothService.adapter.devices.values.filter(dev => {
|
||||
return dev && !dev.paired && !dev.pairing && !dev.blocked && (dev.signalStrength === undefined || dev.signalStrength > 0);
|
||||
}).length;
|
||||
return (availableCount === 0);
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: scanningColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginM
|
||||
spacing: Style.marginL
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: Style.marginXS
|
||||
|
||||
NIcon {
|
||||
icon: "refresh"
|
||||
pointSize: Style.fontSizeXXL * 1.5
|
||||
color: Color.mPrimary
|
||||
|
||||
RotationAnimation on rotation {
|
||||
running: true
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 360
|
||||
duration: Style.animationSlow * 4
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("bluetooth.panel.scanning")
|
||||
pointSize: Style.fontSizeL
|
||||
color: Color.mOnSurface
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("bluetooth.panel.pairing-mode")
|
||||
pointSize: Style.fontSizeM
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,5 +15,9 @@ NIconButtonHot {
|
||||
if (p)
|
||||
p.toggle(this);
|
||||
}
|
||||
onRightClicked: BluetoothService.setBluetoothEnabled(!BluetoothService.enabled)
|
||||
onRightClicked: {
|
||||
if (!Settings.data.network.airplaneModeEnabled) {
|
||||
BluetoothService.setBluetoothEnabled(!BluetoothService.enabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ NIconButtonHot {
|
||||
try {
|
||||
if (NetworkService.ethernetConnected) {
|
||||
// Match design: fixed label when on Ethernet
|
||||
return I18n.tr("control-center.wifi.label-ethernet");
|
||||
return I18n.tr("common.ethernet");
|
||||
}
|
||||
// Wi‑Fi: SSID — link speed (if available)
|
||||
for (const net in NetworkService.networks) {
|
||||
@@ -46,11 +46,15 @@ NIconButtonHot {
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
return I18n.tr("tooltips.manage-wifi");
|
||||
return I18n.tr("common.wifi");
|
||||
}
|
||||
onClicked: {
|
||||
var panel = PanelService.getPanel("networkPanel", screen);
|
||||
panel?.toggle(this);
|
||||
}
|
||||
onRightClicked: NetworkService.setWifiEnabled(!Settings.data.network.wifiEnabled)
|
||||
onRightClicked: {
|
||||
if (!Settings.data.network.airplaneModeEnabled) {
|
||||
NetworkService.setWifiEnabled(!Settings.data.network.wifiEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,13 +140,13 @@ SmartPanel {
|
||||
panelViewMode = "wifi";
|
||||
}
|
||||
}
|
||||
onEntered: TooltipService.show(parent, panelViewMode === "wifi" ? I18n.tr("control-center.wifi.label-ethernet") : I18n.tr("wifi.panel.title"))
|
||||
onEntered: TooltipService.show(parent, panelViewMode === "wifi" ? I18n.tr("common.ethernet") : I18n.tr("common.wifi"))
|
||||
onExited: TooltipService.hide()
|
||||
}
|
||||
}
|
||||
|
||||
NText {
|
||||
text: panelViewMode === "wifi" ? I18n.tr("wifi.panel.title") : I18n.tr("control-center.wifi.label-ethernet")
|
||||
text: panelViewMode === "wifi" ? I18n.tr("common.wifi") : I18n.tr("common.ethernet")
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
@@ -157,6 +157,7 @@ SmartPanel {
|
||||
id: wifiSwitch
|
||||
visible: panelViewMode === "wifi"
|
||||
checked: Settings.data.network.wifiEnabled
|
||||
enabled: !Settings.data.network.airplaneModeEnabled && NetworkService.wifiAvailable
|
||||
onToggled: checked => NetworkService.setWifiEnabled(checked)
|
||||
baseSize: Style.baseWidgetSize * 0.7 // Slightly smaller
|
||||
}
|
||||
@@ -198,13 +199,13 @@ SmartPanel {
|
||||
}
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("tooltips.manage-wifi")
|
||||
text: I18n.tr("common.wifi")
|
||||
tabIndex: 0
|
||||
checked: modeTabBar.currentIndex === 0
|
||||
}
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("control-center.wifi.label-ethernet")
|
||||
text: I18n.tr("common.ethernet")
|
||||
tabIndex: 1
|
||||
checked: modeTabBar.currentIndex === 1
|
||||
}
|
||||
@@ -707,7 +708,7 @@ SmartPanel {
|
||||
const value = (NetworkService.activeEthernetDetails.ifname && NetworkService.activeEthernetDetails.ifname.length > 0) ? NetworkService.activeEthernetDetails.ifname : (NetworkService.activeEthernetIf || "");
|
||||
if (value.length > 0) {
|
||||
Quickshell.execDetached(["wl-copy", value]);
|
||||
ToastService.showNotice(I18n.tr("control-center.wifi.label-ethernet"), I18n.tr("toast.bluetooth.address-copied"), "ethernet");
|
||||
ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("toast.bluetooth.address-copied"), "ethernet");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -859,7 +860,7 @@ SmartPanel {
|
||||
const value = NetworkService.activeEthernetDetails.ipv4 || "";
|
||||
if (value.length > 0) {
|
||||
Quickshell.execDetached(["wl-copy", value]);
|
||||
ToastService.showNotice(I18n.tr("control-center.wifi.label-ethernet"), I18n.tr("toast.bluetooth.address-copied"), "ethernet");
|
||||
ToastService.showNotice(I18n.tr("common.ethernet"), I18n.tr("toast.bluetooth.address-copied"), "ethernet");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +402,7 @@ NBox {
|
||||
const value = NetworkService.activeWifiIf || "";
|
||||
if (value.length > 0) {
|
||||
Quickshell.execDetached(["wl-copy", value]);
|
||||
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("toast.bluetooth.address-copied"), "wifi");
|
||||
ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.bluetooth.address-copied"), "wifi");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -545,7 +545,7 @@ NBox {
|
||||
const value = NetworkService.activeWifiDetails.ipv4 || "";
|
||||
if (value.length > 0) {
|
||||
Quickshell.execDetached(["wl-copy", value]);
|
||||
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("toast.bluetooth.address-copied"), "wifi");
|
||||
ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.bluetooth.address-copied"), "wifi");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,8 +422,8 @@ Item {
|
||||
OsdTab {}
|
||||
}
|
||||
Component {
|
||||
id: networkTab
|
||||
NetworkTab {}
|
||||
id: connectionsTab
|
||||
ConnectionsTab {}
|
||||
}
|
||||
Component {
|
||||
id: regionTab
|
||||
@@ -575,10 +575,10 @@ Item {
|
||||
"source": displayTab
|
||||
},
|
||||
{
|
||||
"id": SettingsPanel.Tab.Network,
|
||||
"label": "common.network",
|
||||
"id": SettingsPanel.Tab.Connections,
|
||||
"label": "panels.connections.title",
|
||||
"icon": "settings-network",
|
||||
"source": networkTab
|
||||
"source": connectionsTab
|
||||
},
|
||||
{
|
||||
"id": SettingsPanel.Tab.Location,
|
||||
|
||||
@@ -84,7 +84,7 @@ SmartPanel {
|
||||
Hooks,
|
||||
Launcher,
|
||||
Location,
|
||||
Network,
|
||||
Connections,
|
||||
Notifications,
|
||||
Plugins,
|
||||
SessionMenu,
|
||||
|
||||
@@ -0,0 +1,734 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
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
|
||||
|
||||
Item {
|
||||
id: btprefs
|
||||
Layout.fillWidth: true
|
||||
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 unnamedAvailableDevices: {
|
||||
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
||||
return [];
|
||||
return BluetoothService.adapter.devices.values.filter(dev => dev && !dev.blocked && !dev.paired && !dev.trusted);
|
||||
}
|
||||
|
||||
readonly property var availableDevices: {
|
||||
var list = btprefs.unnamedAvailableDevices;
|
||||
|
||||
if (Settings.data && Settings.data.ui && Settings.data.network.bluetoothHideUnnamedDevices) {
|
||||
list = list.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 macRegexComb = /^(([0-9A-Fa-f]{2}[:\-]){5}[0-9A-Fa-f]{2}|([0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4}|[0-9A-Fa-f]{12})$/;
|
||||
if (macRegexComb.test(s)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
list = BluetoothService.dedupeDevices(list);
|
||||
return BluetoothService.sortDevices(list);
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
Connections {
|
||||
target: BluetoothService
|
||||
function onEnabledChanged() {
|
||||
stateChangeDebouncer.restart();
|
||||
}
|
||||
function onDiscoverableChanged() {
|
||||
stateChangeDebouncer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
onEffectivelyVisibleChanged: stateChangeDebouncer.restart()
|
||||
|
||||
Timer {
|
||||
id: stateChangeDebouncer
|
||||
interval: 100 // 100ms debounce
|
||||
repeat: false
|
||||
onTriggered: btprefs._updateScanningState()
|
||||
}
|
||||
|
||||
function _updateScanningState() {
|
||||
if (effectivelyVisible && BluetoothService.enabled && !showOnlyLists) {
|
||||
Logger.d("BluetoothPrefs", "Panel/tab active");
|
||||
if (!isScanningActive) {
|
||||
BluetoothService.setScanActive(true);
|
||||
}
|
||||
if (!Settings.data.network.disableDiscoverability && !isDiscoverable) {
|
||||
BluetoothService.setDiscoverable(true);
|
||||
}
|
||||
} else {
|
||||
Logger.d("BluetoothPrefs", "Panel/tab inactive");
|
||||
if (isScanningActive) {
|
||||
BluetoothService.setScanActive(false);
|
||||
}
|
||||
if (isDiscoverable) {
|
||||
BluetoothService.setDiscoverable(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
// Ensure scanning is stopped when component is closed
|
||||
if (isScanningActive) {
|
||||
BluetoothService.setScanActive(false);
|
||||
}
|
||||
// Ensure discoverable is disabled when component is closed
|
||||
if (isDiscoverable) {
|
||||
BluetoothService.setDiscoverable(false);
|
||||
}
|
||||
Logger.d("BluetoothPrefs", "Panel closed");
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Style.marginL
|
||||
|
||||
// Master Control Section
|
||||
NBox {
|
||||
visible: !btprefs.showOnlyLists
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: masterControlCol.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: masterControlCol
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: BluetoothService.enabled ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("common.bluetooth")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
checked: BluetoothService.enabled
|
||||
enabled: !Settings.data.network.airplaneModeEnabled && BluetoothService.bluetoothAvailable
|
||||
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
visible: BluetoothService.enabled
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("panels.connections.bluetooth-discoverable", {hostName: HostService.hostName})
|
||||
visible: (BluetoothService.enabled && isDiscoverable)
|
||||
richTextEnabled: true
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: !showOnlyLists
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Device List [1] (Connected)
|
||||
NBox {
|
||||
id: connectedDevicesBox
|
||||
visible: btprefs.connectedDevices.length > 0 && BluetoothService.enabled
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: connectedDevicesCol.implicitHeight + Style.marginXL
|
||||
|
||||
ColumnLayout {
|
||||
id: connectedDevicesCol
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Style.marginM
|
||||
anchors.bottomMargin: Style.marginM
|
||||
anchors.leftMargin: showOnlyLists ? Style.marginM : 0
|
||||
anchors.rightMargin: showOnlyLists ? Style.marginM : 0
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Devices List [2] (Paired)
|
||||
NBox {
|
||||
id: pairedDevicesBox
|
||||
visible: btprefs.pairedDevices.length > 0 && BluetoothService.enabled
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: pairedDevicesCol.implicitHeight + Style.marginXL
|
||||
|
||||
ColumnLayout {
|
||||
id: pairedDevicesCol
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Style.marginM
|
||||
anchors.bottomMargin: Style.marginM
|
||||
anchors.leftMargin: showOnlyLists ? Style.marginM : 0
|
||||
anchors.rightMargin: showOnlyLists ? Style.marginM : 0
|
||||
spacing: Style.marginM
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Device List [3] (Available)
|
||||
NBox {
|
||||
id: availableDevicesBox
|
||||
visible: !btprefs.showOnlyLists && btprefs.unnamedAvailableDevices.length > 0 && BluetoothService.enabled
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: availableDevicesCol.implicitHeight + Style.marginXL
|
||||
|
||||
ColumnLayout {
|
||||
id: availableDevicesCol
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: Style.marginM
|
||||
anchors.bottomMargin: Style.marginM
|
||||
anchors.leftMargin: showOnlyLists ? Style.marginM : 0
|
||||
anchors.rightMargin: showOnlyLists ? Style.marginM : 0
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: btprefs.availableDevices
|
||||
delegate: nbox_delegate
|
||||
}
|
||||
|
||||
NText {
|
||||
visible: btprefs.availableDevices.length === 0 && btprefs.unnamedAvailableDevices.length > 0
|
||||
text: I18n.tr("panels.connections.bluetooth-devices-unnamed")
|
||||
pointSize: Style.fontSizeS
|
||||
color: Color.mOnSurfaceVariant
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
Layout.margins: Style.marginL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
visible: !showOnlyLists
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.connections.hide-unnamed-devices-label")
|
||||
description: I18n.tr("panels.connections.hide-unnamed-devices-description")
|
||||
checked: Settings.data.network.bluetoothHideUnnamedDevices
|
||||
onToggled: checked => Settings.data.network.bluetoothHideUnnamedDevices = checked
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: !btprefs.showOnlyLists && BluetoothService.enabled
|
||||
}
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("panels.connections.disable-discoverability-label")
|
||||
description: I18n.tr("panels.connections.disable-discoverability-description")
|
||||
checked: Settings.data.network.disableDiscoverability
|
||||
onToggled: checked => {
|
||||
Settings.data.network.disableDiscoverability = checked;
|
||||
BluetoothService.setDiscoverable(!checked);
|
||||
}
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: !btprefs.showOnlyLists && BluetoothService.enabled
|
||||
}
|
||||
|
||||
// RSSI Polling
|
||||
NToggle {
|
||||
label: I18n.tr("panels.connections.bluetooth-rssi-polling-label")
|
||||
description: I18n.tr("panels.connections.bluetooth-rssi-polling-description")
|
||||
checked: Settings.data.network.bluetoothRssiPollingEnabled
|
||||
onToggled: checked => Settings.data.network.bluetoothRssiPollingEnabled = checked
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: !btprefs.showOnlyLists && BluetoothService.enabled
|
||||
}
|
||||
NSpinBox {
|
||||
label: I18n.tr("panels.connections.bluetooth-rssi-polling-interval-label")
|
||||
description: I18n.tr("panels.connections.bluetooth-rssi-polling-interval-description")
|
||||
from: 10000
|
||||
to: 120000
|
||||
stepSize: 1000
|
||||
value: Settings.data.network.bluetoothRssiPollIntervalMs
|
||||
defaultValue: Settings.getDefaultValue("network.bluetoothRssiPollIntervalMs")
|
||||
onValueChanged: Settings.data.network.bluetoothRssiPollIntervalMs = value
|
||||
suffix: " ms"
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
visible: (!btprefs.showOnlyLists && BluetoothService.enabled) && Settings.data.network.bluetoothRssiPollingEnabled
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
color: Color.mSurface
|
||||
radius: Style.radiusM
|
||||
border.color: Style.boxBorderColor
|
||||
border.width: Style.borderS
|
||||
z: 1000
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onClicked: mouse => mouse.accepted = true
|
||||
onWheel: wheel => wheel.accepted = true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: pinCol
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.marginL
|
||||
spacing: Style.marginL
|
||||
|
||||
NIcon {
|
||||
icon: "lock"
|
||||
pointSize: 48
|
||||
color: Color.mPrimary
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
}
|
||||
NText {
|
||||
text: I18n.tr("panels.connections.authentication-required")
|
||||
pointSize: Style.fontSizeXL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
NText {
|
||||
text: I18n.tr("panels.connections.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"
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
text = "";
|
||||
inputItem.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
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 = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
|
||||
import qs.Commons
|
||||
import qs.Services.Networking
|
||||
import qs.Services.System
|
||||
import qs.Services.UI
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: wifiprefs
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: mainLayout.implicitHeight
|
||||
|
||||
// Combined visibility check: tab must be visible AND the window must be visible
|
||||
readonly property bool effectivelyVisible: wifiprefs.visible && Window.window && Window.window.visible
|
||||
|
||||
ColumnLayout {
|
||||
id: mainLayout
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Style.marginL
|
||||
|
||||
// Airplane Mode Toggle
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: masterControlColAirplane.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: masterControlColAirplane
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: Settings.data.network.airplaneModeEnabled ? "plane" : "plane-off"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: Settings.data.network.airplaneModeEnabled ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("toast.airplane-mode.title")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
checked: Settings.data.network.airplaneModeEnabled
|
||||
onToggled: checked => NetworkService.setAirplaneMode(checked)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wi-Fi Master Control
|
||||
NBox {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: masterControlCol.implicitHeight
|
||||
|
||||
ColumnLayout {
|
||||
id: masterControlCol
|
||||
anchors.fill: parent
|
||||
spacing: Style.marginM
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
spacing: Style.marginM
|
||||
|
||||
NIcon {
|
||||
icon: Settings.data.network.wifiEnabled ? "wifi" : "wifi-off"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: Settings.data.network.wifiEnabled ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NLabel {
|
||||
label: I18n.tr("common.wifi")
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
checked: Settings.data.network.wifiEnabled
|
||||
onToggled: checked => NetworkService.setWifiEnabled(checked)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
enabled: ProgramCheckerService.nmcliAvailable && !Settings.data.network.airplaneModeEnabled && NetworkService.wifiAvailable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.Commons
|
||||
import qs.Modules.Panels.Settings.Tabs.Connections
|
||||
import qs.Services.Networking
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: 0
|
||||
|
||||
NTabBar {
|
||||
id: subTabBar
|
||||
Layout.fillWidth: true
|
||||
Layout.bottomMargin: Style.marginM
|
||||
distributeEvenly: true
|
||||
currentIndex: tabView.currentIndex
|
||||
|
||||
NTabButton {
|
||||
text: I18n.tr("common.wifi")
|
||||
visible: NetworkService.wifiAvailable
|
||||
tabIndex: 0
|
||||
checked: subTabBar.currentIndex === 0
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("common.bluetooth")
|
||||
visible: BluetoothService.bluetoothAvailable
|
||||
tabIndex: 1
|
||||
checked: subTabBar.currentIndex === 1
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Style.marginL
|
||||
}
|
||||
|
||||
NTabView {
|
||||
id: tabView
|
||||
Layout.fillHeight: true
|
||||
currentIndex: subTabBar.currentIndex
|
||||
WifiSubTab {}
|
||||
BluetoothSubTab {}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
import qs.Services.Networking
|
||||
import qs.Services.System
|
||||
import qs.Widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("actions.enable-wifi")
|
||||
description: I18n.tr("panels.network.wifi-description")
|
||||
checked: ProgramCheckerService.nmcliAvailable && Settings.data.network.wifiEnabled
|
||||
onToggled: checked => NetworkService.setWifiEnabled(checked)
|
||||
enabled: ProgramCheckerService.nmcliAvailable
|
||||
}
|
||||
|
||||
NDivider {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
// Bluetooth adapter toggle grouped with its panel settings
|
||||
NToggle {
|
||||
label: I18n.tr("actions.enable-bluetooth")
|
||||
description: I18n.tr("panels.network.bluetooth-description")
|
||||
checked: BluetoothService.enabled
|
||||
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
|
||||
}
|
||||
|
||||
// Bluetooth signal strength polling (RSSI via bluetoothctl)
|
||||
NToggle {
|
||||
label: I18n.tr("panels.network.bluetooth-rssi-polling-label")
|
||||
description: I18n.tr("panels.network.bluetooth-rssi-polling-description")
|
||||
checked: Settings.data && Settings.data.network && Settings.data.network.bluetoothRssiPollingEnabled
|
||||
enabled: BluetoothService.enabled
|
||||
onToggled: checked => Settings.data.network.bluetoothRssiPollingEnabled = checked
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,12 @@ Singleton {
|
||||
}
|
||||
|
||||
function isBluetoothDevice(device) {
|
||||
return device && device.batteryAvailable !== undefined;
|
||||
if (!device) {return false;}
|
||||
// Check for Quickshell Bluetooth device property
|
||||
if (device.batteryAvailable !== undefined) {return true;}
|
||||
// Check for UPower device path indicating it's a Bluetooth device
|
||||
if (device.nativePath && (device.nativePath.includes("bluez") || device.nativePath.includes("bluetooth"))) {return true;}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getDeviceName(device) {
|
||||
|
||||
@@ -12,38 +12,48 @@ Singleton {
|
||||
id: root
|
||||
|
||||
// Constants (centralized tunables)
|
||||
readonly property int ctlPollMs: 1500
|
||||
readonly property int ctlPollMs: 10000
|
||||
readonly property int ctlPollSoonMs: 250
|
||||
readonly property int scanAutoStopMs: 10000
|
||||
|
||||
property bool airplaneModeToggled: false
|
||||
property bool lastBluetoothBlocked: false
|
||||
property bool lastWifiBlocked: false
|
||||
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter
|
||||
|
||||
// Power/blocked state
|
||||
readonly property bool enabled: adapter ? adapter.enabled : root.ctlPowered
|
||||
readonly property bool blocked: adapter?.state === BluetoothAdapterState.Blocked
|
||||
// Airplane mode status
|
||||
readonly property bool airplaneModeEnabled: Settings.data.network.airplaneModeEnabled
|
||||
property bool airplaneModeToggled: false
|
||||
|
||||
// Power/blocked/availability state
|
||||
property bool ctlAvailable: false
|
||||
readonly property bool bluetoothAvailable: !!adapter || root.ctlAvailable
|
||||
readonly property bool enabled: adapter?.enabled ?? root.ctlPowered
|
||||
property bool ctlPowered: false
|
||||
property bool ctlDiscovering: false
|
||||
property bool ctlDiscoverable: false
|
||||
// Adapter discoverability (advertising) flag (driven by bluetoothctl)
|
||||
readonly property bool discoverable: root.ctlDiscoverable
|
||||
|
||||
onAdapterChanged: {
|
||||
pollCtlState();
|
||||
if (!adapter) {
|
||||
ctlPollTimer.interval = 2000;
|
||||
}
|
||||
}
|
||||
|
||||
// Adapter discoverability (advertising) flag
|
||||
readonly property bool discoverable: adapter?.discoverable ?? root.ctlDiscoverable
|
||||
readonly property var devices: adapter ? adapter.devices : null
|
||||
readonly property var connectedDevices: {
|
||||
if (!adapter || !adapter.devices) {
|
||||
return [];
|
||||
}
|
||||
return adapter.devices.values.filter(function (dev) {
|
||||
return dev && dev.connected;
|
||||
});
|
||||
return adapter.devices.values.filter(dev => dev && dev.connected);
|
||||
}
|
||||
|
||||
// Current backend in use for scanning (e.g., "native", "bluetoothctl")
|
||||
property string backendUsed: ""
|
||||
|
||||
// Experimental: best‑effort RSSI polling for connected devices (without root)
|
||||
// Enabled in debug mode or via user setting in Settings > Network
|
||||
property bool rssiPollingEnabled: (Settings && (Settings.isDebug || (Settings.data && Settings.data.network && Settings.data.network.bluetoothRssiPollingEnabled))) ? true : false
|
||||
// Interval can be configured from Settings; defaults to 10s
|
||||
property int rssiPollIntervalMs: (Settings && Settings.data && Settings.data.network && Settings.data.network.bluetoothRssiPollIntervalMs) ? Settings.data.network.bluetoothRssiPollIntervalMs : 10000
|
||||
property bool rssiPollingEnabled: Settings?.data?.network?.bluetoothRssiPollingEnabled || Settings?.isDebug || false
|
||||
// Interval can be configured from Settings; defaults to 60s
|
||||
property int rssiPollIntervalMs: Settings?.data?.network?.bluetoothRssiPollIntervalMs || 60000
|
||||
// RSSI helper sub‑component
|
||||
property BluetoothRssi rssi: BluetoothRssi {
|
||||
enabled: root.enabled && root.rssiPollingEnabled
|
||||
@@ -57,215 +67,114 @@ Singleton {
|
||||
property int connectRetryIntervalMs: 2000
|
||||
|
||||
// Internal: temporarily pause discovery during pair/connect to reduce HCI churn
|
||||
// Use a resume deadline to coalesce overlapping pauses safely
|
||||
property bool _discoveryWasRunning: false
|
||||
property double _discoveryResumeAtMs: 0
|
||||
// Timer used to restore discovery after temporary pause during pair/connect
|
||||
property bool _lastEnabledState: root.enabled
|
||||
|
||||
Timer {
|
||||
id: restoreDiscoveryTimer
|
||||
id: initDelayTimer
|
||||
interval: 3000
|
||||
running: true
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
const now = Date.now();
|
||||
if (now < root._discoveryResumeAtMs) {
|
||||
// Not yet time to resume; reschedule
|
||||
interval = Math.max(100, root._discoveryResumeAtMs - now);
|
||||
restart();
|
||||
return;
|
||||
}
|
||||
if (root._discoveryWasRunning) {
|
||||
root.setScanActive(true, 0);
|
||||
}
|
||||
root._discoveryWasRunning = false;
|
||||
root._discoveryResumeAtMs = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function _pauseDiscoveryFor(ms) {
|
||||
try {
|
||||
// Remember if discovery was running before the first pause
|
||||
root._discoveryWasRunning = root._discoveryWasRunning || !!root.ctlDiscovering;
|
||||
if (root.ctlDiscovering) {
|
||||
root.setScanActive(false, 0);
|
||||
}
|
||||
if (ms && ms > 0) {
|
||||
const now = Date.now();
|
||||
const resumeAt = now + ms;
|
||||
if (resumeAt > root._discoveryResumeAtMs) {
|
||||
root._discoveryResumeAtMs = resumeAt;
|
||||
}
|
||||
restoreDiscoveryTimer.interval = Math.max(100, root._discoveryResumeAtMs - now);
|
||||
restoreDiscoveryTimer.restart();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// Persistent process for fallback scanning to keep the session alive
|
||||
// Persistent process for bluetoothctl scanning when native discovery is unavailable
|
||||
Process {
|
||||
id: fallbackScanProcess
|
||||
// Pipe scan on and a long sleep to bluetoothctl to keep it running
|
||||
command: ["sh", "-c", "trap 'kill 0' EXIT; (echo 'scan on'; sleep 3600) | bluetoothctl"]
|
||||
onExited: Logger.d("Bluetooth", "Fallback scan process exited")
|
||||
id: bluetoothctlScanProcess
|
||||
command: ["bluetoothctl", "scan", "on"]
|
||||
onExited: Logger.d("Bluetooth", "bluetoothctl scan process exited.")
|
||||
}
|
||||
|
||||
// Unify discovery controls and auto‑stop window
|
||||
function setScanActive(active, durationMs) {
|
||||
// Logger.e("Bluetooth", "setScanActive called with active=" + active + ", durationMs=" + durationMs); // used for debugging
|
||||
// Cancel any scheduled resume so manual toggle wins
|
||||
try {
|
||||
root._discoveryResumeAtMs = 0;
|
||||
restoreDiscoveryTimer.stop();
|
||||
root._discoveryWasRunning = false;
|
||||
} catch (_) {}
|
||||
|
||||
// Prefer Quickshell API if available, fall back to bluetoothctl
|
||||
// Unify discovery controls
|
||||
function setScanActive(active) {
|
||||
var nativeSuccess = false;
|
||||
try {
|
||||
if (adapter) {
|
||||
if (active && adapter.discovering !== undefined) {
|
||||
// Logger.e("Bluetooth", "Starting discovery with Quickshell API"); // used for debugging
|
||||
adapter.discovering = true;
|
||||
nativeSuccess = true;
|
||||
} else if (!active && adapter.discovering !== undefined) {
|
||||
// Logger.e("Bluetooth", "Stopping discovery with Quickshell API"); // used for debugging
|
||||
adapter.discovering = false;
|
||||
nativeSuccess = true;
|
||||
if (adapter && adapter.discovering !== undefined) {
|
||||
if (active || adapter.discovering) { // Only attempt to set if activating, or if deactivating and currently currently discovering
|
||||
adapter.discovering = active;
|
||||
}
|
||||
} else {
|
||||
Logger.w("Bluetooth", "Adapter is null/undefined in setScanActive");
|
||||
nativeSuccess = true; // Mark as success if adapter was handled without error
|
||||
}
|
||||
} catch (e1) {
|
||||
Logger.e("Bluetooth", "setScanActive failed with exception", e1);
|
||||
} catch (e) {
|
||||
Logger.e("Bluetooth", "setScanActive native failed", e);
|
||||
}
|
||||
|
||||
Logger.d("Bluetooth", "nativeSuccess=" + nativeSuccess);
|
||||
|
||||
// Only issue bluetoothctl if we didn't use the adapter API
|
||||
if (!nativeSuccess) {
|
||||
if (active) {
|
||||
// Logger.e("Bluetooth", "Starting fallback scan process");
|
||||
fallbackScanProcess.running = true;
|
||||
bluetoothctlScanProcess.running = true;
|
||||
} else {
|
||||
// Logger.e("Bluetooth", "Stopping fallback scan process");
|
||||
fallbackScanProcess.running = false;
|
||||
// Explicitly send scan off command as well to ensure state is cleared
|
||||
bluetoothctlScanProcess.running = false;
|
||||
btExec(["bluetoothctl", "scan", "off"]);
|
||||
}
|
||||
} else {
|
||||
// Logger.e("Bluetooth", "Skipping bluetoothctl fallback as native API was used");
|
||||
// Ensure fallback process is stopped if we switched to native
|
||||
if (fallbackScanProcess.running) {
|
||||
fallbackScanProcess.running = false;
|
||||
}
|
||||
} else if (bluetoothctlScanProcess.running) {
|
||||
bluetoothctlScanProcess.running = false;
|
||||
}
|
||||
|
||||
if (active && durationMs && durationMs > 0) {
|
||||
manualScanTimer.interval = durationMs;
|
||||
// Logger.e("Bluetooth", "Restarting manualScanTimer with interval " + durationMs + "ms");
|
||||
manualScanTimer.restart();
|
||||
} else {
|
||||
if (manualScanTimer.running) {
|
||||
// Logger.e("Bluetooth", "Stopping manualScanTimer");
|
||||
manualScanTimer.stop();
|
||||
}
|
||||
}
|
||||
requestCtlPoll(ctlPollSoonMs);
|
||||
}
|
||||
|
||||
// Explicit toggle that cancels any pending restore so UI button behaves predictably
|
||||
function toggleDiscovery() {
|
||||
// Logger.e("Bluetooth", "toggleDiscovery called. Adapter present: " + (!!adapter));
|
||||
if (!adapter) {
|
||||
// Logger.e("Bluetooth", "toggleDiscovery aborting: no adapter");
|
||||
return;
|
||||
}
|
||||
// Logger.e("Bluetooth", "toggleDiscovery calling setScanActive. Current scanningActive=" + root.scanningActive);
|
||||
setScanActive(!root.scanningActive, scanAutoStopMs);
|
||||
}
|
||||
|
||||
// Auto-stop manual discovery after a short window
|
||||
Timer {
|
||||
id: manualScanTimer
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
// Logger.e("Bluetooth", "manualScanTimer triggered");
|
||||
// Stop scan if currently active
|
||||
if (root.scanningActive) {
|
||||
// Logger.e("Bluetooth", "manualScanTimer calling setScanActive(false)");
|
||||
root.setScanActive(false, 0);
|
||||
} else {
|
||||
Logger.d("Bluetooth", "manualScanTimer triggered but scanningActive is false, doing nothing");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Exposed scanning flag for UI button state; reflects adapter discovery when available
|
||||
readonly property bool scanningActive: ((adapter && adapter.discovering) ? true : (root.ctlDiscovering === true)) || manualScanTimer.running
|
||||
readonly property bool scanningActive: adapter?.discovering ?? root.ctlDiscovering
|
||||
|
||||
function init() {
|
||||
Logger.i("Bluetooth", "Service started");
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Prime state immediately so UI reflects correct power/discovery flags
|
||||
pollCtlState();
|
||||
// Ensure Airplane Mode persists upon reboot
|
||||
if (root.airplaneModeEnabled) {
|
||||
Quickshell.execDetached(["rfkill", "block", "wifi"]);
|
||||
Quickshell.execDetached(["rfkill", "block", "bluetooth"]);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: We intentionally avoid creating or managing a custom BlueZ agent in-process.
|
||||
// Pairing flows are delegated to `bluetoothctl` as needed to keep behavior
|
||||
// consistent and reduce maintenance complexity.
|
||||
|
||||
// No implicit discovery auto-start; state polled from bluetoothctl instead
|
||||
|
||||
// Track adapter state changes
|
||||
Connections {
|
||||
target: adapter
|
||||
function onStateChanged() {
|
||||
if (!adapter) {
|
||||
if (!adapter || adapter.state === BluetoothAdapter.Enabling || adapter.state === BluetoothAdapter.Disabling) {
|
||||
return;
|
||||
}
|
||||
if (adapter.state === BluetoothAdapter.Enabling || adapter.state === BluetoothAdapter.Disabling) {
|
||||
return;
|
||||
}
|
||||
Logger.i("Bluetooth", "Bluetooth state change command executed");
|
||||
const bluetoothBlockedToggled = (root.blocked !== lastBluetoothBlocked);
|
||||
root.lastBluetoothBlocked = root.blocked;
|
||||
if (bluetoothBlockedToggled) {
|
||||
checkWifiBlocked.running = true;
|
||||
} else if (adapter.state === BluetoothAdapter.Enabled) {
|
||||
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.enabled"), "bluetooth");
|
||||
Logger.d("Bluetooth", "Adapter enabled");
|
||||
} else if (adapter.state === BluetoothAdapter.Disabled) {
|
||||
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.disabled"), "bluetooth-off");
|
||||
Logger.d("Bluetooth", "Adapter disabled");
|
||||
}
|
||||
checkAirplaneMode.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: checkWifiBlocked
|
||||
id: checkAirplaneMode
|
||||
running: false
|
||||
command: ["rfkill", "list", "wifi"]
|
||||
command: ["rfkill", "list"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var wifiBlocked = text && text.trim().indexOf("Soft blocked: yes") !== -1;
|
||||
Logger.d("Network", "Wi-Fi adapter was detected as blocked:", wifiBlocked);
|
||||
var output = this.text || "";
|
||||
var wifiBlocked = /^\d+:.*Wireless LAN[^\n]*\n\s*Soft blocked:\s*yes/im.test(output)
|
||||
var btBlocked = /^\d+:.*Bluetooth[^\n]*\n\s*Soft blocked:\s*yes/im.test(output)
|
||||
var isAirplaneModeActive = wifiBlocked && btBlocked;
|
||||
|
||||
// Check if airplane mode has been toggled
|
||||
if (wifiBlocked && root.blocked) {
|
||||
if (isAirplaneModeActive && !root.airplaneModeEnabled) {
|
||||
root.airplaneModeToggled = true;
|
||||
root.lastWifiBlocked = true;
|
||||
NetworkService.setWifiEnabled(false);
|
||||
Settings.data.network.airplaneModeEnabled = true;
|
||||
ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.enabled"), "plane");
|
||||
} else if (!wifiBlocked && lastWifiBlocked) {
|
||||
Logger.i("AirplaneMode", "Wi-Fi & Bluetooth adapter blocked")
|
||||
} else if (!isAirplaneModeActive && root.airplaneModeEnabled) {
|
||||
root.airplaneModeToggled = true;
|
||||
root.lastWifiBlocked = false;
|
||||
NetworkService.setWifiEnabled(true);
|
||||
Settings.data.network.airplaneModeEnabled = false;
|
||||
ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.disabled"), "plane-off");
|
||||
} else if (adapter.enabled) {
|
||||
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.enabled"), "bluetooth");
|
||||
Logger.d("Bluetooth", "Adapter enabled");
|
||||
Logger.i("AirplaneMode", "Wi-Fi & Bluetooth adapter unblocked")
|
||||
} else {
|
||||
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.disabled"), "bluetooth-off");
|
||||
Logger.d("Bluetooth", "Adapter disabled");
|
||||
var isCurrentlyEnabled = (adapter && adapter.enabled) || root.ctlPowered;
|
||||
var stateChanged = isCurrentlyEnabled !== root._lastEnabledState;
|
||||
if (!initDelayTimer.running && stateChanged) {
|
||||
if (isCurrentlyEnabled) {
|
||||
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.enabled"), "bluetooth");
|
||||
} else {
|
||||
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.disabled"), "bluetooth-off");
|
||||
}
|
||||
}
|
||||
root._lastEnabledState = isCurrentlyEnabled;
|
||||
Logger.d("Bluetooth", "State updated - enabled:", isCurrentlyEnabled);
|
||||
}
|
||||
root.airplaneModeToggled = false;
|
||||
}
|
||||
@@ -273,7 +182,7 @@ Singleton {
|
||||
stderr: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text && text.trim()) {
|
||||
Logger.w("Bluetooth", "rfkill (wifi) stderr:", text.trim());
|
||||
Logger.w("AirplaneMode", "rfkill stderr:", text.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,28 +192,34 @@ Singleton {
|
||||
Process {
|
||||
id: ctlShowProcess
|
||||
running: false
|
||||
stdout: StdioCollector {
|
||||
id: ctlStdout
|
||||
}
|
||||
stdout: StdioCollector { id: ctlStdout }
|
||||
onExited: function (exitCode, exitStatus) {
|
||||
try {
|
||||
var text = ctlStdout.text || "";
|
||||
// Logger.e("Bluetooth", "ctlShowProcess exited. Output length: " + text.length);
|
||||
// Parse Powered/Discoverable/Discovering lines
|
||||
var mp = text.match(/\bPowered:\s*(yes|no)\b/i);
|
||||
if (mp && mp.length > 1) {
|
||||
root.ctlPowered = (mp[1].toLowerCase() === "yes");
|
||||
}
|
||||
var md = text.match(/\bDiscoverable:\s*(yes|no)\b/i);
|
||||
if (md && md.length > 1) {
|
||||
root.ctlDiscoverable = (md[1].toLowerCase() === "yes");
|
||||
}
|
||||
var ms = text.match(/\bDiscovering:\s*(yes|no)\b/i);
|
||||
if (ms && ms.length > 1) {
|
||||
var discovering = (ms[1].toLowerCase() === "yes");
|
||||
//Logger.e("Bluetooth", "Parsed Discovering state from bluetoothctl: " + discovering + " (current ctlDiscovering: " + root.ctlDiscovering + ")");
|
||||
root.ctlDiscovering = discovering;
|
||||
var lines = text.split('\n');
|
||||
var foundController = false; // Strict state following.
|
||||
var powered = false; // Strict state following.
|
||||
var discoverable = false; // Strict state following.
|
||||
var discovering = false; // Strict state following.
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim();
|
||||
if (line.indexOf("Controller") === 0) {
|
||||
foundController = true;
|
||||
}
|
||||
var mp = line.match(/\bPowered:\s*(yes|no)\b/i);
|
||||
if (mp) { powered = (mp[1].toLowerCase() === "yes"); }
|
||||
|
||||
var md = line.match(/\bDiscoverable:\s*(yes|no)\b/i);
|
||||
if (md) { discoverable = (md[1].toLowerCase() === "yes"); }
|
||||
|
||||
var ms = line.match(/\bDiscovering:\s*(yes|no)\b/i);
|
||||
if (ms) { discovering = (ms[1].toLowerCase() === "yes"); }
|
||||
}
|
||||
root.ctlAvailable = foundController; // Assign findings.
|
||||
root.ctlPowered = powered; // Assign findings.
|
||||
root.ctlDiscoverable = discoverable; // Assign findings.
|
||||
root.ctlDiscovering = discovering; // Assign findings.
|
||||
} catch (e) {
|
||||
Logger.d("Bluetooth", "Failed to parse bluetoothctl show output", e);
|
||||
}
|
||||
@@ -324,23 +239,30 @@ Singleton {
|
||||
// Periodic state polling
|
||||
Timer {
|
||||
id: ctlPollTimer
|
||||
interval: ctlPollMs
|
||||
interval: adapter ? ctlPollMs : 2000
|
||||
repeat: true
|
||||
running: root.enabled
|
||||
onTriggered: pollCtlState()
|
||||
}
|
||||
|
||||
// Short-delay poll scheduler
|
||||
Timer {
|
||||
id: pollCtlStateSoonTimer
|
||||
interval: ctlPollSoonMs
|
||||
repeat: false
|
||||
onTriggered: pollCtlState()
|
||||
running: true
|
||||
onTriggered: {
|
||||
pollCtlState();
|
||||
var targetInterval = adapter ? ctlPollMs : 2000;
|
||||
if (interval !== targetInterval) {
|
||||
interval = targetInterval;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function requestCtlPoll(delayMs) {
|
||||
pollCtlStateSoonTimer.interval = Math.max(50, delayMs || ctlPollSoonMs);
|
||||
pollCtlStateSoonTimer.restart();
|
||||
ctlPollTimer.interval = Math.max(50, delayMs || ctlPollSoonMs);
|
||||
ctlPollTimer.restart();
|
||||
}
|
||||
|
||||
// Handle system wakeup to force-poll and ensure state is up-to-date
|
||||
Connections {
|
||||
target: Time
|
||||
function onResumed() {
|
||||
Logger.i("Bluetooth", "System resumed - forcing state poll");
|
||||
requestCtlPoll();
|
||||
}
|
||||
}
|
||||
|
||||
// Adapter power (enable/disable) via bluetoothctl
|
||||
@@ -362,11 +284,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);
|
||||
@@ -406,15 +323,6 @@ Singleton {
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
Paired
|
||||
Means you’ve successfully exchanged keys with the device.
|
||||
The devices remember each other and can authenticate without repeating the pairing process.
|
||||
Example: once your headphones are paired, you don’t need to type a PIN every time.
|
||||
Hence, instead of !device.paired, should be device.connected
|
||||
*/
|
||||
// Only allow connect if device is already paired or trusted
|
||||
return !device.connected && (device.paired || device.trusted) && !device.pairing && !device.blocked;
|
||||
}
|
||||
|
||||
@@ -424,37 +332,25 @@ 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) {
|
||||
var p = getSignalPercent(device);
|
||||
if (p === null)
|
||||
if (p === null) {
|
||||
return I18n.tr("bluetooth.panel.signal-text-unknown");
|
||||
if (p >= 80)
|
||||
}
|
||||
if (p >= 80) {
|
||||
return I18n.tr("bluetooth.panel.signal-text-excellent");
|
||||
if (p >= 60)
|
||||
}
|
||||
if (p >= 60) {
|
||||
return I18n.tr("bluetooth.panel.signal-text-good");
|
||||
if (p >= 40)
|
||||
}
|
||||
if (p >= 40) {
|
||||
return I18n.tr("bluetooth.panel.signal-text-fair");
|
||||
if (p >= 20)
|
||||
}
|
||||
if (p >= 20) {
|
||||
return I18n.tr("bluetooth.panel.signal-text-poor");
|
||||
}
|
||||
return I18n.tr("bluetooth.panel.signal-text-very-poor");
|
||||
}
|
||||
|
||||
@@ -478,7 +374,6 @@ Singleton {
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return device.pairing || device.state === BluetoothDevice.Disconnecting || device.state === BluetoothDevice.Connecting;
|
||||
}
|
||||
|
||||
@@ -506,7 +401,6 @@ Singleton {
|
||||
return;
|
||||
}
|
||||
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.pairing"), "bluetooth");
|
||||
// Delegate pairing to bluetoothctl which registers/uses its own agent
|
||||
try {
|
||||
pairWithBluetoothctl(device);
|
||||
} catch (e) {
|
||||
@@ -537,8 +431,7 @@ Singleton {
|
||||
id: pairingProcess
|
||||
stdout: SplitParser {
|
||||
onRead: data => {
|
||||
var chunk = data;
|
||||
if (chunk.indexOf("[PIN_REQ]") !== -1) {
|
||||
if (data.indexOf("[PIN_REQ]") !== -1) {
|
||||
root.pinRequired = true;
|
||||
Logger.d("Bluetooth", "PIN required for pairing");
|
||||
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("bluetooth.panel.pin-required"), "lock");
|
||||
@@ -553,9 +446,10 @@ Singleton {
|
||||
Logger.i("Bluetooth", "Pairing process exited.");
|
||||
// Restore discovery if we paused it
|
||||
if (root._discoveryWasRunning) {
|
||||
root.setScanActive(true, 0);
|
||||
root.setScanActive(true);
|
||||
}
|
||||
root._discoveryWasRunning = false;
|
||||
root.requestCtlPoll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -572,24 +466,23 @@ Singleton {
|
||||
|
||||
Logger.i("Bluetooth", "pairWithBluetoothctl", addr);
|
||||
|
||||
// Stop any previous pairing attempt
|
||||
if (pairingProcess.running) {
|
||||
pairingProcess.running = false;
|
||||
}
|
||||
root.pinRequired = false;
|
||||
|
||||
// Compute bounded waits from tunables
|
||||
const pairWait = Math.max(5, Number(root.pairWaitSeconds) | 0);
|
||||
const attempts = Math.max(1, Number(root.connectAttempts) | 0);
|
||||
const intervalMs = Math.max(500, Number(root.connectRetryIntervalMs) | 0);
|
||||
const intervalSec = Math.max(1, Math.round(intervalMs / 1000));
|
||||
|
||||
// Pause discovery during pair/connect to avoid interference
|
||||
const totalPauseMs = (pairWait * 1000) + (attempts * intervalSec * 1000) + 2000;
|
||||
_pauseDiscoveryFor(totalPauseMs);
|
||||
root._discoveryWasRunning = root.scanningActive;
|
||||
if (root.scanningActive) {
|
||||
root.setScanActive(false);
|
||||
}
|
||||
|
||||
const scriptPath = Quickshell.shellDir + "/Scripts/python/src/network/bluetooth-pair.py";
|
||||
|
||||
pairingProcess.command = ["python3", scriptPath, String(addr), String(pairWait), String(attempts), String(intervalSec)];
|
||||
pairingProcess.running = true;
|
||||
}
|
||||
@@ -609,20 +502,15 @@ Singleton {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
if (device.pairing)
|
||||
return "pairing";
|
||||
if (device.blocked)
|
||||
return "blocked";
|
||||
if (device.state === BluetoothDevice.Connecting)
|
||||
return "connecting";
|
||||
if (device.state === BluetoothDevice.Disconnecting)
|
||||
return "disconnecting";
|
||||
if (device.pairing) return "pairing";
|
||||
if (device.blocked) return "blocked";
|
||||
if (device.state === BluetoothDevice.Connecting) return "connecting";
|
||||
if (device.state === BluetoothDevice.Disconnecting) return "disconnecting";
|
||||
} catch (_) {}
|
||||
return "";
|
||||
}
|
||||
|
||||
function unpairDevice(device) {
|
||||
// Alias to forgetDevice for clarity in UI
|
||||
forgetDevice(device);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,12 @@ import qs.Services.UI
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property bool wifiAvailable: _wifiAvailable
|
||||
readonly property bool ethernetAvailable: _ethernetAvailable
|
||||
|
||||
property bool _wifiAvailable: false
|
||||
property bool _ethernetAvailable: false
|
||||
|
||||
// Core state
|
||||
property var networks: ({})
|
||||
property bool scanning: false
|
||||
@@ -69,14 +75,14 @@ Singleton {
|
||||
function onWifiEnabledChanged() {
|
||||
if (Settings.data.network.wifiEnabled) {
|
||||
if (!BluetoothService.airplaneModeToggled) {
|
||||
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("common.enabled"), "wifi");
|
||||
ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("common.enabled"), "wifi");
|
||||
}
|
||||
// Perform a scan to update the UI
|
||||
delayedScanTimer.interval = 3000;
|
||||
delayedScanTimer.restart();
|
||||
} else {
|
||||
if (!BluetoothService.airplaneModeToggled) {
|
||||
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("common.disabled"), "wifi-off");
|
||||
ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("common.disabled"), "wifi-off");
|
||||
}
|
||||
// Clear networks so the widget icon changes
|
||||
root.networks = ({});
|
||||
@@ -84,9 +90,23 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle system resume to refresh state and connectivity
|
||||
Connections {
|
||||
target: Time
|
||||
function onResumed() {
|
||||
Logger.i("Network", "System resumed - forcing state poll");
|
||||
ethernetStateProcess.running = true;
|
||||
root.scan();
|
||||
root.refreshActiveWifiDetails();
|
||||
root.refreshActiveEthernetDetails();
|
||||
connectivityCheckProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.i("Network", "Service started");
|
||||
if (ProgramCheckerService.nmcliAvailable) {
|
||||
detectNetworkCapabilities();
|
||||
syncWifiState();
|
||||
scan();
|
||||
// Prime ethernet state immediately so UI can reflect wired status on startup
|
||||
@@ -101,6 +121,7 @@ Singleton {
|
||||
target: ProgramCheckerService
|
||||
function onNmcliAvailableChanged() {
|
||||
if (ProgramCheckerService.nmcliAvailable) {
|
||||
detectNetworkCapabilities();
|
||||
syncWifiState();
|
||||
scan();
|
||||
// Refresh ethernet status as soon as nmcli becomes available
|
||||
@@ -112,6 +133,39 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Function to detect host's networking capabilities eg has WiFi/Ethernet.
|
||||
function detectNetworkCapabilities() {
|
||||
if (ProgramCheckerService.nmcliAvailable) {
|
||||
Logger.d("Network", "Starting network capability detection...");
|
||||
capabilityDetectProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Process to detect host's networking capabilities
|
||||
Process {
|
||||
id: capabilityDetectProcess
|
||||
running: false
|
||||
command: ["nmcli", "-t", "-f", "TYPE", "device"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var lines = text.trim().split("\n");
|
||||
var wifi = false;
|
||||
var eth = false;
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var type = lines[i].trim();
|
||||
if (type === "wifi") {
|
||||
wifi = true;
|
||||
} else if (type === "ethernet") {
|
||||
eth = true;
|
||||
}
|
||||
}
|
||||
root._wifiAvailable = wifi;
|
||||
root._ethernetAvailable = eth;
|
||||
Logger.d("Network", "Detected capabilities - WiFi:", wifi, "Ethernet:", eth);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save cache with debounce
|
||||
Timer {
|
||||
id: saveDebounce
|
||||
@@ -123,8 +177,9 @@ Singleton {
|
||||
function refreshActiveWifiDetails() {
|
||||
const now = Date.now();
|
||||
// If we're already fetching, don't start a new one
|
||||
if (detailsLoading)
|
||||
if (detailsLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use cached details if they are fresh
|
||||
if (activeWifiIf && activeWifiDetails && (now - activeWifiDetailsTimestamp) < activeWifiDetailsTtlMs)
|
||||
@@ -158,8 +213,9 @@ Singleton {
|
||||
// Refresh details for the currently active Ethernet link
|
||||
function refreshActiveEthernetDetails() {
|
||||
const now = Date.now();
|
||||
if (ethernetDetailsLoading)
|
||||
if (ethernetDetailsLoading) {
|
||||
return;
|
||||
}
|
||||
if (!root.ethernetConnected) {
|
||||
// Link is down: keep the selected interface so UI can still show its info as disconnected
|
||||
// Only clear details to avoid showing stale IP/speed/etc.
|
||||
@@ -187,21 +243,35 @@ Singleton {
|
||||
|
||||
// Core functions
|
||||
function syncWifiState() {
|
||||
if (!ProgramCheckerService.nmcliAvailable)
|
||||
if (!ProgramCheckerService.nmcliAvailable) {
|
||||
return;
|
||||
}
|
||||
wifiStateProcess.running = true;
|
||||
}
|
||||
|
||||
function setWifiEnabled(enabled) {
|
||||
if (!ProgramCheckerService.nmcliAvailable)
|
||||
if (!ProgramCheckerService.nmcliAvailable) {
|
||||
return;
|
||||
}
|
||||
Logger.i("Wi-Fi", "SetWifiEnabled", enabled);
|
||||
Settings.data.network.wifiEnabled = enabled;
|
||||
wifiStateEnableProcess.running = true;
|
||||
}
|
||||
|
||||
function setAirplaneMode(enabled) {
|
||||
if (enabled) {
|
||||
Quickshell.execDetached(["rfkill", "block", "wifi"]);
|
||||
Quickshell.execDetached(["rfkill", "block", "bluetooth"]);
|
||||
} else {
|
||||
Quickshell.execDetached(["rfkill", "unblock", "wifi"]);
|
||||
Quickshell.execDetached(["rfkill", "unblock", "bluetooth"]);
|
||||
}
|
||||
}
|
||||
|
||||
function scan() {
|
||||
if (!ProgramCheckerService.nmcliAvailable || !Settings.data.network.wifiEnabled)
|
||||
if (!ProgramCheckerService.nmcliAvailable || !Settings.data.network.wifiEnabled) {
|
||||
return;
|
||||
}
|
||||
if (scanning) {
|
||||
// Mark current scan results to be ignored and schedule a new scan
|
||||
Logger.d("Network", "Scan already in progress, will ignore results and rescan");
|
||||
@@ -226,15 +296,17 @@ Singleton {
|
||||
|
||||
// Refresh only Ethernet state/details
|
||||
function refreshEthernet() {
|
||||
if (!ProgramCheckerService.nmcliAvailable)
|
||||
if (!ProgramCheckerService.nmcliAvailable) {
|
||||
return;
|
||||
}
|
||||
ethernetStateProcess.running = true;
|
||||
refreshActiveEthernetDetails();
|
||||
}
|
||||
|
||||
function connect(ssid, password = "") {
|
||||
if (!ProgramCheckerService.nmcliAvailable || connecting)
|
||||
if (!ProgramCheckerService.nmcliAvailable || connecting) {
|
||||
return;
|
||||
}
|
||||
connecting = true;
|
||||
connectingTo = ssid;
|
||||
lastError = "";
|
||||
@@ -254,16 +326,18 @@ Singleton {
|
||||
}
|
||||
|
||||
function disconnect(ssid) {
|
||||
if (!ProgramCheckerService.nmcliAvailable)
|
||||
if (!ProgramCheckerService.nmcliAvailable) {
|
||||
return;
|
||||
}
|
||||
disconnectingFrom = ssid;
|
||||
disconnectProcess.ssid = ssid;
|
||||
disconnectProcess.running = true;
|
||||
}
|
||||
|
||||
function forget(ssid) {
|
||||
if (!ProgramCheckerService.nmcliAvailable)
|
||||
if (!ProgramCheckerService.nmcliAvailable) {
|
||||
return;
|
||||
}
|
||||
forgettingNetwork = ssid;
|
||||
|
||||
// Remove from cache
|
||||
@@ -317,16 +391,21 @@ Singleton {
|
||||
|
||||
// Helper functions
|
||||
function signalIcon(signal, isConnected) {
|
||||
if (isConnected === undefined)
|
||||
if (isConnected === undefined) {
|
||||
isConnected = false;
|
||||
if (isConnected && !root.internetConnectivity)
|
||||
}
|
||||
if (isConnected && !root.internetConnectivity) {
|
||||
return "world-off";
|
||||
if (signal >= 80)
|
||||
}
|
||||
if (signal >= 80) {
|
||||
return "wifi";
|
||||
if (signal >= 50)
|
||||
}
|
||||
if (signal >= 50) {
|
||||
return "wifi-2";
|
||||
if (signal >= 20)
|
||||
}
|
||||
if (signal >= 20) {
|
||||
return "wifi-1";
|
||||
}
|
||||
return "wifi-0";
|
||||
}
|
||||
|
||||
@@ -447,8 +526,9 @@ Singleton {
|
||||
});
|
||||
root.ethernetInterfaces = ethList;
|
||||
if (ifname) {
|
||||
if (root.activeEthernetIf !== ifname)
|
||||
root.activeEthernetIf = ifname;
|
||||
if (root.activeEthernetIf !== ifname) {
|
||||
root.activeEthernetIf = ifname;
|
||||
}
|
||||
ethernetDeviceShowProcess.ifname = ifname;
|
||||
ethernetDeviceShowProcess.running = true;
|
||||
} else {
|
||||
@@ -488,11 +568,13 @@ Singleton {
|
||||
const lines = text.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line)
|
||||
continue;
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
const idx = line.indexOf(":");
|
||||
if (idx === -1)
|
||||
continue;
|
||||
if (idx === -1) {
|
||||
continue;
|
||||
}
|
||||
const key = line.substring(0, idx);
|
||||
const val = line.substring(idx + 1);
|
||||
if (key === "GENERAL.CONNECTION") {
|
||||
@@ -502,8 +584,9 @@ Singleton {
|
||||
} else if (key === "IP4.GATEWAY") {
|
||||
gw4 = val;
|
||||
} else if (key.indexOf("IP4.DNS") === 0) {
|
||||
if (val && dnsServers.indexOf(val) === -1)
|
||||
dnsServers.push(val);
|
||||
if (val && dnsServers.indexOf(val) === -1) {
|
||||
dnsServers.push(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
details.ifname = ethernetDeviceShowProcess.ifname;
|
||||
@@ -595,8 +678,9 @@ Singleton {
|
||||
details.speed = speedText;
|
||||
// Try to derive numeric value
|
||||
const m = speedText.match(/([0-9]+(?:\.[0-9]+)?)\s*Mbit\/s/i);
|
||||
if (m)
|
||||
details.speedMbit = parseFloat(m[1]);
|
||||
if (m) {
|
||||
details.speedMbit = parseFloat(m[1]);
|
||||
}
|
||||
root.activeEthernetDetails = details;
|
||||
}
|
||||
root.activeEthernetDetailsTimestamp = Date.now();
|
||||
@@ -669,11 +753,13 @@ Singleton {
|
||||
const lines = text.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (!line)
|
||||
continue;
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
const idx = line.indexOf(":");
|
||||
if (idx === -1)
|
||||
continue;
|
||||
if (idx === -1) {
|
||||
continue;
|
||||
}
|
||||
const key = line.substring(0, idx);
|
||||
const val = line.substring(idx + 1);
|
||||
if (key.indexOf("IP4.ADDRESS") === 0) {
|
||||
@@ -764,8 +850,9 @@ Singleton {
|
||||
var compact = [];
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var p = parts[i];
|
||||
if (p && p.length > 0)
|
||||
if (p && p.length > 0) {
|
||||
compact.push(p);
|
||||
}
|
||||
}
|
||||
// Find a token that represents Mbit/s and use the previous number
|
||||
var unitIdx = -1;
|
||||
@@ -841,7 +928,6 @@ Singleton {
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
Logger.i("Network", "Wi-Fi state change command executed");
|
||||
// Re-check the state to ensure it's in sync
|
||||
syncWifiState();
|
||||
}
|
||||
@@ -995,8 +1081,9 @@ Singleton {
|
||||
|
||||
for (var i = 0; i < lines.length; ++i) {
|
||||
const line = lines[i].trim();
|
||||
if (!line)
|
||||
continue;
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse from the end to handle SSIDs with colons
|
||||
// Format is SSID:SECURITY:SIGNAL:IN-USE
|
||||
@@ -1182,7 +1269,7 @@ Singleton {
|
||||
root.connecting = false;
|
||||
root.connectingTo = "";
|
||||
Logger.i("Network", "Connected to network: '" + connectProcess.ssid + "'");
|
||||
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("toast.wifi.connected", {
|
||||
ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.wifi.connected", {
|
||||
"ssid": connectProcess.ssid
|
||||
}), "wifi");
|
||||
|
||||
@@ -1213,7 +1300,7 @@ Singleton {
|
||||
|
||||
Logger.w("Network", "Connect error: " + text);
|
||||
// Notify user about the failure
|
||||
ToastService.showWarning(I18n.tr("wifi.panel.title"), root.lastError || I18n.tr("toast.wifi.connection-failed"));
|
||||
ToastService.showWarning(I18n.tr("common.wifi"), root.lastError || I18n.tr("toast.wifi.connection-failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1228,7 +1315,7 @@ Singleton {
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
Logger.i("Network", "Disconnected from network: '" + disconnectProcess.ssid + "'");
|
||||
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("toast.wifi.disconnected", {
|
||||
ToastService.showNotice(I18n.tr("common.wifi"), I18n.tr("toast.wifi.disconnected", {
|
||||
"ssid": disconnectProcess.ssid
|
||||
}), "wifi-off");
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ Singleton {
|
||||
readonly property string envRealName: (Quickshell.env("NOCTALIA_REALNAME") || "")
|
||||
property string realName: ""
|
||||
|
||||
// Machine info
|
||||
property string hostName: ""
|
||||
|
||||
// Internal: pending logo name for fallback after probe fails
|
||||
property string pendingLogoName: ""
|
||||
|
||||
@@ -188,4 +191,17 @@ Singleton {
|
||||
}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
|
||||
// Read /etc/hostname
|
||||
FileView {
|
||||
id: hostName
|
||||
path: "/etc/hostname"
|
||||
onLoaded: {
|
||||
const name = text().trim();
|
||||
if (name) {
|
||||
root.hostName = name;
|
||||
Logger.i("HostService", "resolved hostname", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user