mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Bluetooth & Network enhancements
This commit is contained in:
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 (Wi‑Fi / 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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user