Add Wi-Fi QR sharing to network panel (#6463)
* Add Wi-Fi QR sharing to network panel * Refine Wi-Fi QR sharing * Use QR glyph for Wi-Fi sharing * Add click-to-reveal password to the Wi-Fi share card Scanning the QR is the fast path, but the person typing on a laptop needs the actual password. A dimmed "Show password" hint under the QR toggles the secret in place. The password stays out of the shell until asked for: a click runs the new omarchy-network-password helper (a private pipe, never an argument), and closing the card drops it again. Open and enterprise networks never show the control. The card loses its Close button -- Escape and clicking outside already cover it -- and now sizes itself to its content. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Keep a dismissed Wi-Fi share card closed and make Escape reliable Closing the card mid-generation killed the helper, but its buffered stdout still arrived and repopulated the matrix, reopening the card the user just closed. Both collectors now honor qrExpectedStop, and the flag survives onExited because exit and stream-finished have no guaranteed order. The password fetch gets the same treatment so a reveal in flight during dismissal can't stash the secret into a closed card's state. The content's focus was claimed while the window was still unmapped, so Escape could land nowhere. Re-acquire it after mapping, the way KeyboardPanel does. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Share WEP networks correctly and restore the QR quiet zone NetworkManager models WEP as key-mgmt "none" plus a wep-key, so the QR helper encoded WEP networks as open -- a QR that scans fine and then silently fails to join. Encode them as T:WEP and let the password helper print the key. Also widen qrencode's margin from 2 to the spec's 4-module quiet zone; the card surround is dark, so that white border is all a scanner gets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: David Heinemeier Hansson <david@hey.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
David Heinemeier Hansson
parent
1ea910f662
commit
a79d1dc8da
@@ -40,13 +40,11 @@ function formatHeaderFreq(mhz) {
|
||||
return ghz.toFixed(ghz % 1 === 0 ? 0 : 1) + "ghz"
|
||||
}
|
||||
|
||||
// `hideWifiBand` is set when the band toggle is on screen: the band is already
|
||||
// spelled out there, so repeating it in "York (5ghz)" is noise. A single-band
|
||||
// network hides the toggle, and then the header stays the only place it shows.
|
||||
function headerDetail(info, hideWifiBand) {
|
||||
// Wi-Fi band state belongs in the selector section, not beside the hero name.
|
||||
// Ethernet has no equivalent selector, so keep its negotiated link speed here.
|
||||
function headerDetail(info) {
|
||||
var value = info || {}
|
||||
if (value.type === "ethernet") return formatHeaderSpeed(value.speed || "")
|
||||
if (value.type === "wifi") return hideWifiBand ? "" : formatHeaderFreq(value.freq || "")
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -284,6 +282,20 @@ function isProtected(security, openSecurity) {
|
||||
return security !== openSecurity
|
||||
}
|
||||
|
||||
function parseQrMatrix(raw) {
|
||||
var lines = String(raw || "").trim().split(/\r?\n/).filter(function(line) { return line !== "" })
|
||||
if (lines.length === 0) return { rows: [], size: 0 }
|
||||
|
||||
var size = lines[0].length
|
||||
if (size !== lines.length) return { rows: [], size: 0 }
|
||||
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
if (lines[i].length !== size || !/^[01]+$/.test(lines[i])) return { rows: [], size: 0 }
|
||||
}
|
||||
|
||||
return { rows: lines, size: size }
|
||||
}
|
||||
|
||||
// The password arrives on stdin and reaches nmcli through the scriptable
|
||||
// `connection edit` editor -- argv is world-readable in /proc, so the secret
|
||||
// must never be an argument (printf is a bash builtin, so no process spawns
|
||||
@@ -332,6 +344,7 @@ if (typeof module !== "undefined") {
|
||||
sortWifiRows: sortWifiRows,
|
||||
wifiSectionTitle: wifiSectionTitle,
|
||||
isProtected: isProtected,
|
||||
parseQrMatrix: parseQrMatrix,
|
||||
enterpriseConnectScript: enterpriseConnectScript,
|
||||
networkFailureReason: networkFailureReason
|
||||
}
|
||||
|
||||
@@ -103,6 +103,16 @@ Panel {
|
||||
property string passwordText: ""
|
||||
property string identityText: ""
|
||||
|
||||
property var qrRows: []
|
||||
property int qrSize: 0
|
||||
property string qrError: ""
|
||||
property bool qrLoading: false
|
||||
property bool qrExpectedStop: false
|
||||
property string qrPassword: ""
|
||||
property bool qrPasswordVisible: false
|
||||
property string qrPasswordError: ""
|
||||
readonly property bool qrVisible: qrLoading || qrSize > 0 || qrError !== ""
|
||||
|
||||
// True while any wifi action is mid-flight. Rows
|
||||
// disable themselves on this so clicks on the other rows don't silently
|
||||
// no-op against runNetworkAction's serialized guard.
|
||||
@@ -120,15 +130,16 @@ Panel {
|
||||
property int headerIndex: 0
|
||||
readonly property bool canDisconnect: !!connectedWifiNetwork
|
||||
readonly property bool headerHasDisconnect: false
|
||||
// The hero switch is the Wi-Fi radio and nothing else, so it only exists
|
||||
// when there is a radio to switch. A click carried no state, but a switch
|
||||
// asserts one: on a wired box it would otherwise sit there reading "off"
|
||||
// beside a perfectly live Ethernet connection.
|
||||
readonly property bool canShareWifi: info.type === "wifi" && canShareNetwork(connectedWifiNetwork)
|
||||
// The hero switch is the Wi-Fi radio, so it only exists when there is a
|
||||
// radio to switch. On a wired box it would otherwise sit there reading
|
||||
// "off" beside a perfectly live Ethernet connection.
|
||||
readonly property bool canToggleWifi: networkManagerAvailable && wifiStationAvailable
|
||||
readonly property int headerActionCount: canToggleWifi ? 1 : 0
|
||||
// Only claim the header cursor when the switch is actually on screen —
|
||||
// "header" stays navigable, but a machine with no radio has nothing to highlight.
|
||||
readonly property bool headerHasCursor: cursorActive && focusSection === "header" && canToggleWifi
|
||||
readonly property int qrHeaderIndex: canShareWifi ? 0 : -1
|
||||
readonly property int toggleHeaderIndex: canToggleWifi ? (canShareWifi ? 1 : 0) : -1
|
||||
readonly property int headerActionCount: (canShareWifi ? 1 : 0) + (canToggleWifi ? 1 : 0)
|
||||
readonly property bool qrHeaderHasCursor: cursorActive && focusSection === "header" && headerIndex === qrHeaderIndex
|
||||
readonly property bool toggleHeaderHasCursor: cursorActive && focusSection === "header" && headerIndex === toggleHeaderIndex
|
||||
readonly property string toggleHint: Networking.wifiEnabled ? "Turn Wi-Fi off" : "Turn Wi-Fi on"
|
||||
readonly property var dnsProviders: ["DHCP", "Cloudflare", "Google", "Custom"]
|
||||
property int dnsIndex: 0
|
||||
@@ -204,13 +215,14 @@ Panel {
|
||||
}
|
||||
|
||||
function activateHeader() {
|
||||
toggleNetwork()
|
||||
if (headerIndex === qrHeaderIndex) showWifiQr()
|
||||
else if (headerIndex === toggleHeaderIndex) toggleNetwork()
|
||||
}
|
||||
|
||||
function setHeaderCursor() {
|
||||
function setHeaderCursor(index) {
|
||||
cursorActive = true
|
||||
focusSection = "header"
|
||||
headerIndex = 0
|
||||
headerIndex = index
|
||||
}
|
||||
|
||||
function selectDnsByDelta(delta) {
|
||||
@@ -355,6 +367,11 @@ Panel {
|
||||
return !!(net && net.known && isProtected(net.security) && !net.connected)
|
||||
}
|
||||
|
||||
function canShareNetwork(net) {
|
||||
if (!net || !net.connected) return false
|
||||
return net.security !== WifiSecurityType.Wpa2Eap && net.security !== WifiSecurityType.WpaEap
|
||||
}
|
||||
|
||||
function selectWifiActionByDelta(delta) {
|
||||
if (selectedIndex < 0 || selectedIndex >= wifiNetworks.length) return
|
||||
if (!canForgetNetwork(wifiNetworks[selectedIndex])) {
|
||||
@@ -398,6 +415,51 @@ Panel {
|
||||
|
||||
readonly property string icon: Model.connectionIcon(kind, signalStrength)
|
||||
|
||||
function showWifiQr() {
|
||||
if (qrProc.running || !info.iface || info.type !== "wifi") return
|
||||
qrSize = 0
|
||||
qrRows = []
|
||||
qrError = ""
|
||||
qrLoading = true
|
||||
qrExpectedStop = false
|
||||
qrProc.command = ["omarchy-network-qr", info.iface]
|
||||
qrProc.running = true
|
||||
|
||||
// Leave the compact network panel behind while the centered share card is open.
|
||||
controller.hide()
|
||||
cancelPasswordPrompt()
|
||||
}
|
||||
|
||||
function hideWifiQr() {
|
||||
if (qrProc.running) {
|
||||
qrExpectedStop = true
|
||||
qrProc.running = false
|
||||
}
|
||||
if (pwProc.running) pwProc.running = false
|
||||
qrSize = 0
|
||||
qrRows = []
|
||||
qrError = ""
|
||||
qrLoading = false
|
||||
qrPassword = ""
|
||||
qrPasswordVisible = false
|
||||
qrPasswordError = ""
|
||||
}
|
||||
|
||||
function updateQr(raw) {
|
||||
var matrix = Model.parseQrMatrix(raw)
|
||||
qrRows = matrix.rows
|
||||
qrSize = matrix.size
|
||||
}
|
||||
|
||||
function toggleQrPassword() {
|
||||
if (qrPasswordVisible) { qrPasswordVisible = false; return }
|
||||
if (qrPassword !== "") { qrPasswordVisible = true; return }
|
||||
if (pwProc.running || !info.iface) return
|
||||
qrPasswordError = ""
|
||||
pwProc.command = ["omarchy-network-password", info.iface]
|
||||
pwProc.running = true
|
||||
}
|
||||
|
||||
function refresh(scanWifi) {
|
||||
if (scanWifi === undefined) scanWifi = false
|
||||
if (!detailsProc.running) detailsProc.running = true
|
||||
@@ -430,7 +492,7 @@ Panel {
|
||||
}
|
||||
|
||||
function headerDetail() {
|
||||
return Model.headerDetail(info, canSelectBand)
|
||||
return Model.headerDetail(info)
|
||||
}
|
||||
|
||||
function updateDetails(raw) {
|
||||
@@ -792,6 +854,49 @@ Panel {
|
||||
onTriggered: root.syncWifiNetworks()
|
||||
}
|
||||
|
||||
Process {
|
||||
id: qrProc
|
||||
// Both collectors check qrExpectedStop: a dismissal mid-generation kills
|
||||
// the process, but buffered output still arrives afterwards and would
|
||||
// repopulate qrSize -- reopening the card the user just closed. The flag
|
||||
// stays set through onExited (showWifiQr resets it) because the exit and
|
||||
// stream-finished signals have no guaranteed order.
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: if (!root.qrExpectedStop) root.updateQr(text)
|
||||
}
|
||||
stderr: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: if (!root.qrExpectedStop) root.qrError = String(text || "").trim()
|
||||
}
|
||||
onExited: function(exitCode) {
|
||||
root.qrLoading = false
|
||||
if (root.qrExpectedStop) return
|
||||
if (exitCode !== 0 || root.qrSize === 0) {
|
||||
root.qrSize = 0
|
||||
root.qrRows = []
|
||||
if (root.qrError === "") root.qrError = "Could not generate the Wi-Fi QR code"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The Wi-Fi password only enters shell memory when the user clicks to
|
||||
// reveal it, and hideWifiQr drops it again when the share card closes.
|
||||
// Both handlers bail when the card is gone so a fetch that was in flight
|
||||
// during dismissal can't stash the secret into a closed panel's state.
|
||||
Process {
|
||||
id: pwProc
|
||||
stdout: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: if (root.qrVisible) root.qrPassword = String(text || "").trim()
|
||||
}
|
||||
onExited: function(exitCode) {
|
||||
if (!root.qrVisible) return
|
||||
if (exitCode === 0 && root.qrPassword !== "") root.qrPasswordVisible = true
|
||||
else root.qrPasswordError = "Could not read the Wi-Fi password"
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: dnsProc
|
||||
stdout: StdioCollector {
|
||||
@@ -1074,7 +1179,7 @@ Panel {
|
||||
// ---------- Hero: network icon · SSID + state · actions ----------
|
||||
Item {
|
||||
width: parent.width
|
||||
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight, powerSwitch.implicitHeight)
|
||||
implicitHeight: Math.max(heroIcon.implicitHeight, heroLabels.implicitHeight, heroActions.implicitHeight)
|
||||
|
||||
// Status only — the switch owns toggling, mouse and keyboard alike.
|
||||
Text {
|
||||
@@ -1088,23 +1193,45 @@ Panel {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
// Compact on/off switch on the trailing edge of the hero, and the
|
||||
// header's only cursor target.
|
||||
ToggleSwitch {
|
||||
id: powerSwitch
|
||||
visible: root.canToggleWifi
|
||||
checked: Networking.wifiEnabled
|
||||
hasCursor: root.headerHasCursor
|
||||
foreground: root.bar.foreground
|
||||
// Sharing belongs to the connected-network hero rather than the scan
|
||||
// result row. The radio switch remains beside it as the other hero action.
|
||||
RowLayout {
|
||||
id: heroActions
|
||||
spacing: Style.space(8)
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onHovered: function(on) { if (on) root.setHeaderCursor() }
|
||||
onToggled: root.toggleNetwork()
|
||||
|
||||
PanelToolTip {
|
||||
visible: powerSwitch.containsMouse
|
||||
text: root.toggleHint
|
||||
Button {
|
||||
id: qrAction
|
||||
visible: root.canShareWifi
|
||||
iconText: ""
|
||||
tooltipText: "Show QR code"
|
||||
foreground: root.bar.foreground
|
||||
fontFamily: root.bar.fontFamily
|
||||
iconSize: Style.font.subtitle * 1.5
|
||||
horizontalPadding: Style.space(5)
|
||||
verticalPadding: Style.space(2)
|
||||
hasCursor: root.qrHeaderHasCursor
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onHovered: function(on) { if (on) root.setHeaderCursor(root.qrHeaderIndex) }
|
||||
onClicked: root.showWifiQr()
|
||||
}
|
||||
|
||||
ToggleSwitch {
|
||||
id: powerSwitch
|
||||
visible: root.canToggleWifi
|
||||
checked: Networking.wifiEnabled
|
||||
hasCursor: root.toggleHeaderHasCursor
|
||||
foreground: root.bar.foreground
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onHovered: function(on) { if (on) root.setHeaderCursor(root.toggleHeaderIndex) }
|
||||
onToggled: root.toggleNetwork()
|
||||
|
||||
PanelToolTip {
|
||||
visible: powerSwitch.containsMouse
|
||||
text: root.toggleHint
|
||||
fontFamily: root.bar.fontFamily
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1113,7 +1240,7 @@ Panel {
|
||||
anchors.left: heroIcon.right
|
||||
anchors.leftMargin: Style.space(14)
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: powerSwitch.visible ? powerSwitch.width + Style.space(12) : 0
|
||||
anchors.rightMargin: heroActions.width > 0 ? heroActions.width + Style.space(12) : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Style.space(2)
|
||||
|
||||
@@ -1552,6 +1679,23 @@ Panel {
|
||||
}
|
||||
}
|
||||
|
||||
WifiQrPanel {
|
||||
anchorItem: button
|
||||
bar: root.bar
|
||||
qrRows: root.qrRows
|
||||
qrSize: root.qrSize
|
||||
loading: root.qrLoading
|
||||
error: root.qrError
|
||||
ssid: root.info.ssid || ""
|
||||
secured: root.connectedWifiNetwork ? root.isProtected(root.connectedWifiNetwork.security) : false
|
||||
password: root.qrPassword
|
||||
passwordVisible: root.qrPasswordVisible
|
||||
passwordError: root.qrPasswordError
|
||||
open: root.qrVisible
|
||||
onCloseRequested: root.hideWifiQr()
|
||||
onPasswordToggleRequested: root.toggleQrPassword()
|
||||
}
|
||||
|
||||
// One Wi-Fi band pill. `active` (fill) is the band actually in use and
|
||||
// `selected` (bold) is the pinned choice; with Automatic on nothing is
|
||||
// pinned, so only the live band lights up and the two can no longer read as
|
||||
@@ -1797,8 +1941,7 @@ Panel {
|
||||
spacing: Style.space(1)
|
||||
anchors.left: networkIcon.right
|
||||
anchors.leftMargin: Style.space(10)
|
||||
anchors.right: rightAction.visible ? rightAction.left
|
||||
: parent.right
|
||||
anchors.right: rightAction.visible ? rightAction.left : parent.right
|
||||
anchors.rightMargin: rightAction.visible ? Style.space(8) : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
required property Item anchorItem
|
||||
required property QtObject bar
|
||||
required property var qrRows
|
||||
required property int qrSize
|
||||
required property bool loading
|
||||
required property string error
|
||||
required property string ssid
|
||||
required property bool secured
|
||||
required property string password
|
||||
required property bool passwordVisible
|
||||
required property string passwordError
|
||||
property bool open: false
|
||||
|
||||
readonly property bool showingQr: qrSize > 0 && !loading && error === ""
|
||||
|
||||
signal closeRequested()
|
||||
signal passwordToggleRequested()
|
||||
|
||||
visible: open
|
||||
// The window is instantiated hidden, so the content's `focus: true` is
|
||||
// evaluated before the surface is mapped and Escape would land nowhere.
|
||||
// Re-acquire after mapping, as KeyboardPanel does.
|
||||
onOpenChanged: {
|
||||
if (open) Qt.callLater(function() {
|
||||
if (root.open) keyCatcher.forceActiveFocus()
|
||||
})
|
||||
}
|
||||
screen: anchorItem.QsWindow.window ? anchorItem.QsWindow.window.screen : null
|
||||
anchors { top: true; bottom: true; left: true; right: true }
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.namespace: "omarchy-network-qr"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.45)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.closeRequested()
|
||||
}
|
||||
}
|
||||
|
||||
BorderSurface {
|
||||
width: Style.space(320)
|
||||
height: content.implicitHeight + Style.space(48)
|
||||
anchors.centerIn: parent
|
||||
radius: Style.cornerRadius
|
||||
color: Color.menu.background
|
||||
borderSpec: Border.surfaceSpec("menu", "border", Color.popups.border, Math.max(1, Style.space(2)))
|
||||
|
||||
MouseArea { anchors.fill: parent; onClicked: {} }
|
||||
|
||||
Item {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
anchors.margins: Style.space(24)
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: root.closeRequested()
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
spacing: Style.space(12)
|
||||
|
||||
Text {
|
||||
text: "Share " + (root.ssid || "Wi-Fi")
|
||||
color: root.bar.foreground
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.title
|
||||
font.bold: true
|
||||
elide: Text.ElideRight
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
// Render every QR module as an integer-sized native rectangle. This
|
||||
// stays crisp and avoids temporary images and file-cache races.
|
||||
Rectangle {
|
||||
id: qrCanvas
|
||||
readonly property int moduleSize: root.qrSize > 0
|
||||
? Math.max(4, Math.floor(Style.space(240) / root.qrSize))
|
||||
: 0
|
||||
|
||||
visible: root.showingQr
|
||||
width: root.qrSize * moduleSize
|
||||
height: width
|
||||
color: "white"
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
Grid {
|
||||
anchors.fill: parent
|
||||
columns: root.qrSize
|
||||
|
||||
Repeater {
|
||||
model: root.qrSize * root.qrSize
|
||||
|
||||
Rectangle {
|
||||
required property int index
|
||||
readonly property int matrixRow: Math.floor(index / root.qrSize)
|
||||
readonly property int matrixColumn: index % root.qrSize
|
||||
|
||||
width: qrCanvas.moduleSize
|
||||
height: qrCanvas.moduleSize
|
||||
color: root.qrRows[matrixRow].charAt(matrixColumn) === "1" ? "#111111" : "white"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.loading
|
||||
text: "Generating QR code…"
|
||||
color: root.bar.foreground
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.error !== ""
|
||||
text: root.error
|
||||
color: root.bar.urgent
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.showingQr
|
||||
text: "Scan to join this network"
|
||||
color: root.bar.foreground
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.showingQr && root.secured
|
||||
text: root.passwordError !== "" ? root.passwordError
|
||||
: root.passwordVisible ? root.password
|
||||
: "Show password"
|
||||
color: root.passwordError !== "" ? root.bar.urgent : root.bar.foreground
|
||||
opacity: root.passwordVisible || root.passwordError !== "" ? 1 : 0.6
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
wrapMode: Text.WrapAnywhere
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: root.passwordToggleRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"name": "Network",
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"description": "Wi-Fi list and connection state",
|
||||
"description": "Wi-Fi list, connection state, and QR sharing",
|
||||
"kinds": [
|
||||
"bar-widget"
|
||||
],
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"barWidget": {
|
||||
"displayName": "Network",
|
||||
"description": "Wi-Fi list and connection state",
|
||||
"description": "Wi-Fi list, connection state, and QR sharing",
|
||||
"category": "Network",
|
||||
"allowMultiple": false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user