646 lines
26 KiB
QML
646 lines
26 KiB
QML
// Connections — the whole network, not the half that was easy.
|
|
//
|
|
// This page used to end in a card headed "Owned by Fedora" with two doors back
|
|
// to GNOME's panels: one for hidden and enterprise networks, one for VPN and
|
|
// proxies. Everything behind those doors now lives here, so the card is gone.
|
|
//
|
|
// Two mechanisms, deliberately kept apart:
|
|
//
|
|
// * Wi-Fi and Bluetooth state -- scanning, joining, pairing, the radio
|
|
// switches -- go through Quickshell.Networking and Quickshell.Bluetooth,
|
|
// which speak to NetworkManager and BlueZ over DBus. Connectivity.qml is
|
|
// pinned shell-out-free and stays that way.
|
|
// * Everything NetworkManager exposes only through nmcli -- per-connection
|
|
// addresses, forgetting a profile, MAC randomization, VPN import, hotspot,
|
|
// enterprise join -- goes through NetworkTools.qml and scripts/panama-network.
|
|
//
|
|
// Scanning follows this page being on screen. Wi-Fi scanning and especially
|
|
// Bluetooth discovery hold the radio, and doing either for a list nobody is
|
|
// looking at is battery and airtime spent on nothing.
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Networking
|
|
import qs.config
|
|
import qs.services
|
|
|
|
SettingsPage {
|
|
id: root
|
|
|
|
objectName: "connectivity"
|
|
title: "Connections"
|
|
lede: {
|
|
const wifi = Connectivity.activeNetwork ? Connectivity.activeNetwork.name : "";
|
|
if (wifi !== "" && Connectivity.wiredOn)
|
|
return "On " + wifi + " and wired.";
|
|
if (wifi !== "")
|
|
return "On " + wifi + ".";
|
|
if (Connectivity.wiredOn)
|
|
return "Wired.";
|
|
return "Wi-Fi, Bluetooth, VPN, and what this machine can reach.";
|
|
}
|
|
|
|
// The wired connection's own drawer, and the two inline forms that are only
|
|
// open while someone is filling them in.
|
|
property bool wiredOpen: false
|
|
property bool hotspotOpen: false
|
|
property string hotspotName: ""
|
|
property bool importOpen: false
|
|
property string importPath: ""
|
|
|
|
// The saved list, folded at the house cap. Sorted by the helper with the
|
|
// active and autoconnecting profiles first, so a slice keeps the ones worth
|
|
// seeing and folds the tail.
|
|
property bool savedShowAll: false
|
|
readonly property int savedCap: 6
|
|
readonly property var savedShown: {
|
|
const list = NetworkTools.savedConnections;
|
|
if (root.savedShowAll || list.length <= root.savedCap)
|
|
return list;
|
|
return list.slice(0, root.savedCap);
|
|
}
|
|
readonly property int savedHidden:
|
|
NetworkTools.savedConnections.length - root.savedShown.length
|
|
|
|
// The proxy dropdown and its address, page-local until they add up to a
|
|
// whole setting.
|
|
//
|
|
// A manual proxy is a host AND a port -- GNOME ignores a proxy whose host
|
|
// is empty or whose port is 0 -- so engaging "manual" before an address
|
|
// exists produces a mode that says it is proxying and is not. The dropdown
|
|
// therefore leads the applied mode: picking Manual reveals the fields, and
|
|
// the proxy is written on the edit that completes the pair, whichever of
|
|
// the two that is. Committing on the first field instead would have to
|
|
// guess at the other one; requiring the other to be stored first, which is
|
|
// what this page used to do, meant neither could ever be first.
|
|
//
|
|
// The three start as bindings and stay bound until the first edit, which is
|
|
// what carries the helper's opening read into a page that was built before
|
|
// it answered. After that they are the user's, and nothing re-seeds them:
|
|
// a reply landing mid-edit must not move the dropdown or empty the field
|
|
// somebody is typing into.
|
|
property string proxyChoice: NetworkTools.proxyMode
|
|
property string draftProxyHost: NetworkTools.proxyHost
|
|
property string draftProxyPort: NetworkTools.proxyPort
|
|
|
|
// "host", "port", "both" or "" -- which half of the pair is still missing.
|
|
readonly property string proxyMissing: {
|
|
const host = root.draftProxyHost.trim() === "";
|
|
const port = root.draftProxyPort.trim() === "";
|
|
if (host && port)
|
|
return "both";
|
|
if (host)
|
|
return "host";
|
|
if (port)
|
|
return "port";
|
|
return "";
|
|
}
|
|
|
|
// Called after either field is edited. Writes only a whole address, so a
|
|
// half-filled form leaves the proxy exactly as it was.
|
|
function commitProxyManual(): void {
|
|
if (root.proxyMissing !== "")
|
|
return;
|
|
NetworkTools.setProxyManual(root.draftProxyHost.trim(), root.draftProxyPort.trim());
|
|
}
|
|
|
|
readonly property string wiredConnection:
|
|
String(Connectivity.wiredDevice?.network?.name ?? "")
|
|
|
|
readonly property string activeWifi:
|
|
Connectivity.activeNetwork ? Connectivity.activeNetwork.name : ""
|
|
|
|
// Drive the scanners and the helper only while this page is the one being
|
|
// shown. Both cost radio time or nmcli invocations for a list nobody is
|
|
// reading.
|
|
Component.onCompleted: {
|
|
Connectivity.active = true;
|
|
NetworkTools.active = true;
|
|
if (!WifiShare.scanned)
|
|
WifiShare.refresh();
|
|
Vpn.refresh();
|
|
}
|
|
Component.onDestruction: {
|
|
Connectivity.active = false;
|
|
NetworkTools.active = false;
|
|
}
|
|
|
|
// Addresses and the MAC in use both change with the connection, so the
|
|
// cached details for a network that just came up are stale the moment it
|
|
// does.
|
|
onActiveWifiChanged: {
|
|
if (root.activeWifi !== "")
|
|
NetworkTools.refreshDetails(root.activeWifi);
|
|
}
|
|
onWiredConnectionChanged: {
|
|
if (root.wiredConnection !== "")
|
|
NetworkTools.refreshDetails(root.wiredConnection);
|
|
}
|
|
|
|
TextRow {
|
|
visible: NetworkTools.lastError !== ""
|
|
label: "The network needs attention"
|
|
detail: NetworkTools.lastError
|
|
value: ""
|
|
divider: false
|
|
}
|
|
|
|
// ── Wired ────────────────────────────────────────────────────────────────
|
|
|
|
SettingsCard {
|
|
title: "Wired"
|
|
visible: Connectivity.wiredDevice !== null
|
|
|
|
SettingRow {
|
|
width: parent.width
|
|
|
|
label: root.wiredConnection !== "" ? root.wiredConnection : "Ethernet"
|
|
// Three states worth telling apart: on, off but plugged in, and
|
|
// nothing in the socket. "Not connected" covered all three and
|
|
// explained none of them.
|
|
detail: {
|
|
const device = Connectivity.wiredDevice;
|
|
if (!device)
|
|
return "";
|
|
if (device.connected)
|
|
return device.name + (device.linkSpeed > 0
|
|
? " · " + device.linkSpeed + " Mb/s" : "");
|
|
// Not "no cable": that cannot be told apart from "switched off"
|
|
// by anything reliable here, and guessing produced a switch
|
|
// that blamed the hardware for what it had just done itself.
|
|
return device.name + " · off";
|
|
}
|
|
controlWidth: 78
|
|
divider: false
|
|
activatable: root.wiredConnection !== "" && Connectivity.wiredOn
|
|
onActivated: root.wiredOpen = !root.wiredOpen
|
|
|
|
Row {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 9
|
|
|
|
SettingsToggle {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
checked: Connectivity.wiredOn
|
|
enabled: Connectivity.wiredAvailable
|
|
onToggled: value => Connectivity.setWired(value)
|
|
}
|
|
|
|
Text {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: root.wiredConnection !== "" && Connectivity.wiredOn
|
|
text: root.wiredOpen ? "▴" : "▾"
|
|
color: Theme.fgMuted
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
}
|
|
}
|
|
}
|
|
|
|
ConnectionDetails {
|
|
width: parent.width
|
|
visible: root.wiredOpen && Connectivity.wiredOn
|
|
connection: root.wiredConnection
|
|
details: root.wiredConnection !== ""
|
|
? NetworkTools.detailsFor(root.wiredConnection) : null
|
|
}
|
|
}
|
|
|
|
// ── Wi-Fi ────────────────────────────────────────────────────────────────
|
|
|
|
SettingsCard {
|
|
title: "Wi-Fi"
|
|
// A Wi-Fi switch reading "On" above the words "No Wi-Fi adapter" is a
|
|
// contradiction; with no radio the card simply does not belong.
|
|
visible: Connectivity.wifiDevice !== null
|
|
subtitle: "Networks are re-scanned while this page is open."
|
|
|
|
SettingRow {
|
|
label: "Wi-Fi"
|
|
detail: Connectivity.wifiEnabled ? "On" : "Off"
|
|
controlWidth: 48
|
|
divider: Connectivity.wifiEnabled
|
|
|
|
SettingsToggle {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
checked: Connectivity.wifiEnabled
|
|
enabled: Connectivity.wifiAvailable
|
|
onToggled: value => Connectivity.setWifiEnabled(value)
|
|
}
|
|
}
|
|
|
|
WifiPanel {
|
|
width: parent.width
|
|
visible: Connectivity.wifiEnabled
|
|
}
|
|
|
|
// ── Hotspot ──────────────────────────────────────────────────────────
|
|
//
|
|
// One radio cannot be a client and an access point at the same time, so
|
|
// starting this drops whatever network the machine is on. Said before it
|
|
// happens rather than discovered when the browser stops loading.
|
|
|
|
ActionRow {
|
|
width: parent.width
|
|
visible: Connectivity.wifiEnabled && !NetworkTools.hotspotActive
|
|
label: "Hotspot"
|
|
detail: "Share this machine's connection over Wi-Fi"
|
|
action: root.hotspotOpen ? "Cancel" : "Start hotspot…"
|
|
enabled: !NetworkTools.busy
|
|
divider: root.hotspotOpen
|
|
onTriggered: {
|
|
root.hotspotOpen = !root.hotspotOpen;
|
|
root.hotspotName = "";
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
visible: root.hotspotOpen && !NetworkTools.hotspotActive
|
|
|
|
TextFieldRow {
|
|
width: parent.width
|
|
label: "Network name"
|
|
detail: "What the hotspot calls itself to phones and laptops nearby"
|
|
placeholder: "panama-hotspot"
|
|
text: root.hotspotName
|
|
enabled: !NetworkTools.busy
|
|
onAccepted: value => root.hotspotName = value
|
|
}
|
|
|
|
ActionRow {
|
|
width: parent.width
|
|
label: "Start the hotspot"
|
|
detail: Connectivity.activeNetwork
|
|
? "This machine leaves " + Connectivity.activeNetwork.name
|
|
+ " while the hotspot runs — one radio cannot do both."
|
|
: "NetworkManager makes up a password and shows it once."
|
|
action: NetworkTools.busy ? "Starting…" : "Start"
|
|
enabled: root.hotspotName.trim() !== "" && !NetworkTools.busy
|
|
divider: false
|
|
onTriggered: {
|
|
NetworkTools.startHotspot(root.hotspotName.trim());
|
|
root.hotspotOpen = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
TextRow {
|
|
width: parent.width
|
|
visible: NetworkTools.hotspotActive
|
|
label: "Hotspot is running"
|
|
detail: "Phones and laptops nearby can see this network and join it"
|
|
value: NetworkTools.hotspotSsid
|
|
}
|
|
|
|
// Shown once, on purpose. NetworkManager keeps the passphrase; this page
|
|
// never stores it, so leaving this screen means asking NetworkManager
|
|
// again rather than reading it back from Panama.
|
|
TextRow {
|
|
width: parent.width
|
|
visible: NetworkTools.hotspotActive && NetworkTools.hotspotPassword !== ""
|
|
label: "Password"
|
|
detail: "Shown once. Panama does not keep a copy — write it down or let someone type it in now."
|
|
value: NetworkTools.hotspotPassword
|
|
}
|
|
|
|
ActionRow {
|
|
width: parent.width
|
|
visible: NetworkTools.hotspotActive
|
|
label: "Stop the hotspot"
|
|
detail: "Anything connected through this machine loses its connection"
|
|
action: "Stop"
|
|
enabled: !NetworkTools.busy
|
|
divider: false
|
|
onTriggered: NetworkTools.stopHotspot()
|
|
}
|
|
}
|
|
|
|
// ── Saved networks ───────────────────────────────────────────────────────
|
|
//
|
|
// The list above is what is nearby. This is what this machine REMEMBERS,
|
|
// which is a different set and the more useful one to tidy: a profile you
|
|
// want rid of is invisible in a scan-driven list until you are standing
|
|
// next to it, which is exactly when you are least able to deal with it.
|
|
|
|
SettingsCard {
|
|
title: "Saved networks"
|
|
visible: NetworkTools.savedConnections.length > 0
|
|
subtitle: NetworkTools.savedConnections.length
|
|
+ (NetworkTools.savedConnections.length === 1 ? " profile" : " profiles")
|
|
+ " NetworkManager holds, including the ones nowhere near you."
|
|
|
|
Repeater {
|
|
model: root.savedShown
|
|
|
|
delegate: SettingRow {
|
|
id: savedRow
|
|
|
|
required property var modelData
|
|
required property int index
|
|
|
|
readonly property string name: String(savedRow.modelData.name ?? "")
|
|
readonly property bool wireless: savedRow.modelData.wifi === true
|
|
|
|
width: parent.width
|
|
label: savedRow.name
|
|
// Armed, the row stops describing the profile and names what
|
|
// the confirming press costs. The saved passphrase goes with
|
|
// the profile, and nothing else on this page says so.
|
|
detail: {
|
|
if (forgetSaved.armed)
|
|
return "This deletes the saved profile and its password. Rejoining "
|
|
+ savedRow.name + " means typing it again.";
|
|
const bits = [];
|
|
if (savedRow.modelData.active === true)
|
|
bits.push("Connected");
|
|
else if (savedRow.wireless)
|
|
bits.push(savedRow.modelData.inRange === true ? "In range" : "Out of range");
|
|
else
|
|
bits.push(String(savedRow.modelData.type ?? ""));
|
|
bits.push(savedRow.modelData.autoconnect === true
|
|
? "autoconnects" : "never autoconnects");
|
|
return bits.join(" · ");
|
|
}
|
|
value: savedRow.modelData.active === true ? "this one" : ""
|
|
controlWidth: 200
|
|
divider: savedRow.index < root.savedShown.length - 1 || root.savedHidden > 0
|
|
|| root.savedShowAll
|
|
|
|
ConfirmAction {
|
|
id: forgetSaved
|
|
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
visible: savedRow.modelData.active !== true
|
|
actionId: "forget-saved-network:" + savedRow.name
|
|
armText: "Forget…"
|
|
confirmText: "Forget it"
|
|
enabled: !NetworkTools.busy
|
|
onConfirmed: NetworkTools.forget(savedRow.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
SettingRow {
|
|
width: parent.width
|
|
visible: root.savedHidden > 0
|
|
|| (root.savedShowAll && NetworkTools.savedConnections.length > root.savedCap)
|
|
label: root.savedShowAll
|
|
? "Show fewer"
|
|
: root.savedHidden + (root.savedHidden === 1 ? " more" : " more")
|
|
detail: root.savedShowAll
|
|
? ""
|
|
: "Folded to keep the list short — the ones you use least are at the bottom"
|
|
activatable: true
|
|
divider: false
|
|
onActivated: root.savedShowAll = !root.savedShowAll
|
|
}
|
|
}
|
|
|
|
// ── VPN ──────────────────────────────────────────────────────────────────
|
|
|
|
SettingsCard {
|
|
title: "VPN"
|
|
subtitle: "WireGuard and OpenVPN profiles NetworkManager holds for you."
|
|
|
|
TextRow {
|
|
width: parent.width
|
|
visible: Vpn.lastError !== ""
|
|
label: "The last VPN action did not finish"
|
|
detail: Vpn.lastError
|
|
value: ""
|
|
}
|
|
|
|
Repeater {
|
|
model: Vpn.connections
|
|
|
|
delegate: SwitchRow {
|
|
id: vpnRow
|
|
|
|
required property var modelData
|
|
required property int index
|
|
|
|
width: parent.width
|
|
label: String(vpnRow.modelData.name ?? "")
|
|
detail: String(vpnRow.modelData.kind ?? "VPN")
|
|
+ (vpnRow.modelData.active === true ? " · connected" : "")
|
|
checked: vpnRow.modelData.active === true
|
|
enabled: !Vpn.busy
|
|
onToggled: value => Vpn.setActive(String(vpnRow.modelData.uuid ?? ""), value)
|
|
}
|
|
}
|
|
|
|
TextRow {
|
|
width: parent.width
|
|
visible: Vpn.scanned && Vpn.connections.length === 0
|
|
label: "No VPNs configured"
|
|
detail: "Import a WireGuard or OpenVPN file to add one"
|
|
value: ""
|
|
}
|
|
|
|
// Names the profile that appeared rather than saying "done". An import
|
|
// that succeeds under a name you did not choose is otherwise invisible
|
|
// until you go looking for it in the list.
|
|
TextRow {
|
|
width: parent.width
|
|
visible: NetworkTools.lastImport !== ""
|
|
label: "Imported " + NetworkTools.lastImport
|
|
detail: "It is switched off until you turn it on above"
|
|
value: ""
|
|
}
|
|
|
|
ActionRow {
|
|
width: parent.width
|
|
label: "Import a VPN"
|
|
detail: "A .conf file from WireGuard, or a .ovpn file from OpenVPN"
|
|
action: root.importOpen ? "Cancel" : "Import…"
|
|
enabled: !NetworkTools.busy
|
|
divider: root.importOpen
|
|
onTriggered: {
|
|
root.importOpen = !root.importOpen;
|
|
root.importPath = "";
|
|
}
|
|
}
|
|
|
|
// A typed path rather than a file chooser this phase. A chooser is the
|
|
// better answer and is worth doing properly; a typed path is worth far
|
|
// more than the door back to GNOME it replaces.
|
|
Column {
|
|
width: parent.width
|
|
visible: root.importOpen
|
|
|
|
TextFieldRow {
|
|
width: parent.width
|
|
label: "File"
|
|
detail: "The full path to the profile you were given"
|
|
placeholder: "~/Downloads/work.ovpn"
|
|
text: root.importPath
|
|
enabled: !NetworkTools.busy
|
|
onAccepted: value => root.importPath = value
|
|
}
|
|
|
|
ActionRow {
|
|
width: parent.width
|
|
label: "Import this profile"
|
|
detail: "NetworkManager reads it and adds a profile. Nothing connects until you switch it on."
|
|
action: NetworkTools.busy ? "Importing…" : "Import"
|
|
enabled: root.importPath.trim() !== "" && !NetworkTools.busy
|
|
divider: false
|
|
onTriggered: {
|
|
NetworkTools.importVpn(root.importPath.trim());
|
|
root.importOpen = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Bluetooth ────────────────────────────────────────────────────────────
|
|
|
|
SettingsCard {
|
|
title: "Bluetooth"
|
|
visible: Connectivity.adapter !== null
|
|
subtitle: "Discovery runs while this page is open."
|
|
|
|
SettingRow {
|
|
label: "Bluetooth"
|
|
detail: Connectivity.bluetoothAvailable
|
|
? (Connectivity.bluetoothEnabled ? "On" : "Off")
|
|
: "Unavailable"
|
|
controlWidth: 48
|
|
divider: Connectivity.bluetoothEnabled
|
|
|
|
SettingsToggle {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
checked: Connectivity.bluetoothEnabled
|
|
enabled: Connectivity.bluetoothAvailable
|
|
onToggled: value => Connectivity.setBluetoothEnabled(value)
|
|
}
|
|
}
|
|
|
|
BluetoothPanel {
|
|
width: parent.width
|
|
visible: Connectivity.bluetoothEnabled
|
|
}
|
|
}
|
|
|
|
// ── Radios and proxy ─────────────────────────────────────────────────────
|
|
|
|
SettingsCard {
|
|
title: "Radios and proxy"
|
|
subtitle: "The two settings that apply to every connection at once."
|
|
|
|
// A hardware kill switch cannot be overridden from software, so the
|
|
// switch says so rather than moving and having nothing happen.
|
|
SwitchRow {
|
|
width: parent.width
|
|
label: "Airplane mode"
|
|
detail: NetworkTools.airplaneHardBlocked
|
|
? "A switch on this machine is holding the radios off. Software cannot turn them back on."
|
|
: "Turns Wi-Fi and Bluetooth off together — the same switch the keyboard's airplane key throws"
|
|
checked: NetworkTools.airplaneOn
|
|
enabled: !NetworkTools.busy && !NetworkTools.airplaneHardBlocked
|
|
onToggled: value => NetworkTools.setAirplane(value)
|
|
}
|
|
|
|
OptionPickerRow {
|
|
width: parent.width
|
|
label: "Network proxy"
|
|
detail: {
|
|
if (root.proxyChoice === "manual" && NetworkTools.proxyMode !== "manual") {
|
|
if (root.proxyMissing === "both")
|
|
return "Not applied yet — fill in the host and the port below.";
|
|
if (root.proxyMissing !== "")
|
|
return "Not applied yet — the " + root.proxyMissing + " below is still empty.";
|
|
return "Not applied yet.";
|
|
}
|
|
if (NetworkTools.proxyMode === "none")
|
|
return "Applications that honour the system proxy use this. Not every application does.";
|
|
return "In use: " + NetworkTools.proxySummary;
|
|
}
|
|
enabled: !NetworkTools.busy
|
|
options: [
|
|
{
|
|
value: "none",
|
|
label: "Off",
|
|
detail: "Applications reach the network directly"
|
|
},
|
|
{
|
|
value: "manual",
|
|
label: "Manual",
|
|
detail: "A host and port you enter, used for http, https and socks alike"
|
|
},
|
|
{
|
|
value: "auto",
|
|
label: "Automatic (PAC)",
|
|
detail: "A configuration URL decides, per address"
|
|
}
|
|
]
|
|
current: root.proxyChoice
|
|
// Off and Automatic mean something the moment they are picked;
|
|
// Manual does not, so it only opens the fields. See proxyChoice.
|
|
onPicked: value => {
|
|
root.proxyChoice = String(value);
|
|
if (root.proxyChoice === "manual")
|
|
root.commitProxyManual();
|
|
else
|
|
NetworkTools.setProxyMode(root.proxyChoice);
|
|
}
|
|
}
|
|
|
|
Column {
|
|
width: parent.width
|
|
visible: root.proxyChoice === "manual"
|
|
|
|
// Host and port are written together, because the proxy is only
|
|
// usable as a pair. Each field edits the page's draft; the pair is
|
|
// written once both halves are there.
|
|
TextFieldRow {
|
|
width: parent.width
|
|
label: "Proxy host"
|
|
detail: root.proxyMissing === "host"
|
|
? "Still empty — the proxy applies once this and the port are both set"
|
|
: "The machine applications should go through"
|
|
placeholder: "proxy.example.com"
|
|
text: root.draftProxyHost
|
|
enabled: !NetworkTools.busy
|
|
onAccepted: value => {
|
|
root.draftProxyHost = value.trim();
|
|
root.commitProxyManual();
|
|
}
|
|
}
|
|
|
|
TextFieldRow {
|
|
width: parent.width
|
|
label: "Port"
|
|
detail: root.proxyMissing === "port"
|
|
? "Still empty — the proxy applies once this and the host are both set"
|
|
: "The port that proxy listens on"
|
|
placeholder: "8080"
|
|
text: root.draftProxyPort
|
|
enabled: !NetworkTools.busy
|
|
divider: false
|
|
onAccepted: value => {
|
|
root.draftProxyPort = value.trim();
|
|
root.commitProxyManual();
|
|
}
|
|
}
|
|
}
|
|
|
|
TextFieldRow {
|
|
width: parent.width
|
|
visible: root.proxyChoice === "auto"
|
|
label: "Configuration URL"
|
|
detail: "The .pac file whoever runs the network published"
|
|
placeholder: "http://example.com/proxy.pac"
|
|
text: NetworkTools.proxyPac
|
|
enabled: !NetworkTools.busy
|
|
divider: false
|
|
onAccepted: value => NetworkTools.setProxyPac(value.trim())
|
|
}
|
|
}
|
|
}
|