- hypr/apps.lua - hypr/autostart.lua - hypr/envs.lua - hypr/hyprland.lua - hypr/hyprsunset.conf - hypr/input.lua - hypr/looknfeel.lua - hypr/omasettings.lua - hypr/xdph.conf - omarchy/branding/about.txt - omarchy/branding/screensaver.txt - omarchy/extensions/omarchy-menu.jsonc - omarchy/hooks/battery-low.d/play-warning-sound.sample - omarchy/hooks/font-set.d/show-font-notification.sample - omarchy/hooks/post-boot.d/weather.sample - omarchy/hooks/post-update.d/install-voxtype.hook - omarchy/hooks/post-update.d/setup-agent.hook - omarchy/hooks/post-update.d/setup-fingerprint.hook - omarchy/hooks/post-update.d/show-update-notification.sample - omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample - omarchy/hooks/theme-set.d/show-theme-notification.sample - omarchy/shell.json - omarchy/shell.toml - omarchy/theme.name - omarchy/themes/azure-glow/README.md - omarchy/themes/azure-glow/alacritty.toml - omarchy/themes/azure-glow/btop.theme - omarchy/themes/azure-glow/hyprland.conf - omarchy/themes/azure-glow/hyprlock.conf - omarchy/themes/azure-glow/icons.theme - … 269 more
485 lines
15 KiB
QML
485 lines
15 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import qs.Commons
|
|
import qs.Ui
|
|
import "readout.js" as Readout
|
|
import "format.js" as Fmt
|
|
|
|
// Speed: live throughput, content-check history as paired bars, and the
|
|
// last peak result in full.
|
|
Column {
|
|
id: tab
|
|
|
|
required property var panel
|
|
|
|
spacing: Style.space(12)
|
|
|
|
Component.onCompleted: panel.requestTests()
|
|
|
|
readonly property var tests: panel.testsData && panel.testsData.tests
|
|
? panel.testsData.tests : []
|
|
readonly property var contentTests: {
|
|
var out = tests.filter(function(t) { return t.kind === "content" && t.ok })
|
|
out.reverse() // oldest first for the bars
|
|
return out.slice(-12)
|
|
}
|
|
readonly property var lastPeak: {
|
|
for (var i = 0; i < tests.length; i++)
|
|
if (tests[i].kind === "peak" && tests[i].ok) return tests[i]
|
|
return null
|
|
}
|
|
readonly property real bestRate: {
|
|
var best = 0
|
|
for (var i = 0; i < contentTests.length; i++) {
|
|
best = Math.max(best, contentTests[i].down_mbps || 0)
|
|
best = Math.max(best, contentTests[i].up_mbps || 0)
|
|
}
|
|
return Math.max(1, best)
|
|
}
|
|
|
|
|
|
// ---- live throughput ----------------------------------------------------
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
text: "LIVE THROUGHPUT · LAST 3 MIN"
|
|
color: tab.panel.dim
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.caption
|
|
font.letterSpacing: 1
|
|
}
|
|
|
|
// Download above the axis, upload mirrored below — the mockup's cluster
|
|
// shape. Both directions share one scale so the asymmetry is honest.
|
|
Canvas {
|
|
id: flowChart
|
|
width: parent.width
|
|
height: Style.space(84)
|
|
|
|
property real hoverX: -1
|
|
onHoverXChanged: requestPaint()
|
|
|
|
HoverHandler {
|
|
onPointChanged: flowChart.hoverX = hovered ? point.position.x : -1
|
|
onHoveredChanged: if (!hovered) flowChart.hoverX = -1
|
|
}
|
|
|
|
readonly property var pts: {
|
|
var cut = Date.now() / 1000 - 180
|
|
return tab.panel.recentPoints.filter(function(p) { return p.t >= cut })
|
|
}
|
|
onPtsChanged: requestPaint()
|
|
onWidthChanged: requestPaint()
|
|
|
|
onPaint: {
|
|
var ctx = getContext("2d")
|
|
ctx.reset()
|
|
ctx.clearRect(0, 0, width, height)
|
|
var mid = Math.round(height / 2)
|
|
ctx.strokeStyle = Qt.rgba(tab.panel.fg.r, tab.panel.fg.g,
|
|
tab.panel.fg.b, 0.22)
|
|
ctx.lineWidth = 1
|
|
ctx.beginPath()
|
|
ctx.moveTo(0, mid + 0.5)
|
|
ctx.lineTo(width, mid + 0.5)
|
|
ctx.stroke()
|
|
|
|
var p = pts
|
|
if (!p || p.length < 2) return
|
|
var peak = 10 * 1024
|
|
for (var i = 0; i < p.length; i++) {
|
|
if (p[i].rx !== null) peak = Math.max(peak, p[i].rx)
|
|
if (p[i].tx !== null) peak = Math.max(peak, p[i].tx)
|
|
}
|
|
peak *= 1.1
|
|
var t0 = p[0].t, span = Math.max(1, p[p.length - 1].t - t0)
|
|
var half = mid - 2
|
|
|
|
function draw(key, up, tint) {
|
|
var runs = [], cur = []
|
|
for (var i = 0; i < p.length; i++) {
|
|
var v = p[i][key]
|
|
if (v === null || v === undefined) {
|
|
if (cur.length > 1) runs.push(cur)
|
|
cur = []
|
|
} else {
|
|
cur.push([(p[i].t - t0) * (width - 1) / span,
|
|
mid + (up ? -1 : 1) * half * Math.min(1, v / peak)])
|
|
}
|
|
}
|
|
if (cur.length > 1) runs.push(cur)
|
|
for (var r = 0; r < runs.length; r++) {
|
|
var run = runs[r]
|
|
ctx.beginPath()
|
|
ctx.moveTo(run[0][0], mid)
|
|
for (var j = 0; j < run.length; j++) ctx.lineTo(run[j][0], run[j][1])
|
|
ctx.lineTo(run[run.length - 1][0], mid)
|
|
ctx.closePath()
|
|
ctx.fillStyle = Qt.rgba(tint.r, tint.g, tint.b, 0.2)
|
|
ctx.fill()
|
|
ctx.beginPath()
|
|
for (j = 0; j < run.length; j++) {
|
|
if (j === 0) ctx.moveTo(run[j][0], run[j][1])
|
|
else ctx.lineTo(run[j][0], run[j][1])
|
|
}
|
|
ctx.strokeStyle = tint
|
|
ctx.lineWidth = 1.4
|
|
ctx.stroke()
|
|
}
|
|
}
|
|
draw("rx", true, Color.accent)
|
|
draw("tx", false, tab.panel.warnTone)
|
|
|
|
// Top-of-scale label so the silhouette has magnitude.
|
|
ctx.font = "10px " + tab.panel.fontFamily
|
|
ctx.textBaseline = "top"
|
|
ctx.fillStyle = tab.panel.dim
|
|
ctx.fillText("\u2264 " + Fmt.rate(peak), 4, 3)
|
|
|
|
if (hoverX >= 0) {
|
|
var tAt = t0 + hoverX * span / (width - 1)
|
|
var best = null, bestD = Infinity
|
|
for (var h = 0; h < p.length; h++) {
|
|
var d = Math.abs(p[h].t - tAt)
|
|
if (d < bestD) { bestD = d; best = p[h] }
|
|
}
|
|
if (best) {
|
|
var cx = (best.t - t0) * (width - 1) / span
|
|
ctx.strokeStyle = Qt.rgba(tab.panel.fg.r, tab.panel.fg.g,
|
|
tab.panel.fg.b, 0.4)
|
|
ctx.lineWidth = 1
|
|
ctx.beginPath()
|
|
ctx.moveTo(cx + 0.5, 0)
|
|
ctx.lineTo(cx + 0.5, height)
|
|
ctx.stroke()
|
|
var label = Qt.formatTime(new Date(best.t * 1000), "HH:mm:ss")
|
|
+ " · \u2193 " + Fmt.rate(best.rx) + " · \u2191 " + Fmt.rate(best.tx)
|
|
Readout.draw(ctx, tab.panel.fontFamily, label, cx, width,
|
|
tab.panel.fg, Color.popups.background)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Row {
|
|
width: parent.width
|
|
spacing: Style.space(14)
|
|
readonly property real cell: (width - Style.space(14) * 3) / 4
|
|
|
|
component BigStat: Column {
|
|
property string label: ""
|
|
property string value: ""
|
|
property color tint: tab.panel.fg
|
|
spacing: Style.space(3)
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
text: parent.label
|
|
color: tab.panel.dim
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.caption
|
|
font.letterSpacing: 1
|
|
}
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
text: parent.value
|
|
color: parent.tint
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.title
|
|
font.weight: Font.Medium
|
|
}
|
|
}
|
|
|
|
BigStat {
|
|
width: parent.cell
|
|
label: "RECEIVING"
|
|
tint: Color.accent
|
|
value: Fmt.rate(tab.panel.live && tab.panel.live.rates
|
|
? tab.panel.live.rates.rx_bps : null)
|
|
}
|
|
BigStat {
|
|
width: parent.cell
|
|
label: "SENDING"
|
|
tint: tab.panel.warnTone
|
|
value: Fmt.rate(tab.panel.live && tab.panel.live.rates
|
|
? tab.panel.live.rates.tx_bps : null)
|
|
}
|
|
BigStat {
|
|
width: parent.cell
|
|
label: "DOWNLOADED"
|
|
value: Fmt.bytes(tab.panel.live && tab.panel.live.rates
|
|
? tab.panel.live.rates.rx_total : null)
|
|
}
|
|
BigStat {
|
|
width: parent.cell
|
|
label: "UPLOADED"
|
|
value: Fmt.bytes(tab.panel.live && tab.panel.live.rates
|
|
? tab.panel.live.rates.tx_total : null)
|
|
}
|
|
}
|
|
|
|
PanelSeparator { width: parent.width }
|
|
|
|
// ---- content-check history ---------------------------------------------
|
|
Item {
|
|
width: parent.width
|
|
height: historyLabel.implicitHeight
|
|
|
|
Text {
|
|
id: historyLabel
|
|
textFormat: Text.PlainText
|
|
text: "CONTENT SPEED · FEEDS YOUR SCORE"
|
|
color: tab.panel.dim
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.caption
|
|
font.letterSpacing: 1
|
|
}
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
anchors.right: parent.right
|
|
text: tab.contentTests.length + " checks kept"
|
|
color: tab.panel.dim
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.caption
|
|
}
|
|
}
|
|
|
|
Item {
|
|
width: parent.width
|
|
height: Style.space(80)
|
|
visible: tab.contentTests.length > 0
|
|
|
|
Rectangle {
|
|
anchors.bottom: parent.bottom
|
|
width: parent.width
|
|
height: 1
|
|
color: Qt.rgba(tab.panel.fg.r, tab.panel.fg.g, tab.panel.fg.b, 0.15)
|
|
}
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
|
|
readonly property real slot: width / Math.max(1, tab.contentTests.length)
|
|
|
|
Repeater {
|
|
model: tab.contentTests
|
|
|
|
Item {
|
|
id: barSlot
|
|
required property var modelData
|
|
width: parent.slot
|
|
height: parent.height
|
|
|
|
// The pair sits centered in its slot with capped widths, so a
|
|
// panel with two checks still reads as two paired results, not
|
|
// four unrelated bars.
|
|
Row {
|
|
anchors.bottom: parent.bottom
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
spacing: Style.space(3)
|
|
|
|
Rectangle {
|
|
anchors.bottom: parent.bottom
|
|
width: Math.min(Style.space(22), barSlot.width * 0.4)
|
|
height: Math.max(2, barSlot.height *
|
|
(barSlot.modelData.down_mbps || 0) / tab.bestRate)
|
|
color: Color.accent
|
|
}
|
|
Rectangle {
|
|
anchors.bottom: parent.bottom
|
|
width: Math.min(Style.space(12), barSlot.width * 0.25)
|
|
height: Math.max(2, barSlot.height *
|
|
(barSlot.modelData.up_mbps || 0) / tab.bestRate)
|
|
color: tab.panel.warnTone
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
visible: tab.contentTests.length === 0
|
|
text: "No content checks yet — the first runs shortly after the daemon starts."
|
|
color: tab.panel.dim
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
}
|
|
|
|
PanelSeparator { width: parent.width }
|
|
|
|
// ---- last peak ----------------------------------------------------------
|
|
Item {
|
|
width: parent.width
|
|
height: peakLabel.implicitHeight
|
|
|
|
Text {
|
|
id: peakLabel
|
|
textFormat: Text.PlainText
|
|
text: tab.lastPeak
|
|
? "PEAK · " + new Date(tab.lastPeak.ts * 1000).toLocaleString(Qt.locale(), "d MMM HH:mm").toUpperCase()
|
|
: "PEAK SPEED"
|
|
color: tab.panel.dim
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.caption
|
|
font.letterSpacing: 1
|
|
}
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
anchors.right: parent.right
|
|
text: "informational, not scored"
|
|
color: tab.panel.dim
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.caption
|
|
}
|
|
}
|
|
|
|
Grid {
|
|
width: parent.width
|
|
columns: 2
|
|
columnSpacing: Style.space(24)
|
|
rowSpacing: Style.space(6)
|
|
visible: tab.lastPeak !== null
|
|
|
|
readonly property real cell: (width - Style.space(24)) / 2
|
|
readonly property var p: tab.lastPeak
|
|
|
|
component KvRow: Item {
|
|
property string k: ""
|
|
property string v: ""
|
|
property color vColor: tab.panel.fg
|
|
height: kText.implicitHeight
|
|
Text {
|
|
id: kText
|
|
textFormat: Text.PlainText
|
|
text: parent.k
|
|
color: tab.panel.dim
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
}
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
anchors.right: parent.right
|
|
width: parent.width - kText.implicitWidth - Style.space(10)
|
|
horizontalAlignment: Text.AlignRight
|
|
elide: Text.ElideLeft
|
|
text: parent.v
|
|
color: parent.vColor
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
}
|
|
}
|
|
|
|
KvRow { width: parent.cell; k: "Download"
|
|
v: parent.p ? (parent.p.down_mbps || 0).toFixed(1) + " Mbps" : "" }
|
|
KvRow { width: parent.cell; k: "Upload"
|
|
v: parent.p && parent.p.up_mbps ? parent.p.up_mbps.toFixed(1) + " Mbps" : "--" }
|
|
KvRow { width: parent.cell; k: "Idle ping"
|
|
v: parent.p && parent.p.ping_idle ? Math.round(parent.p.ping_idle) + " ms" : "--" }
|
|
KvRow {
|
|
width: parent.cell; k: "Loaded ping"
|
|
v: parent.p && parent.p.ping_loaded ? Math.round(parent.p.ping_loaded) + " ms" : "--"
|
|
vColor: parent.p && parent.p.ping_loaded && parent.p.ping_idle
|
|
&& parent.p.ping_loaded > parent.p.ping_idle * 3
|
|
? tab.panel.warnTone : tab.panel.fg
|
|
}
|
|
KvRow { width: parent.cell; k: "Data used"
|
|
v: parent.p && parent.p.bytes ? (parent.p.bytes / 1e6).toFixed(0) + " MB" : "--" }
|
|
KvRow { width: parent.cell; k: "Engine"
|
|
v: parent.p ? parent.p.engine : "" }
|
|
}
|
|
|
|
Item {
|
|
width: parent.width
|
|
height: serverKey.implicitHeight
|
|
visible: tab.lastPeak !== null && !!tab.lastPeak.server
|
|
|
|
Text {
|
|
id: serverKey
|
|
textFormat: Text.PlainText
|
|
text: "Server"
|
|
color: tab.panel.dim
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
}
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
anchors.right: parent.right
|
|
width: parent.width - serverKey.implicitWidth - Style.space(10)
|
|
horizontalAlignment: Text.AlignRight
|
|
elide: Text.ElideLeft
|
|
text: tab.lastPeak && tab.lastPeak.server ? tab.lastPeak.server : ""
|
|
color: tab.panel.fg
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
}
|
|
}
|
|
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
visible: tab.lastPeak === null
|
|
text: "No peak test yet."
|
|
color: tab.panel.dim
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
}
|
|
|
|
PanelSeparator { width: parent.width }
|
|
|
|
Item {
|
|
width: parent.width
|
|
height: Style.space(28)
|
|
|
|
Text {
|
|
textFormat: Text.PlainText
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: parent.width - runButton.width - Style.space(12)
|
|
text: "A peak test saturates the line for ~10 s each way — up to ~600 MB on a fast line. It only runs when you ask."
|
|
color: tab.panel.dim
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.caption
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
|
|
Rectangle {
|
|
id: runButton
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: runText.implicitWidth + Style.space(24)
|
|
height: Style.space(28)
|
|
color: runHover.hovered
|
|
? Style.hoverFillFor(tab.panel.fg, Color.accent)
|
|
: Style.normalFillFor(tab.panel.fg, Color.accent)
|
|
border.width: Style.normalBorderWidth
|
|
border.color: Style.normalBorderFor(tab.panel.fg, Color.accent)
|
|
|
|
Text {
|
|
id: runText
|
|
textFormat: Text.PlainText
|
|
anchors.centerIn: parent
|
|
text: tab.panel.live && tab.panel.live.peak_running ? " Testing…"
|
|
: tab.panel.peakArmed ? " Uses data · press again"
|
|
: " Run test"
|
|
color: tab.panel.fg
|
|
font.family: tab.panel.fontFamily
|
|
font.pixelSize: Style.font.bodySmall
|
|
}
|
|
HoverHandler { id: runHover }
|
|
TapHandler {
|
|
enabled: !(tab.panel.live && tab.panel.live.peak_running)
|
|
onTapped: { tab.panel.runPeakTest(); refreshTimer.start() }
|
|
}
|
|
}
|
|
}
|
|
|
|
// While a peak test runs, poll the tests list so the result appears.
|
|
Timer {
|
|
id: refreshTimer
|
|
interval: 3000
|
|
repeat: true
|
|
running: tab.panel.live && tab.panel.live.peak_running === true
|
|
onTriggered: tab.panel.requestTests()
|
|
onRunningChanged: if (!running) tab.panel.requestTests()
|
|
}
|
|
}
|