Bluetooth & Network enhancements

This commit is contained in:
notiant
2026-01-07 12:56:07 +01:00
committed by GitHub
parent bb73608282
commit 7e3500d6fc
5 changed files with 101 additions and 62 deletions
@@ -92,7 +92,7 @@ NBox {
radius: Style.radiusM
clip: true
color: modelData.connected ? Qt.alpha(getContentColor(), 0.08) : Color.mSurface
color: (modelData.pairing || modelData.state === BluetoothDeviceState.Connecting || modelData.connected || modelData.blocked) ? Qt.alpha(getContentColor(), 0.08) : Color.mSurface
// Content column so expanded details are laid out inside the card
ColumnLayout {
@@ -311,6 +311,7 @@ NBox {
// Row 1: Signal | Battery
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
spacing: Style.marginXS
NIcon {
icon: BluetoothService.getSignalIcon(modelData)
@@ -337,6 +338,7 @@ NBox {
}
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
spacing: Style.marginXS
NIcon {
icon: "battery"
@@ -469,4 +471,4 @@ NBox {
}
}
}
}
}
+15 -16
View File
@@ -19,11 +19,7 @@ SmartPanel {
id: panelContent
color: "transparent"
// Calculate content height based on header + devices list (or minimum for empty states)
property real headerHeight: headerRow.implicitHeight + Style.marginM * 2
property real devicesHeight: devicesList.implicitHeight
property real calculatedHeight: (devicesHeight !== 0) ? (headerHeight + devicesHeight + Style.marginL * 2 + Style.marginM) : (280 * Style.uiScaleRatio)
property real contentPreferredHeight: (BluetoothService.adapter && BluetoothService.adapter.enabled) ? Math.min(root.preferredHeight, calculatedHeight) : Math.min(root.preferredHeight, 280 * Style.uiScaleRatio)
property real contentPreferredHeight: Math.min(root.preferredHeight, mainColumn.implicitHeight + Style.marginL * 2)
ColumnLayout {
id: mainColumn
@@ -43,9 +39,9 @@ SmartPanel {
spacing: Style.marginM
NIcon {
icon: "bluetooth"
icon: BluetoothService.enabled ? "bluetooth" : "bluetooth-off"
pointSize: Style.fontSizeXXL
color: Color.mPrimary
color: BluetoothService.enabled ? Color.mPrimary : Color.mOnSurfaceVariant
}
NText {
@@ -98,12 +94,14 @@ SmartPanel {
id: disabledBox
visible: !(BluetoothService.adapter && BluetoothService.adapter.enabled)
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: disabledColumn.implicitHeight + Style.marginM * 2
// Center the content within this rectangle
ColumnLayout {
id: disabledColumn
anchors.fill: parent
spacing: Style.marginM
anchors.margins: Style.marginM
spacing: Style.marginL
Item {
Layout.fillHeight: true
@@ -278,10 +276,12 @@ SmartPanel {
return (availableCount === 0);
}
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: emptyColumn.implicitHeight + Style.marginM * 2
ColumnLayout {
id: emptyColumn
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginL
Item {
@@ -290,7 +290,7 @@ SmartPanel {
NIcon {
icon: "bluetooth"
pointSize: 64
pointSize: 48
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
@@ -320,7 +320,7 @@ SmartPanel {
// Fallback - No devices, scanning
NBox {
Layout.fillWidth: true
Layout.preferredHeight: columnScanning.implicitHeight + Style.marginM * 2
Layout.preferredHeight: scanningColumn.implicitHeight + Style.marginM * 2
visible: {
if (!(BluetoothService.adapter && BluetoothService.adapter.devices) || !BluetoothService.scanningActive) {
return false;
@@ -333,11 +333,10 @@ SmartPanel {
}
ColumnLayout {
id: columnScanning
id: scanningColumn
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginM
spacing: Style.marginL
RowLayout {
Layout.alignment: Qt.AlignHCenter
@@ -378,4 +377,4 @@ SmartPanel {
}
}
}
}
}
+7 -13
View File
@@ -121,6 +121,8 @@ SmartPanel {
panelContent: Rectangle {
color: "transparent"
property real contentPreferredHeight: Math.min(root.preferredHeight, mainColumn.implicitHeight + Style.marginL * 2)
ColumnLayout {
id: mainColumn
@@ -211,19 +213,12 @@ SmartPanel {
// Mode switch (WiFi / Ethernet)
NTabBar {
id: modeTabBar
visible: NetworkService.hasEthernet()
Layout.fillWidth: true
spacing: Style.marginM
distributeEvenly: true
currentIndex: root.panelViewMode === "wifi" ? 0 : 1
onCurrentIndexChanged: {
if (currentIndex === 1 && !NetworkService.hasEthernet()) {
// Revert selection if Ethernet is not available and inform the user
modeTabBar.currentIndex = 0;
if (typeof TooltipService !== "undefined") {
TooltipService.show(modeTabBar, I18n.tr("wifi.panel.no-ethernet-devices"));
}
return;
}
root.panelViewMode = (currentIndex === 0) ? "wifi" : "ethernet";
}
@@ -234,8 +229,6 @@ SmartPanel {
}
NTabButton {
// Dim when no Ethernet devices are detected
opacity: NetworkService.hasEthernet() ? 1.0 : 0.5
text: I18n.tr("control-center.wifi.label-ethernet")
tabIndex: 1
checked: modeTabBar.currentIndex === 1
@@ -305,7 +298,8 @@ SmartPanel {
id: disabledColumn
anchors.fill: parent
anchors.margins: Style.marginM
spacing: Style.marginL
Item {
Layout.fillHeight: true
}
@@ -395,7 +389,7 @@ SmartPanel {
NIcon {
icon: "search"
pointSize: 64
pointSize: 48
color: Color.mOnSurfaceVariant
Layout.alignment: Qt.AlignHCenter
}
@@ -900,4 +894,4 @@ SmartPanel {
}
}
}
}
}
+3 -1
View File
@@ -357,6 +357,7 @@ NBox {
// Row 1: Interface | Band
RowLayout {
Layout.fillWidth: true
Layout.preferredWidth: 1
spacing: Style.marginXS
NIcon {
icon: "network"
@@ -376,6 +377,7 @@ NBox {
pointSize: Style.fontSizeXS
color: Color.mOnSurface
Layout.fillWidth: true
Layout.preferredWidth: 1
Layout.alignment: Qt.AlignVCenter
wrapMode: root.detailsGrid ? Text.NoWrap : Text.WrapAtWordBoundaryOrAnywhere
elide: root.detailsGrid ? Text.ElideRight : Text.ElideNone
@@ -684,4 +686,4 @@ NBox {
}
}
}
}
}
+72 -30
View File
@@ -1,5 +1,4 @@
pragma Singleton
import QtQml
import QtQuick
import Quickshell
@@ -10,19 +9,21 @@ import "."
import qs.Commons
import qs.Services.UI
QtObject {
Singleton {
id: root
// ---- Constants (centralized tunables) ----
// Constants (centralized tunables)
readonly property int ctlPollMs: 1500
readonly property int ctlPollSoonMs: 250
readonly property int scanAutoStopMs: 6000
property bool airplaneModeToggled: false
property bool lastBluetoothBlocked: false
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter
// Power/blocked state
property bool enabled: false // driven by bluetoothctl
readonly property bool enabled: adapter ? adapter.enabled : root.ctlPowered
readonly property bool blocked: adapter?.state === BluetoothAdapterState.Blocked
property bool ctlPowered: false
property bool ctlDiscovering: false
property bool ctlDiscoverable: false
@@ -60,7 +61,8 @@ QtObject {
property bool _discoveryWasRunning: false
property double _discoveryResumeAtMs: 0
// Timer used to restore discovery after temporary pause during pair/connect
property Timer restoreDiscoveryTimer: Timer {
Timer {
id: restoreDiscoveryTimer
repeat: false
onTriggered: {
const now = Date.now();
@@ -97,8 +99,8 @@ QtObject {
}
// Persistent process for fallback scanning to keep the session alive
property Process fallbackScanProcess: Process {
id: fallbackProc
Process {
id: fallbackScanProcess
// Pipe scan on and a long sleep to bluetoothctl to keep it running
command: ["sh", "-c", "(echo 'scan on'; sleep 3600) | bluetoothctl"]
onExited: Logger.d("Bluetooth", "Fallback scan process exited")
@@ -180,7 +182,8 @@ QtObject {
}
// Auto-stop manual discovery after a short window
property Timer manualScanTimer: Timer {
Timer {
id: manualScanTimer
repeat: false
onTriggered: {
// Logger.e("Bluetooth", "manualScanTimer triggered");
@@ -212,25 +215,69 @@ QtObject {
// No implicit discovery auto-start; state polled from bluetoothctl instead
// Track adapter state changes (for enabled/disabled logging only; avoid discovery writes here)
property Connections adapterConnections: Connections {
// Track adapter state changes
Connections {
target: adapter
function onStateChanged() {
if (!adapter)
return;
Logger.d("Bluetooth", "Adapter state changed: " + adapter.state);
if (adapter.state === BluetoothAdapter.Enabled) {
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("toast.wifi.enabled"), "bluetooth");
Logger.d("Bluetooth", "Adapter enabled");
// Keep UI default to refresh icon; bluetoothctl polling will set ctlDiscovering accordingly.
} else if (adapter.state === BluetoothAdapter.Disabled) {
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("toast.wifi.disabled"), "bluetooth-off");
Logger.d("Bluetooth", "Adapter disabled");
}
}
}
Process {
id: checkWifiBlocked
running: false
command: ["rfkill", "list", "wifi"]
stdout: StdioCollector {
onStreamFinished: {
var wifiBlocked = text && text.trim().indexOf("Soft blocked: yes") !== -1;
Logger.d("Network", "Wi-Fi adapter was detected as blocked:", wifiBlocked);
// Check if airplane mode has been toggled
if (wifiBlocked && wifiBlocked === root.blocked) {
root.airplaneModeToggled = true;
NetworkService.setWifiEnabled(false);
ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("toast.wifi.enabled"), "plane");
} else if (!wifiBlocked && wifiBlocked === root.blocked) {
root.airplaneModeToggled = true;
NetworkService.setWifiEnabled(true);
ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("toast.wifi.disabled"), "plane-off");
} else if (adapter.enabled) {
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("toast.wifi.enabled"), "bluetooth");
Logger.d("Bluetooth", "Adapter enabled");
} else {
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("toast.wifi.disabled"), "bluetooth-off");
Logger.d("Bluetooth", "Adapter disabled");
}
root.airplaneModeToggled = false;
}
}
stderr: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
Logger.w("Bluetooth", "rfkill (wifi) stderr:", text.trim());
}
}
}
}
// --- bluetoothctl state polling ---
property Process ctlShowProcess: Process {
id: ctlProc
// bluetoothctl state polling
Process {
id: ctlShowProcess
running: false
stdout: StdioCollector {
id: ctlStdout
@@ -243,7 +290,6 @@ QtObject {
var mp = text.match(/\bPowered:\s*(yes|no)\b/i);
if (mp && mp.length > 1) {
root.ctlPowered = (mp[1].toLowerCase() === "yes");
root.enabled = root.ctlPowered;
}
var md = text.match(/\bDiscoverable:\s*(yes|no)\b/i);
if (md && md.length > 1) {
@@ -262,16 +308,17 @@ QtObject {
}
function pollCtlState() {
if (ctlProc.running)
if (ctlShowProcess.running)
return;
try {
ctlProc.command = ["bluetoothctl", "show"];
ctlProc.running = true;
ctlShowProcess.command = ["bluetoothctl", "show"];
ctlShowProcess.running = true;
} catch (_) {}
}
// Periodic state polling
property Timer ctlPollTimer: Timer {
Timer {
id: ctlPollTimer
interval: ctlPollMs
repeat: true
running: root.enabled
@@ -279,7 +326,8 @@ QtObject {
}
// Short-delay poll scheduler
property Timer pollCtlStateSoonTimer: Timer {
Timer {
id: pollCtlStateSoonTimer
interval: ctlPollSoonMs
repeat: false
onTriggered: pollCtlState()
@@ -296,12 +344,6 @@ QtObject {
try {
btExec(["bluetoothctl", "power", state ? "on" : "off"]);
root.ctlPowered = !!state;
root.enabled = root.ctlPowered;
if (state) {
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("toast.wifi.enabled"), "bluetooth");
} else {
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("toast.wifi.disabled"), "bluetooth-off");
}
requestCtlPoll(ctlPollSoonMs);
} catch (e) {
Logger.w("Bluetooth", "Enable/Disable failed", e);
@@ -489,7 +531,7 @@ QtObject {
btExec(["bash", scriptPath, String(addr), String(pairWait), String(attempts), String(intervalSec)]);
}
// --- Helper to run bluetoothctl and scripts with consistent error logging ---
// Helper to run bluetoothctl and scripts with consistent error logging
function btExec(args) {
try {
Quickshell.execDetached(args);
@@ -557,4 +599,4 @@ QtObject {
ToastService.showWarning(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.forget-failed"));
}
}
}
}