Merge pull request #1743 from turannul/pr/bluetooth-refactor-pt1

Bluetooth Panel Rework pt1
This commit is contained in:
Lemmy
2026-02-13 16:07:44 -05:00
committed by GitHub
23 changed files with 1299 additions and 1153 deletions
+18 -5
View File
@@ -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}",
+4 -2
View File
@@ -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,
+31 -10
View File
@@ -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",
+1 -1
View File
@@ -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",
+3 -1
View File
@@ -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
+10 -1
View File
@@ -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);
}
+13 -4
View File
@@ -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 WiFi, then Unpair, then main CTA)
RowLayout {
spacing: Style.marginS
// Info for connected device (placed before the CTA for consistency with WiFi)
NIconButton {
visible: modelData.connected
icon: "info"
tooltipText: I18n.tr("common.info")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
const key = BluetoothService.deviceKey(modelData);
root.expandedDeviceKey = (root.expandedDeviceKey === key) ? "" : key;
}
}
// Unpair for saved devices when not connected
NIconButton {
visible: (modelData.paired || modelData.trusted) && !modelData.connected && !isBusy && !modelData.blocked
icon: "trash"
tooltipText: I18n.tr("common.unpair")
baseSize: Style.baseWidgetSize * 0.8
onClicked: BluetoothService.unpairDevice(modelData)
}
// Main Call to action
NButton {
id: button
visible: (modelData.state !== BluetoothDeviceState.Connecting)
enabled: (canConnect || canDisconnect || canPair) && !isBusy
outlined: !button.hovered
fontSize: Style.fontSizeS
tooltipText: root.tooltipText
backgroundColor: modelData.connected ? Color.mError : Color.mPrimary
text: {
if (modelData.pairing) {
return I18n.tr("common.pairing");
}
if (modelData.blocked) {
return I18n.tr("bluetooth.panel.blocked");
}
if (modelData.connected) {
return I18n.tr("common.disconnect");
}
if (device.canPair) {
return I18n.tr("common.pair");
}
return I18n.tr("common.connect");
}
icon: (isBusy ? "busy" : null)
onClicked: {
if (modelData.connected) {
BluetoothService.disconnectDevice(modelData);
} else {
if (device.canPair) {
BluetoothService.pairDevice(modelData);
} else {
BluetoothService.connectDeviceWithTrust(modelData);
}
}
}
}
}
}
// Expanded info section
Rectangle {
visible: device.isExpanded
Layout.fillWidth: true
implicitHeight: infoColumn.implicitHeight + Style.marginS * 2
radius: Style.radiusS
color: Color.mSurfaceVariant
border.width: Style.borderS
border.color: Color.mOutline
clip: true
onVisibleChanged: {
if (visible && infoColumn && infoColumn.forceLayout) {
Qt.callLater(function () {
infoColumn.forceLayout();
});
}
}
// Grid/List toggle moved here to the top-right corner of the info box
NIconButton {
id: detailsToggle
anchors.top: parent.top
anchors.right: parent.right
anchors.margins: Style.marginS
// Use Tabler layout icons; "grid" alone doesn't exist in our font
icon: root.detailsGrid ? "layout-list" : "layout-grid"
tooltipText: root.detailsGrid ? I18n.tr("tooltips.list-view") : I18n.tr("tooltips.grid-view")
baseSize: Style.baseWidgetSize * 0.8
onClicked: {
root.detailsGrid = !root.detailsGrid;
if (Settings.data && Settings.data.ui) {
Settings.data.network.bluetoothDetailsViewMode = root.detailsGrid ? "grid" : "list";
}
}
z: 1
}
GridLayout {
id: infoColumn
anchors.fill: parent
anchors.margins: Style.marginS
// Layout toggle based on local state
columns: root.detailsGrid ? 2 : 1
columnSpacing: Style.marginM
rowSpacing: Style.marginXS
// Ensure proper relayout when switching grid/list while open
onColumnsChanged: {
if (infoColumn.forceLayout) {
Qt.callLater(function () {
infoColumn.forceLayout();
});
}
}
// Icons only; labels shown as tooltips on hover
// Row 1: Signal | Battery
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
spacing: Style.marginXS
NIcon {
icon: BluetoothService.getSignalIcon(modelData)
pointSize: Style.fontSizeXS
color: Color.mOnSurface
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, I18n.tr("common.signal"))
onExited: TooltipService.hide()
}
}
NText {
text: BluetoothService.getSignalStrength(modelData)
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
// Wrap only when needed to avoid extra spacing
wrapMode: implicitWidth > width ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap
elide: Text.ElideNone
maximumLineCount: 4
clip: true
}
}
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
spacing: Style.marginXS
NIcon {
icon: {
var b = BluetoothService.getBatteryPercent(modelData);
return BatteryService.getIcon(b !== null ? b : 0, false, false, b !== null);
}
pointSize: Style.fontSizeXS
color: Color.mOnSurface
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, I18n.tr("common.battery"))
onExited: TooltipService.hide()
}
}
NText {
text: {
var b = BluetoothService.getBatteryPercent(modelData);
return b === null ? "-" : (b + "%");
}
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: implicitWidth > width ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap
elide: Text.ElideNone
maximumLineCount: 4
clip: true
}
}
// Row 2: Paired | Trusted
RowLayout {
Layout.fillWidth: true
spacing: Style.marginXS
NIcon {
icon: "link"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, I18n.tr("common.paired"))
onExited: TooltipService.hide()
}
}
NText {
text: modelData.paired ? I18n.tr("common.yes") : I18n.tr("common.no")
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: implicitWidth > width ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap
elide: Text.ElideNone
maximumLineCount: 2
clip: true
}
}
RowLayout {
Layout.fillWidth: true
spacing: Style.marginXS
NIcon {
icon: "shield-check"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, I18n.tr("common.trusted"))
onExited: TooltipService.hide()
}
}
NText {
text: modelData.trusted ? I18n.tr("common.yes") : I18n.tr("common.no")
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
wrapMode: implicitWidth > width ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap
elide: Text.ElideNone
maximumLineCount: 2
clip: true
}
}
// Row 3: Address (single row; spans two columns when grid)
RowLayout {
Layout.fillWidth: true
Layout.columnSpan: infoColumn.columns === 2 ? 2 : 1
spacing: Style.marginXS
NIcon {
icon: "hash"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: TooltipService.show(parent, I18n.tr("bluetooth.panel.device-address"))
onExited: TooltipService.hide()
}
}
NText {
id: macAddressText
text: modelData.address || "-"
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
// MAC addresses usually fit; wrap only if necessary
wrapMode: implicitWidth > width ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap
elide: Text.ElideNone
maximumLineCount: 2
clip: true
// Click-to-copy MAC address
MouseArea {
anchors.fill: parent
enabled: (modelData.address && modelData.address.length > 0)
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: TooltipService.show(parent, I18n.tr("tooltips.copy-address"))
onExited: TooltipService.hide()
onClicked: {
const addr = modelData.address || "";
if (addr.length > 0) {
// Copy to clipboard via wl-copy (runtime dependency)
Quickshell.execDetached(["wl-copy", addr]);
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.address-copied"), "bluetooth");
}
}
}
}
}
}
}
}
}
}
}
}
+18 -298
View File
@@ -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");
}
// WiFi: 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)
}
}
}
+7 -6
View File
@@ -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");
}
}
}
+2 -2
View File
@@ -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");
}
}
}
+5 -5
View File
@@ -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,
+1 -1
View File
@@ -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
}
}
+6 -1
View File
@@ -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) {
+145 -257
View File
@@ -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: besteffort 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 subcomponent
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 autostop 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 youve 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 dont 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);
}
+124 -37
View File
@@ -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");
+16
View File
@@ -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);
}
}
}
}