mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Added airpllane toggle. Minimal wifi UI
This commit is contained in:
@@ -505,7 +505,8 @@
|
||||
"width": "Width",
|
||||
"windows": "Windows",
|
||||
"yes": "Yes",
|
||||
"confirm": "Confirm"
|
||||
"confirm": "Confirm",
|
||||
"settings": "Settings"
|
||||
},
|
||||
"control-center": {
|
||||
"power-profile": {
|
||||
|
||||
@@ -99,8 +99,8 @@
|
||||
"clockStyle": "custom",
|
||||
"clockFormat": "hh\\nmm",
|
||||
"lockScreenMonitors": [],
|
||||
"lockScreenBlur": 0.0,
|
||||
"lockScreenTint": 0.0,
|
||||
"lockScreenBlur": 0,
|
||||
"lockScreenTint": 0,
|
||||
"keybinds": {
|
||||
"keyUp": [
|
||||
"Up"
|
||||
@@ -335,6 +335,7 @@
|
||||
},
|
||||
"network": {
|
||||
"wifiEnabled": true,
|
||||
"airplaneModeEnabled": false,
|
||||
"bluetoothRssiPollingEnabled": false,
|
||||
"bluetoothRssiPollIntervalMs": 60000,
|
||||
"wifiDetailsViewMode": "grid",
|
||||
|
||||
@@ -540,6 +540,7 @@ Singleton {
|
||||
// network
|
||||
property JsonObject network: JsonObject {
|
||||
property bool wifiEnabled: true
|
||||
property bool airplaneModeEnabled: false // New property for persistent airplane mode state
|
||||
property bool bluetoothRssiPollingEnabled: false // Opt-in Bluetooth RSSI polling (uses bluetoothctl)
|
||||
property int bluetoothRssiPollIntervalMs: 60000 // Polling interval in milliseconds for RSSI queries
|
||||
property string wifiDetailsViewMode: "grid" // "grid" or "list"
|
||||
|
||||
@@ -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
|
||||
@@ -49,6 +50,16 @@ Item {
|
||||
"action": "toggle-wifi",
|
||||
"icon": Settings.data.network.wifiEnabled ? "wifi-off" : "wifi"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("tooltips.manage-wifi") + " " + I18n.tr("tooltips.open-settings"),
|
||||
"action": "wifi-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("panels.connections.ethernet") + " " + I18n.tr("tooltips.open-settings"),
|
||||
"action": "ethernet-settings",
|
||||
"icon": "settings"
|
||||
},
|
||||
{
|
||||
"label": I18n.tr("actions.widget-settings"),
|
||||
"action": "widget-settings",
|
||||
@@ -62,7 +73,11 @@ 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 === "ethernet-settings") {
|
||||
SettingsPanelService.openToTab(SettingsPanel.Tab.Connections, 2, screen);
|
||||
}else if (action === "widget-settings") {
|
||||
BarService.openWidgetSettings(screen, section, sectionWidgetIndex, widgetId, widgetSettings);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,7 @@ Item {
|
||||
|
||||
NToggle {
|
||||
checked: BluetoothService.enabled
|
||||
enabled: !NetworkService.bluetoothBlocked || !Settings.data.network.airplaneModeEnabled
|
||||
onToggled: checked => BluetoothService.setBluetoothEnabled(checked)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
@@ -1,22 +1,106 @@
|
||||
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
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
spacing: Style.marginL
|
||||
Item {
|
||||
id: wifiprefs
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: mainLayout.implicitHeight
|
||||
|
||||
NToggle {
|
||||
label: I18n.tr("actions.enable-wifi")
|
||||
description: I18n.tr("panels.connections.wifi-description")
|
||||
checked: ProgramCheckerService.nmcliAvailable && Settings.data.network.wifiEnabled
|
||||
onToggled: checked => NetworkService.setWifiEnabled(checked)
|
||||
enabled: ProgramCheckerService.nmcliAvailable
|
||||
// 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: (NetworkService.bluetoothBlocked || NetworkService.wifiBlocked) ? "plane" : "plane-off"
|
||||
pointSize: Style.fontSizeXXL
|
||||
color: (NetworkService.bluetoothBlocked || NetworkService.wifiBlocked) ? Color.mPrimary : Color.mOnSurfaceVariant
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("toast.airplane-mode.title")
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
checked: Settings.data.network.airplaneModeEnabled
|
||||
onToggled: checked => {
|
||||
Settings.data.network.airplaneModeEnabled = checked; // Store the new state
|
||||
NetworkService.setAirplaneMode(checked); // Call new function
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
NText {
|
||||
text: I18n.tr("tooltips.manage-wifi")
|
||||
pointSize: Style.fontSizeL
|
||||
font.weight: Style.fontWeightBold
|
||||
color: Color.mOnSurface
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
NToggle {
|
||||
checked: Settings.data.network.wifiEnabled
|
||||
onToggled: checked => NetworkService.setWifiEnabled(checked)
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
enabled: !NetworkService.wifiBlocked || !Settings.data.network.airplaneModeEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ ColumnLayout {
|
||||
}
|
||||
NTabButton {
|
||||
text: I18n.tr("panels.connections.ethernet")
|
||||
// visible: NetworkService.wifiAvailable
|
||||
enabled: NetworkService.wifiAvailable // Remove when work finished, only use visibility
|
||||
// visible: NetworkService.ethernetAvailable
|
||||
enabled: NetworkService.ethernetAvailable // Remove when work finished, only use visibility
|
||||
tabIndex: 2
|
||||
checked: subTabBar.currentIndex === 2
|
||||
}
|
||||
|
||||
@@ -15,19 +15,16 @@ Singleton {
|
||||
readonly property int ctlPollMs: 10000
|
||||
readonly property int ctlPollSoonMs: 250
|
||||
|
||||
property bool airplaneModeToggled: false
|
||||
property bool lastBluetoothBlocked: false
|
||||
property bool lastWifiBlocked: false
|
||||
readonly property BluetoothAdapter adapter: Bluetooth.defaultAdapter
|
||||
|
||||
// Power/blocked/availability state
|
||||
readonly property bool bluetoothAvailable: adapter ? adapter !== null : false
|
||||
property bool ctlAvailable: false
|
||||
readonly property bool bluetoothAvailable: !!adapter
|
||||
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
|
||||
|
||||
// Adapter discoverability (advertising) flag (driven by bluetoothctl)
|
||||
readonly property bool discoverable: root.ctlDiscoverable
|
||||
readonly property var devices: adapter ? adapter.devices : null
|
||||
@@ -35,16 +32,17 @@ Singleton {
|
||||
if (!adapter || !adapter.devices) {
|
||||
return [];
|
||||
}
|
||||
return adapter.devices.values.filter(function (dev) {
|
||||
return dev && dev.connected;
|
||||
});
|
||||
return adapter.devices.values.filter(dev => dev && dev.connected);
|
||||
}
|
||||
|
||||
// Current backend in use for scanning (e.g., "native", "bluetoothctl")
|
||||
property string backendUsed: ""
|
||||
|
||||
// Experimental: best‑effort RSSI polling for connected devices (without root)
|
||||
// Enabled in debug mode or via user setting in Settings > Network
|
||||
property bool rssiPollingEnabled: (Settings && (Settings.isDebug || (Settings.data && Settings.data.network && Settings.data.network.bluetoothRssiPollingEnabled))) ? true : false
|
||||
property bool rssiPollingEnabled: Settings?.data?.network?.bluetoothRssiPollingEnabled || Settings?.isDebug || false
|
||||
// Interval can be configured from Settings; defaults to 60s
|
||||
property int rssiPollIntervalMs: (Settings && Settings.data && Settings.data.network && Settings.data.network.bluetoothRssiPollIntervalMs) ? Settings.data.network.bluetoothRssiPollIntervalMs : 60000
|
||||
property int rssiPollIntervalMs: Settings?.data?.network?.bluetoothRssiPollIntervalMs || 60000
|
||||
// RSSI helper sub‑component
|
||||
property BluetoothRssi rssi: BluetoothRssi {
|
||||
enabled: root.enabled && root.rssiPollingEnabled
|
||||
@@ -60,57 +58,36 @@ Singleton {
|
||||
// Internal: temporarily pause discovery during pair/connect to reduce HCI churn
|
||||
property bool _discoveryWasRunning: false
|
||||
|
||||
// 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
|
||||
// Afaik we don't need bluetoothctl scanning for an entire hour. back then this was made to save to day. (when i originaly wrote this)
|
||||
command: ["sh", "-c", "(echo 'scan on'; sleep 30) | bluetoothctl"] // trap is not sending sigterm to bctl, guess what to parent (qs)... | BAD Idea should have guessed that, expected to sh to crash and stop.
|
||||
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
|
||||
function setScanActive(active) {
|
||||
Logger.d("Bluetooth", "setScanActive called with active=" + active); // used for debugging
|
||||
// Prefer Quickshell API if available, fallback to bluetoothctl
|
||||
var nativeSuccess = false;
|
||||
try {
|
||||
if (adapter) {
|
||||
if (active && adapter.discovering !== undefined) {
|
||||
Logger.d("Bluetooth", "Starting discovery with Quickshell API"); // used for debugging
|
||||
adapter.discovering = true;
|
||||
nativeSuccess = true;
|
||||
} else if (!active && adapter.discovering !== undefined) {
|
||||
Logger.d("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.d("Bluetooth", "Starting fallback scan process");
|
||||
fallbackScanProcess.running = true;
|
||||
bluetoothctlScanProcess.running = true;
|
||||
} else {
|
||||
Logger.d("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.d("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;
|
||||
}
|
||||
|
||||
requestCtlPoll(ctlPollSoonMs);
|
||||
@@ -123,76 +100,20 @@ Singleton {
|
||||
Logger.i("Bluetooth", "Service started");
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// Prime state immediately so UI reflects correct power/discovery flags
|
||||
pollCtlState();
|
||||
}
|
||||
|
||||
// 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
|
||||
Component.onCompleted: pollCtlState()
|
||||
|
||||
// Track adapter state changes
|
||||
Connections {
|
||||
target: adapter
|
||||
function onStateChanged() {
|
||||
if (!adapter) {
|
||||
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");
|
||||
}
|
||||
if (!adapter || adapter.state === BluetoothAdapter.Enabling || adapter.state === BluetoothAdapter.Disabling) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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 && root.blocked) {
|
||||
root.airplaneModeToggled = true;
|
||||
root.lastWifiBlocked = true;
|
||||
NetworkService.setWifiEnabled(false);
|
||||
ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.enabled"), "plane");
|
||||
} else if (!wifiBlocked && lastWifiBlocked) {
|
||||
root.airplaneModeToggled = true;
|
||||
root.lastWifiBlocked = false;
|
||||
NetworkService.setWifiEnabled(true);
|
||||
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");
|
||||
} else {
|
||||
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.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());
|
||||
}
|
||||
if (adapter.state === BluetoothAdapter.Enabled) {
|
||||
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.enabled"), "bluetooth");
|
||||
} else if (adapter.state === BluetoothAdapter.Disabled && !root.blocked) {
|
||||
ToastService.showNotice(I18n.tr("common.bluetooth"), I18n.tr("common.disabled"), "bluetooth-off");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -201,25 +122,21 @@ Singleton {
|
||||
Process {
|
||||
id: ctlShowProcess
|
||||
running: false
|
||||
stdout: StdioCollector {
|
||||
id: ctlStdout
|
||||
}
|
||||
stdout: StdioCollector { id: ctlStdout }
|
||||
onExited: function (exitCode, exitStatus) {
|
||||
try {
|
||||
var text = ctlStdout.text || "";
|
||||
Logger.d("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) {
|
||||
root.ctlDiscovering = (ms[1].toLowerCase() === "yes");
|
||||
var lines = text.split('\n');
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i].trim();
|
||||
var mp = line.match(/\bPowered:\s*(yes|no)\b/i);
|
||||
if (mp) { root.ctlPowered = (mp[1].toLowerCase() === "yes"); }
|
||||
|
||||
var md = line.match(/\bDiscoverable:\s*(yes|no)\b/i);
|
||||
if (md) { root.ctlDiscoverable = (md[1].toLowerCase() === "yes"); }
|
||||
|
||||
var ms = line.match(/\bDiscovering:\s*(yes|no)\b/i);
|
||||
if (ms) { root.ctlDiscovering = (ms[1].toLowerCase() === "yes"); }
|
||||
}
|
||||
} catch (e) {
|
||||
Logger.d("Bluetooth", "Failed to parse bluetoothctl show output", e);
|
||||
@@ -243,20 +160,17 @@ Singleton {
|
||||
interval: ctlPollMs
|
||||
repeat: true
|
||||
running: root.enabled
|
||||
onTriggered: pollCtlState()
|
||||
}
|
||||
|
||||
// Short-delay poll scheduler
|
||||
Timer {
|
||||
id: pollCtlStateSoonTimer
|
||||
interval: ctlPollSoonMs
|
||||
repeat: false
|
||||
onTriggered: pollCtlState()
|
||||
onTriggered: {
|
||||
pollCtlState();
|
||||
if (interval !== ctlPollMs) {
|
||||
interval = ctlPollMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -264,8 +178,7 @@ Singleton {
|
||||
target: Time
|
||||
function onResumed() {
|
||||
Logger.i("Bluetooth", "System resumed - forcing state poll");
|
||||
ctlPollTimer.restart();
|
||||
pollCtlState();
|
||||
requestCtlPoll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,15 +240,6 @@ Singleton {
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
Paired
|
||||
Means you’ve successfully exchanged keys with the device.
|
||||
The devices remember each other and can authenticate without repeating the pairing process.
|
||||
Example: once your headphones are paired, you don’t need to type a PIN every time.
|
||||
Hence, instead of !device.paired, should be device.connected
|
||||
*/
|
||||
// Only allow connect if device is already paired or trusted
|
||||
return !device.connected && (device.paired || device.trusted) && !device.pairing && !device.blocked;
|
||||
}
|
||||
|
||||
@@ -349,16 +253,21 @@ Singleton {
|
||||
// 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");
|
||||
}
|
||||
|
||||
@@ -382,7 +291,6 @@ Singleton {
|
||||
if (!device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return device.pairing || device.state === BluetoothDevice.Disconnecting || device.state === BluetoothDevice.Connecting;
|
||||
}
|
||||
|
||||
@@ -410,7 +318,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) {
|
||||
@@ -441,8 +348,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");
|
||||
@@ -477,13 +383,11 @@ 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);
|
||||
@@ -496,7 +400,6 @@ Singleton {
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -516,20 +419,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);
|
||||
}
|
||||
|
||||
@@ -570,4 +468,4 @@ Singleton {
|
||||
ToastService.showWarning(I18n.tr("common.bluetooth"), I18n.tr("toast.bluetooth.forget-failed"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,18 +70,22 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
property bool airplaneModeToggled: false
|
||||
property bool bluetoothBlocked: false
|
||||
property bool wifiBlocked: false
|
||||
|
||||
Connections {
|
||||
target: Settings.data.network
|
||||
function onWifiEnabledChanged() {
|
||||
if (Settings.data.network.wifiEnabled) {
|
||||
if (!BluetoothService.airplaneModeToggled) {
|
||||
if (!root.airplaneModeToggled) {
|
||||
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("common.enabled"), "wifi");
|
||||
}
|
||||
// Perform a scan to update the UI
|
||||
delayedScanTimer.interval = 3000;
|
||||
delayedScanTimer.restart();
|
||||
} else {
|
||||
if (!BluetoothService.airplaneModeToggled) {
|
||||
if (!root.airplaneModeToggled) {
|
||||
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("common.disabled"), "wifi-off");
|
||||
}
|
||||
// Clear networks so the widget icon changes
|
||||
@@ -90,6 +94,100 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
// Handle Airplane Mode detection via rfkill
|
||||
Process {
|
||||
id: checkWifiBlocked
|
||||
running: false
|
||||
command: ["rfkill", "list", "wifi"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var wifiBlocked = text && text.trim().indexOf("Soft blocked: yes") !== -1;
|
||||
checkBluetoothBlocked.wifiBlockedState = wifiBlocked;
|
||||
checkBluetoothBlocked.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: checkBluetoothBlocked
|
||||
running: false
|
||||
command: ["rfkill", "list", "bluetooth"]
|
||||
property bool wifiBlockedState: false // To pass state from checkWifiBlocked
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
var wifiBlocked = checkBluetoothBlocked.wifiBlockedState;
|
||||
var btBlocked = text && text.trim().indexOf("Soft blocked: yes") !== -1;
|
||||
|
||||
// Check if airplane mode is desired by the user
|
||||
var desiredAirplaneMode = Settings.data.network.airplaneModeEnabled;
|
||||
|
||||
// Track if actual state changed
|
||||
var actualAirplaneModeActive = wifiBlocked && btBlocked;
|
||||
var previousAirplaneModeActive = root.wifiBlocked && root.bluetoothBlocked;
|
||||
|
||||
// Enforcement: If desired state doesn't match actual state, force it.
|
||||
if (desiredAirplaneMode && !actualAirplaneModeActive) {
|
||||
// User wants airplane mode ON, but it's not actually active. Force it.
|
||||
Logger.i("Network", "Enforcing Airplane Mode ON (rfkill block all)");
|
||||
Quickshell.execDetached(["rfkill", "block", "wifi"]);
|
||||
Quickshell.execDetached(["rfkill", "block", "bluetooth"]);
|
||||
// We expect subsequent rfkill checks to confirm the state change.
|
||||
} else if (!desiredAirplaneMode && actualAirplaneModeActive) {
|
||||
// User wants airplane mode OFF, but it's still active. Force it off.
|
||||
Logger.i("Network", "Enforcing Airplane Mode OFF (rfkill unblock all)");
|
||||
Quickshell.execDetached(["rfkill", "unblock", "wifi"]);
|
||||
Quickshell.execDetached(["rfkill", "unblock", "bluetooth"]);
|
||||
// We expect subsequent rfkill checks to confirm the state change.
|
||||
}
|
||||
|
||||
// Now handle toasts and update internal state based on current rfkill states.
|
||||
// This part needs to be outside the enforcement blocks to correctly react to delayed rfkill changes.
|
||||
if (actualAirplaneModeActive && !previousAirplaneModeActive) {
|
||||
root.airplaneModeToggled = true; // Temporarily set to suppress individual toasts
|
||||
ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.enabled"), "plane");
|
||||
root.airplaneModeToggled = false;
|
||||
} else if (!actualAirplaneModeActive && previousAirplaneModeActive) {
|
||||
root.airplaneModeToggled = true; // Temporarily set to suppress individual toasts
|
||||
ToastService.showNotice(I18n.tr("toast.airplane-mode.title"), I18n.tr("common.disabled"), "plane-off");
|
||||
root.airplaneModeToggled = false;
|
||||
} else {
|
||||
// Standard state change notifications for WiFi only, if not in airplane mode context
|
||||
if (wifiBlocked !== root.wifiBlocked) {
|
||||
// Only show individual wifi toast if airplane mode is not currently active
|
||||
if (!actualAirplaneModeActive) {
|
||||
if (wifiBlocked) {
|
||||
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("common.disabled"), "wifi-off");
|
||||
} else {
|
||||
ToastService.showNotice(I18n.tr("wifi.panel.title"), I18n.tr("common.enabled"), "wifi");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update current blocked states (always reflect actual rfkill state)
|
||||
root.wifiBlocked = wifiBlocked;
|
||||
root.bluetoothBlocked = btBlocked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 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;
|
||||
checkWifiBlocked.running = true; // Refresh airplane mode state after resume
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
Logger.i("Network", "Service started");
|
||||
if (ProgramCheckerService.nmcliAvailable) {
|
||||
@@ -100,6 +198,7 @@ Singleton {
|
||||
ethernetStateProcess.running = true;
|
||||
refreshActiveWifiDetails();
|
||||
refreshActiveEthernetDetails();
|
||||
checkWifiBlocked.running = true; // Trigger airplane mode check on startup
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,8 +263,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)
|
||||
@@ -199,8 +299,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.
|
||||
@@ -226,23 +327,40 @@ Singleton {
|
||||
onTriggered: connectivityCheckProcess.running = true
|
||||
}
|
||||
|
||||
function setAirplaneMode(enabled) {
|
||||
if (enabled) {
|
||||
Logger.i("Network", "Executing rfkill block all (airplane mode ON)");
|
||||
Quickshell.execDetached(["rfkill", "block", "wifi"]);
|
||||
Quickshell.execDetached(["rfkill", "block", "bluetooth"]);
|
||||
} else {
|
||||
Logger.i("Network", "Executing rfkill unblock all (airplane mode OFF)");
|
||||
Quickshell.execDetached(["rfkill", "unblock", "wifi"]);
|
||||
Quickshell.execDetached(["rfkill", "unblock", "bluetooth"]);
|
||||
}
|
||||
// Trigger the check immediately to reflect state changes
|
||||
checkWifiBlocked.running = true;
|
||||
}
|
||||
|
||||
// Core functions
|
||||
function syncWifiState() {
|
||||
if (!ProgramCheckerService.nmcliAvailable)
|
||||
if (!ProgramCheckerService.nmcliAvailable) {
|
||||
return;
|
||||
}
|
||||
wifiStateProcess.running = true;
|
||||
}
|
||||
|
||||
function setWifiEnabled(enabled) {
|
||||
if (!ProgramCheckerService.nmcliAvailable)
|
||||
if (!ProgramCheckerService.nmcliAvailable) {
|
||||
return;
|
||||
}
|
||||
Settings.data.network.wifiEnabled = enabled;
|
||||
wifiStateEnableProcess.running = true;
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -267,15 +385,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 = "";
|
||||
@@ -295,16 +415,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
|
||||
@@ -358,16 +480,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";
|
||||
}
|
||||
|
||||
@@ -488,8 +615,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 {
|
||||
@@ -529,11 +657,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") {
|
||||
@@ -637,8 +767,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();
|
||||
@@ -711,11 +842,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) {
|
||||
@@ -806,8 +939,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;
|
||||
@@ -1037,8 +1171,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
|
||||
|
||||
Reference in New Issue
Block a user