Extract the speed test into its own omarchy.speedtest panel plugin
The network panel now summons it, so a clone or a third-party plugin declaring clonedFrom: omarchy.speedtest can replace the whole speed test -- dials and run orchestration alike -- for every caller. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
4519451655
commit
dd2f434a7d
@@ -17,7 +17,7 @@ Panel {
|
||||
manageIpc: false
|
||||
|
||||
// Centralized close so callers can't forget to drop the passphrase prompt.
|
||||
readonly property bool overlayVisible: qrVisible || speedTestModalOpen
|
||||
readonly property bool overlayVisible: qrVisible
|
||||
|
||||
// Shadows the base open(): a summon or toggle while a centered card is up
|
||||
// dismisses the card instead of opening the compact panel behind an
|
||||
@@ -26,7 +26,6 @@ Panel {
|
||||
function open() {
|
||||
if (overlayVisible) {
|
||||
hideWifiQr()
|
||||
hideSpeedTest()
|
||||
return
|
||||
}
|
||||
root.controller.show()
|
||||
@@ -35,10 +34,9 @@ Panel {
|
||||
function close() {
|
||||
root.controller.hide()
|
||||
cancelPasswordPrompt()
|
||||
// The centered cards outlive the compact panel, but the widget's
|
||||
// canonical close must not leave an overlay (or its traffic) behind.
|
||||
// The centered card outlives the compact panel, but the widget's
|
||||
// canonical close must not leave an overlay behind.
|
||||
hideWifiQr()
|
||||
hideSpeedTest()
|
||||
}
|
||||
|
||||
function cancelPasswordPrompt() {
|
||||
@@ -101,15 +99,6 @@ Panel {
|
||||
property string bandSelected: "auto"
|
||||
property var bandAvailable: []
|
||||
property string pendingBand: ""
|
||||
property bool speedTestRunning: false
|
||||
property bool speedTestModalOpen: false
|
||||
property bool speedTestExpectedStop: false
|
||||
property bool pendingSpeedRun: false
|
||||
property string speedTestPhase: ""
|
||||
property string speedTestStderr: ""
|
||||
property string speedTestDownloadMbps: ""
|
||||
property string speedTestUploadMbps: ""
|
||||
property string speedTestError: ""
|
||||
|
||||
// Per-row in-flight state. `actionSsid` flips on for the row whose action
|
||||
// is currently running so it can render "Connecting…" / "Disconnecting…" /
|
||||
@@ -251,15 +240,12 @@ Panel {
|
||||
root.refresh()
|
||||
root.showWifiQr(true)
|
||||
}
|
||||
function speedTest() {
|
||||
root.refresh()
|
||||
root.showSpeedTest()
|
||||
}
|
||||
function speedTest() { root.summonSpeedTest() }
|
||||
}
|
||||
|
||||
function activateHeader() {
|
||||
if (headerIndex === qrHeaderIndex) showWifiQr()
|
||||
else if (headerIndex === speedHeaderIndex) showSpeedTest()
|
||||
else if (headerIndex === speedHeaderIndex) summonSpeedTest()
|
||||
else if (headerIndex === toggleHeaderIndex) toggleNetwork()
|
||||
}
|
||||
|
||||
@@ -691,83 +677,17 @@ Panel {
|
||||
actionProc.running = true
|
||||
}
|
||||
|
||||
function updateSpeedTestLine(line) {
|
||||
var value = parseFloat(line)
|
||||
if (!isFinite(value) || value < 0) return
|
||||
|
||||
if (speedTestPhase === "down") speedTestDownloadMbps = String(value)
|
||||
else if (speedTestPhase === "up") speedTestUploadMbps = String(value)
|
||||
speedTestError = ""
|
||||
}
|
||||
|
||||
// The speed test lives in a centered modal card like the QR share.
|
||||
// Opening it starts a fresh run; dismissing it stops the traffic, so the
|
||||
// download workers never keep saturating the link behind a closed card.
|
||||
function showSpeedTest() {
|
||||
if (!speedTestModalOpen) {
|
||||
speedTestModalOpen = true
|
||||
controller.hide()
|
||||
cancelPasswordPrompt()
|
||||
}
|
||||
runSpeedTest()
|
||||
}
|
||||
|
||||
function hideSpeedTest() {
|
||||
speedTestModalOpen = false
|
||||
pendingSpeedRun = false
|
||||
speedTestPhaseTimer.stop()
|
||||
// Clear the phase before killing the process: onExited advances to the
|
||||
// upload phase when it still reads "down".
|
||||
speedTestPhase = ""
|
||||
speedTestRunning = false
|
||||
if (speedTestProc.running) {
|
||||
speedTestExpectedStop = true
|
||||
speedTestProc.running = false
|
||||
}
|
||||
}
|
||||
|
||||
function runSpeedTest() {
|
||||
if (speedTestProc.running) {
|
||||
// A dismissal's SIGTERM is still in flight; Process.running stays true
|
||||
// until the child exits, so queue the fresh run for onExited.
|
||||
if (speedTestExpectedStop) pendingSpeedRun = true
|
||||
return
|
||||
}
|
||||
speedTestError = ""
|
||||
speedTestDownloadMbps = ""
|
||||
speedTestUploadMbps = ""
|
||||
speedTestRunning = true
|
||||
startSpeedTestPhase("down")
|
||||
}
|
||||
|
||||
function startSpeedTestPhase(phase) {
|
||||
speedTestExpectedStop = false
|
||||
speedTestPhase = phase
|
||||
speedTestStderr = ""
|
||||
speedTestProc.command = ["omarchy-network-speedtest", phase]
|
||||
speedTestProc.running = true
|
||||
speedTestPhaseTimer.restart()
|
||||
}
|
||||
|
||||
function stopSpeedTestPhase() {
|
||||
speedTestPhaseTimer.stop()
|
||||
if (speedTestProc.running) {
|
||||
speedTestExpectedStop = true
|
||||
speedTestProc.running = false
|
||||
return
|
||||
}
|
||||
finishSpeedTestPhase()
|
||||
}
|
||||
|
||||
function finishSpeedTestPhase() {
|
||||
if (speedTestPhase === "down") {
|
||||
startSpeedTestPhase("up")
|
||||
return
|
||||
}
|
||||
|
||||
speedTestPhase = ""
|
||||
speedTestRunning = false
|
||||
speedTestExpectedStop = false
|
||||
// The speed test is its own panel plugin (omarchy.speedtest) so a
|
||||
// replacement design can take it over; summon() routes to whichever
|
||||
// implementation is enabled. The payload names the connection when this
|
||||
// panel knows it; the plugin looks it up itself otherwise.
|
||||
function summonSpeedTest() {
|
||||
controller.hide()
|
||||
cancelPasswordPrompt()
|
||||
var connection = ""
|
||||
if (info.type === "wifi") connection = info.ssid || "Wi-Fi"
|
||||
else if (info.type === "ethernet") connection = "Ethernet"
|
||||
bar.shell.summon("omarchy.speedtest", connection ? JSON.stringify({ connection: connection }) : "{}")
|
||||
}
|
||||
|
||||
function dnsCommand(provider) {
|
||||
@@ -1017,42 +937,6 @@ Panel {
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: speedTestProc
|
||||
stdout: SplitParser { onRead: function(line) { root.updateSpeedTestLine(line) } }
|
||||
stderr: StdioCollector {
|
||||
waitForEnd: true
|
||||
onStreamFinished: root.speedTestStderr = String(text || "").trim()
|
||||
}
|
||||
onExited: function(exitCode) {
|
||||
speedTestPhaseTimer.stop()
|
||||
|
||||
if (root.pendingSpeedRun) {
|
||||
root.pendingSpeedRun = false
|
||||
root.speedTestExpectedStop = false
|
||||
if (root.speedTestModalOpen) Qt.callLater(root.runSpeedTest)
|
||||
return
|
||||
}
|
||||
|
||||
if (!root.speedTestExpectedStop && exitCode !== 0) {
|
||||
root.speedTestError = root.speedTestStderr || "Speed test failed"
|
||||
root.speedTestPhase = ""
|
||||
root.speedTestRunning = false
|
||||
return
|
||||
}
|
||||
|
||||
root.speedTestExpectedStop = false
|
||||
root.finishSpeedTestPhase()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: speedTestPhaseTimer
|
||||
interval: 5000
|
||||
repeat: false
|
||||
onTriggered: root.stopSpeedTestPhase()
|
||||
}
|
||||
|
||||
// Action runner for DNS provider changes. Wi-Fi actions use the
|
||||
// Quickshell.Networking NetworkManager backend directly.
|
||||
Process {
|
||||
@@ -1316,7 +1200,7 @@ Panel {
|
||||
hasCursor: root.speedHeaderHasCursor
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onHovered: function(on) { if (on) root.setHeaderCursor(root.speedHeaderIndex) }
|
||||
onClicked: root.showSpeedTest()
|
||||
onClicked: root.summonSpeedTest()
|
||||
}
|
||||
|
||||
ToggleSwitch {
|
||||
@@ -1720,24 +1604,6 @@ Panel {
|
||||
onPasswordToggleRequested: root.toggleQrPassword()
|
||||
}
|
||||
|
||||
SpeedTestPanel {
|
||||
anchorItem: button
|
||||
bar: root.bar
|
||||
running: root.speedTestRunning
|
||||
phase: root.speedTestPhase
|
||||
downloadMbps: root.speedTestDownloadMbps
|
||||
uploadMbps: root.speedTestUploadMbps
|
||||
error: root.speedTestError
|
||||
connectionName: {
|
||||
if (root.info.type === "wifi") return root.info.ssid || "Wi-Fi"
|
||||
if (root.info.type === "ethernet") return "Ethernet"
|
||||
return ""
|
||||
}
|
||||
open: root.speedTestModalOpen
|
||||
onCloseRequested: root.hideSpeedTest()
|
||||
onRunAgainRequested: root.runSpeedTest()
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -1,398 +0,0 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Commons
|
||||
import qs.Ui
|
||||
|
||||
// Centered speed test overlay. No card: like the Tucson's floating cluster,
|
||||
// the two dials (download left, upload right) sit directly on a darkened
|
||||
// scrim -- open 270° arcs, faint tick rings, hubless gradient needles, and a
|
||||
// digital readout in the middle. Esc, the scrim, or the corner dismiss close
|
||||
// it; the needles sweep to full scale and back on open, then track the live
|
||||
// readings.
|
||||
PanelWindow {
|
||||
id: root
|
||||
|
||||
required property Item anchorItem
|
||||
required property QtObject bar
|
||||
required property bool running
|
||||
required property string phase // "down" | "up" | ""
|
||||
required property string downloadMbps
|
||||
required property string uploadMbps
|
||||
required property string error
|
||||
required property string connectionName
|
||||
property bool open: false
|
||||
|
||||
signal closeRequested()
|
||||
signal runAgainRequested()
|
||||
|
||||
readonly property real downloadValue: toMbps(downloadMbps)
|
||||
readonly property real uploadValue: toMbps(uploadMbps)
|
||||
readonly property bool failed: error !== ""
|
||||
readonly property bool finished: !running && !failed && (downloadValue > 0 || uploadValue > 0)
|
||||
|
||||
// The scrim below is a fixed near-black regardless of theme, so text and
|
||||
// ticks on it need a fixed light palette, not the themed bar.foreground.
|
||||
readonly property color onScrim: "white"
|
||||
readonly property color onScrimDim: Qt.rgba(1, 1, 1, 0.55)
|
||||
readonly property color onScrimUrgent: "#ff6b6b"
|
||||
|
||||
function toMbps(raw) {
|
||||
var value = parseFloat(raw)
|
||||
return isFinite(value) && value > 0 ? value : 0
|
||||
}
|
||||
|
||||
visible: open
|
||||
// The window is instantiated hidden, so re-acquire focus after mapping and
|
||||
// fire the ignition sweep once the surface is actually on screen.
|
||||
onOpenChanged: {
|
||||
if (open) Qt.callLater(function() {
|
||||
if (!root.open) return
|
||||
keyCatcher.forceActiveFocus()
|
||||
downDial.ignite()
|
||||
upDial.ignite()
|
||||
})
|
||||
}
|
||||
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-speedtest"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
|
||||
// Deep scrim: with no card behind them, the floating dials need the
|
||||
// backdrop to carry the contrast on any wallpaper, like the near-black
|
||||
// panel behind a real cluster.
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Qt.rgba(0, 0, 0, 0.78)
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: root.closeRequested()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: keyCatcher
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
|
||||
Keys.onEscapePressed: root.closeRequested()
|
||||
Keys.onReturnPressed: if (!root.running) root.runAgainRequested()
|
||||
Keys.onEnterPressed: if (!root.running) root.runAgainRequested()
|
||||
|
||||
Item {
|
||||
id: cluster
|
||||
anchors.centerIn: parent
|
||||
width: content.implicitWidth
|
||||
height: content.implicitHeight
|
||||
// Narrow or heavily scaled outputs: shrink the whole cluster rather
|
||||
// than clipping it at the screen edge.
|
||||
scale: Math.min(1,
|
||||
(keyCatcher.width - Style.space(32)) / Math.max(1, width),
|
||||
(keyCatcher.height - Style.space(32)) / Math.max(1, height))
|
||||
|
||||
// Swallow clicks so only the scrim outside the cluster dismisses.
|
||||
MouseArea { anchors.fill: parent; onClicked: {} }
|
||||
|
||||
ColumnLayout {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
spacing: Style.space(16)
|
||||
|
||||
Text {
|
||||
visible: root.connectionName !== ""
|
||||
text: root.connectionName.toUpperCase()
|
||||
color: root.onScrimDim
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
font.bold: true
|
||||
font.letterSpacing: 2
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Style.space(48)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
||||
SpeedDial {
|
||||
id: downDial
|
||||
label: "DOWNLOAD"
|
||||
value: root.downloadValue
|
||||
live: root.running && root.phase === "down"
|
||||
}
|
||||
|
||||
SpeedDial {
|
||||
id: upDial
|
||||
label: "UPLOAD"
|
||||
value: root.uploadValue
|
||||
live: root.running && root.phase === "up"
|
||||
}
|
||||
}
|
||||
|
||||
// Centered on the dial pair. Fades rather than unmounts while a run
|
||||
// is in flight, so the cluster never shifts.
|
||||
Button {
|
||||
text: "Run Again"
|
||||
tooltipText: "Measure again via fast.com"
|
||||
bordered: true
|
||||
enabled: !root.running
|
||||
opacity: root.running ? 0 : 1
|
||||
foreground: root.onScrim
|
||||
fontFamily: root.bar.fontFamily
|
||||
fontSize: Style.font.bodySmall
|
||||
horizontalPadding: Style.space(14)
|
||||
verticalPadding: Style.space(4)
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: root.runAgainRequested()
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 240; easing.type: Easing.OutCubic }
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
visible: root.failed
|
||||
text: root.error
|
||||
color: root.onScrimUrgent
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.bodySmall
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumWidth: Style.space(440)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One floating cluster dial: an open 270° scale with the gap at the
|
||||
// bottom, a faint tick ring, a glowing accent value arc, a hubless needle
|
||||
// that fades toward the pivot, and a digital readout in the middle. All
|
||||
// writes to the needle funnel through `shown` so the ignition sweep and
|
||||
// live readings share one animation.
|
||||
component SpeedDial: Item {
|
||||
id: dial
|
||||
|
||||
required property string label
|
||||
required property real value
|
||||
required property bool live
|
||||
|
||||
readonly property real diameter: Style.space(210)
|
||||
// 0° = 3 o'clock, increasing clockwise (PathAngleArc's convention).
|
||||
readonly property real dialStart: 135
|
||||
readonly property real dialSweep: 270
|
||||
readonly property int tickCount: 46
|
||||
readonly property real arcWidth: Style.space(4)
|
||||
readonly property real arcRadius: diameter / 2 - arcWidth
|
||||
readonly property color trackColor: Qt.rgba(1, 1, 1, 0.14)
|
||||
readonly property color minorTickColor: Qt.rgba(1, 1, 1, 0.12)
|
||||
readonly property color majorTickColor: Qt.rgba(1, 1, 1, 0.3)
|
||||
// The dial that isn't measuring yet sits dimmed until it gets a figure.
|
||||
readonly property bool engaged: live || value > 0
|
||||
|
||||
property real shown: 0
|
||||
// The digital readout stays on the real figure while the ignition sweep
|
||||
// drives the needle -- a cluster sweeps its gauges, not its numerals.
|
||||
readonly property real reading: ignition.running ? value : shown
|
||||
property real fullScale: 100
|
||||
readonly property var scaleStops: [100, 250, 500, 1000, 2500, 5000, 10000]
|
||||
readonly property real fraction: fullScale > 0 ? Math.max(0, Math.min(1, shown / fullScale)) : 0
|
||||
readonly property bool arcVisible: fraction > 0.004
|
||||
|
||||
width: diameter
|
||||
height: diameter
|
||||
opacity: engaged ? 1 : 0.5
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation { duration: 240; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
// Live readings land once a second; glide between them rather than snap.
|
||||
Behavior on shown {
|
||||
enabled: !ignition.running
|
||||
NumberAnimation { duration: 600; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
Behavior on fullScale {
|
||||
enabled: !ignition.running
|
||||
NumberAnimation { duration: 400; easing.type: Easing.OutCubic }
|
||||
}
|
||||
|
||||
// A fresh measurement re-ranges from the base scale. Without this, one
|
||||
// unusually fast run would compress every later one for the lifetime of
|
||||
// the shell process.
|
||||
onLiveChanged: {
|
||||
if (live) fullScale = scaleStops[0]
|
||||
}
|
||||
|
||||
onValueChanged: {
|
||||
// Latch the scale upward to the next stop when a reading approaches the
|
||||
// rim. Never shrink mid-run; a fresh run just re-sweeps from zero.
|
||||
for (var i = 0; i < scaleStops.length; i++) {
|
||||
if (value <= scaleStops[i] * 0.92) {
|
||||
if (scaleStops[i] > fullScale) fullScale = scaleStops[i]
|
||||
break
|
||||
}
|
||||
if (i === scaleStops.length - 1) fullScale = scaleStops[i]
|
||||
}
|
||||
if (!ignition.running) shown = value
|
||||
}
|
||||
|
||||
function ignite() {
|
||||
ignition.restart()
|
||||
}
|
||||
|
||||
// Car-cluster power-on: needle sweeps to full scale and falls back before
|
||||
// the live figures take over.
|
||||
SequentialAnimation {
|
||||
id: ignition
|
||||
NumberAnimation { target: dial; property: "shown"; to: dial.fullScale; duration: 550; easing.type: Easing.InOutCubic }
|
||||
NumberAnimation { target: dial; property: "shown"; to: 0; duration: 650; easing.type: Easing.OutCubic }
|
||||
onFinished: dial.shown = dial.value
|
||||
}
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
// Track: the full scale, always visible, dim.
|
||||
ShapePath {
|
||||
strokeWidth: dial.arcWidth
|
||||
strokeColor: dial.trackColor
|
||||
fillColor: "transparent"
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
centerX: dial.width / 2
|
||||
centerY: dial.height / 2
|
||||
radiusX: dial.arcRadius
|
||||
radiusY: dial.arcRadius
|
||||
startAngle: dial.dialStart
|
||||
sweepAngle: dial.dialSweep
|
||||
}
|
||||
}
|
||||
|
||||
// Soft under-glow beneath the value arc, standing in for the backlit
|
||||
// ring of a real cluster. Both arcs go transparent at rest, or their
|
||||
// round caps would leave a stray dot at the foot of the scale.
|
||||
ShapePath {
|
||||
strokeWidth: dial.arcWidth * 3
|
||||
strokeColor: dial.arcVisible ? Qt.rgba(Color.accent.r, Color.accent.g, Color.accent.b, 0.18) : "transparent"
|
||||
fillColor: "transparent"
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
centerX: dial.width / 2
|
||||
centerY: dial.height / 2
|
||||
radiusX: dial.arcRadius
|
||||
radiusY: dial.arcRadius
|
||||
startAngle: dial.dialStart
|
||||
sweepAngle: dial.dialSweep * dial.fraction
|
||||
}
|
||||
}
|
||||
|
||||
// Value: fills behind the needle.
|
||||
ShapePath {
|
||||
strokeWidth: dial.arcWidth
|
||||
strokeColor: dial.arcVisible ? Color.accent : "transparent"
|
||||
fillColor: "transparent"
|
||||
capStyle: ShapePath.RoundCap
|
||||
|
||||
PathAngleArc {
|
||||
centerX: dial.width / 2
|
||||
centerY: dial.height / 2
|
||||
radiusX: dial.arcRadius
|
||||
radiusY: dial.arcRadius
|
||||
startAngle: dial.dialStart
|
||||
sweepAngle: dial.dialSweep * dial.fraction
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Faint tick ring just inside the arc; every fifth tick is a major.
|
||||
Repeater {
|
||||
model: dial.tickCount
|
||||
|
||||
Item {
|
||||
required property int index
|
||||
readonly property bool major: index % 5 === 0
|
||||
|
||||
anchors.fill: parent
|
||||
rotation: dial.dialStart + (index / (dial.tickCount - 1)) * dial.dialSweep - 270
|
||||
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: dial.arcWidth * 2 + (parent.major ? 0 : Style.space(2))
|
||||
width: parent.major ? Math.max(2, Style.space(2)) : 1
|
||||
height: parent.major ? Style.space(10) : Style.space(6)
|
||||
radius: width / 2
|
||||
color: parent.major ? dial.majorTickColor : dial.minorTickColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hubless needle: a slender sliver that fades out toward the pivot, so
|
||||
// it reads as floating like the rest of the cluster.
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
rotation: dial.dialStart + dial.fraction * dial.dialSweep - 270
|
||||
|
||||
Rectangle {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
y: dial.arcWidth * 2 + Style.space(10)
|
||||
width: Math.max(2, Style.space(3))
|
||||
height: dial.diameter * 0.32
|
||||
radius: width / 2
|
||||
|
||||
gradient: Gradient {
|
||||
GradientStop { position: 0.0; color: Color.accent }
|
||||
GradientStop { position: 0.55; color: Color.accent }
|
||||
GradientStop { position: 1.0; color: "transparent" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.verticalCenter
|
||||
anchors.topMargin: Style.space(14)
|
||||
spacing: 0
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: dial.reading < 10 ? dial.reading.toFixed(1) : Math.round(dial.reading).toString()
|
||||
color: root.onScrim
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.display
|
||||
font.bold: true
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "Mbps"
|
||||
color: root.onScrimDim
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
}
|
||||
}
|
||||
|
||||
// The 90° gap at the bottom of the scale is where a cluster prints its
|
||||
// unit; here it names the direction.
|
||||
Text {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
text: dial.label
|
||||
color: root.onScrimDim
|
||||
font.family: root.bar.fontFamily
|
||||
font.pixelSize: Style.font.caption
|
||||
font.bold: true
|
||||
font.letterSpacing: 1.5
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
"name": "Network",
|
||||
"version": "1.0.0",
|
||||
"author": "Omarchy",
|
||||
"description": "Wi-Fi list, connection state, QR sharing, and speed test",
|
||||
"description": "Wi-Fi list, connection state, and QR sharing",
|
||||
"kinds": [
|
||||
"bar-widget"
|
||||
],
|
||||
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"barWidget": {
|
||||
"displayName": "Network",
|
||||
"description": "Wi-Fi list, connection state, QR sharing, and speed test",
|
||||
"description": "Wi-Fi list, connection state, and QR sharing",
|
||||
"category": "Network",
|
||||
"allowMultiple": false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user