| 1 | import QtQuick |
| 2 | import QtQuick.Layouts |
| 3 | import Quickshell |
| 4 | import Quickshell.Wayland |
| 5 | import Quickshell.Hyprland |
| 6 | |
| 7 | // Calendar dashboard under the bar clock (top centre): one card, sections split by dotted lines: |
| 8 | // month calendar | weather (now, next 4 days, daylight) | system (cpu/mem/gpu rings, top processes, threads) | net | media. |
| 9 | // Calendar: Monday first, ISO week numbers, today in a primary circle. Scroll over it / arrows change the month, |
| 10 | // up / down the year, space = back to today, Esc closes. SysStats samples its details only while this is open. |
| 11 | // After glyph-rice (github.com/Nebelung-cloud/glyph-rice, GPL-3.0), cal/Cal.qml and deck/Deck.qml. |
| 12 | PanelWindow { |
| 13 | id: win |
| 14 | property bool opened: false |
| 15 | signal closeRequested() |
| 16 | |
| 17 | property real t: opened ? 1 : 0 |
| 18 | Behavior on t { NumberAnimation { duration: Theme.t; easing.type: Easing.BezierSpline; easing.bezierCurve: Theme.glide } } |
| 19 | |
| 20 | visible: t > 0 |
| 21 | // under the clock: top centre, or next to it (left edge, centred) when the bar is vertical |
| 22 | readonly property bool side: Theme.barVertical |
| 23 | anchors.top: !side |
| 24 | anchors.left: side |
| 25 | margins.top: 6 |
| 26 | margins.left: 8 |
| 27 | implicitWidth: card.width + (side ? 20 : 0) // room for the slide |
| 28 | implicitHeight: card.height + (side ? 0 : 20) |
| 29 | exclusiveZone: 0 |
| 30 | color: "transparent" |
| 31 | mask: Region { item: card } |
| 32 | WlrLayershell.layer: WlrLayer.Overlay |
| 33 | WlrLayershell.namespace: "aurora-calendar" |
| 34 | WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand |
| 35 | |
| 36 | HyprlandFocusGrab { |
| 37 | windows: [win] |
| 38 | active: win.opened |
| 39 | onCleared: win.closeRequested() |
| 40 | } |
| 41 | |
| 42 | Binding { target: SysStats; property: "detailed"; value: win.opened } |
| 43 | |
| 44 | SystemClock { id: clock; precision: SystemClock.Minutes } |
| 45 | readonly property var en: Qt.locale("en_US") |
| 46 | readonly property var now: clock.date |
| 47 | readonly property real h: calCol.implicitHeight // every section is as tall as the calendar |
| 48 | |
| 49 | property int year: 0 |
| 50 | property int month: 0 // 0..11 |
| 51 | function today() { |
| 52 | var d = new Date() |
| 53 | year = d.getFullYear() |
| 54 | month = d.getMonth() |
| 55 | } |
| 56 | function shift(n) { |
| 57 | var m = month + n |
| 58 | year += Math.floor(m / 12) |
| 59 | month = ((m % 12) + 12) % 12 |
| 60 | } |
| 61 | onOpenedChanged: if (opened) { today(); Weather.poke() } |
| 62 | |
| 63 | function isoWeek(d) { |
| 64 | var t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())) |
| 65 | var day = t.getUTCDay() || 7 |
| 66 | t.setUTCDate(t.getUTCDate() + 4 - day) |
| 67 | var y0 = new Date(Date.UTC(t.getUTCFullYear(), 0, 1)) |
| 68 | return Math.ceil(((t - y0) / 86400000 + 1) / 7) |
| 69 | } |
| 70 | // 6 rows x 7 days starting on the Monday on/before the 1st |
| 71 | readonly property var days: { |
| 72 | var first = new Date(year, month, 1) |
| 73 | var start = new Date(year, month, 1 - ((first.getDay() + 6) % 7)) |
| 74 | var out = [] |
| 75 | for (var i = 0; i < 42; i++) |
| 76 | out.push(new Date(start.getFullYear(), start.getMonth(), start.getDate() + i)) |
| 77 | return out |
| 78 | } |
| 79 | function hm(d) { return Theme.hh(d) + ":" + Qt.formatDateTime(d, "mm") } |
| 80 | |
| 81 | // weather of today (sunrise/sunset) and the next days |
| 82 | readonly property var wToday: Weather.dayOf(now) |
| 83 | readonly property var wNext: Weather.after(now, 4) |
| 84 | |
| 85 | // MPRIS doesn't push position updates, so poke it while playing |
| 86 | Timer { |
| 87 | running: win.visible && Media.playing && Media.seekable |
| 88 | interval: 500 |
| 89 | repeat: true |
| 90 | onTriggered: Media.player.positionChanged() |
| 91 | } |
| 92 | |
| 93 | Rectangle { |
| 94 | id: card |
| 95 | x: win.side ? -16 * (1 - win.t) : 0 |
| 96 | y: win.side ? 0 : -16 * (1 - win.t) |
| 97 | opacity: win.t |
| 98 | width: row.width + 52 |
| 99 | height: row.height + 48 |
| 100 | radius: 36 |
| 101 | color: Theme.fade(Theme.s1, Theme.panelAlpha) |
| 102 | border.width: 1 |
| 103 | border.color: Theme.s2 |
| 104 | focus: true |
| 105 | Keys.onEscapePressed: win.closeRequested() |
| 106 | Keys.onLeftPressed: win.shift(-1) |
| 107 | Keys.onRightPressed: win.shift(1) |
| 108 | Keys.onUpPressed: win.shift(-12) |
| 109 | Keys.onDownPressed: win.shift(12) |
| 110 | Keys.onSpacePressed: win.today() |
| 111 | |
| 112 | Row { |
| 113 | id: row |
| 114 | x: 26 |
| 115 | y: 24 |
| 116 | spacing: 22 |
| 117 | |
| 118 | // ---------------- calendar ---------------- |
| 119 | Item { |
| 120 | width: calCol.implicitWidth |
| 121 | height: calCol.implicitHeight |
| 122 | MouseArea { |
| 123 | anchors.fill: parent |
| 124 | acceptedButtons: Qt.NoButton |
| 125 | onWheel: w => win.shift(w.angleDelta.y > 0 ? -1 : 1) |
| 126 | } |
| 127 | ColumnLayout { |
| 128 | id: calCol |
| 129 | width: 288 |
| 130 | spacing: 0 |
| 131 | |
| 132 | Lb { |
| 133 | Layout.leftMargin: 6 |
| 134 | px: 10 |
| 135 | text: win.en.toString(win.now, "dddd · d MMMM") + " · week " + win.isoWeek(win.now) |
| 136 | } |
| 137 | RowLayout { |
| 138 | Layout.fillWidth: true |
| 139 | Layout.topMargin: 12 |
| 140 | Layout.leftMargin: 6 |
| 141 | spacing: 10 |
| 142 | Num { |
| 143 | text: win.en.standaloneMonthName(win.month, Locale.ShortFormat) |
| 144 | font.pixelSize: 44 |
| 145 | MouseArea { |
| 146 | anchors.fill: parent |
| 147 | cursorShape: Qt.PointingHandCursor |
| 148 | onClicked: win.today() |
| 149 | } |
| 150 | } |
| 151 | Lb { |
| 152 | Layout.alignment: Qt.AlignVCenter |
| 153 | text: win.year |
| 154 | } |
| 155 | Item { Layout.fillWidth: true } |
| 156 | IconBtn { implicitHeight: 36; icon: Theme.iChevronLeft; onClicked: win.shift(-1) } |
| 157 | IconBtn { implicitHeight: 36; icon: Theme.iChevron; onClicked: win.shift(1) } |
| 158 | } |
| 159 | Grid { |
| 160 | id: grid |
| 161 | Layout.fillWidth: true |
| 162 | Layout.topMargin: 12 |
| 163 | columns: 8 |
| 164 | readonly property real cw: calCol.width / 8 |
| 165 | |
| 166 | Repeater { |
| 167 | model: ["wk", "m", "t", "w", "t", "f", "s", "s"] |
| 168 | delegate: Lb { |
| 169 | required property var modelData |
| 170 | required property int index |
| 171 | width: grid.cw |
| 172 | height: 24 |
| 173 | horizontalAlignment: Text.AlignHCenter |
| 174 | verticalAlignment: Text.AlignVCenter |
| 175 | text: modelData |
| 176 | px: 10 |
| 177 | color: index === 0 ? Theme.mute : Theme.grey |
| 178 | } |
| 179 | } |
| 180 | Repeater { |
| 181 | model: 48 |
| 182 | delegate: Item { |
| 183 | id: cell |
| 184 | required property int index |
| 185 | readonly property bool isWeek: index % 8 === 0 |
| 186 | // week-number cells use that row's Monday |
| 187 | readonly property var d: win.days[Math.floor(index / 8) * 7 + Math.max(0, (index % 8) - 1)] |
| 188 | readonly property bool inMonth: d.getMonth() === win.month |
| 189 | readonly property bool isToday: !isWeek && d.toDateString() === win.now.toDateString() |
| 190 | readonly property bool weekend: (index % 8) >= 6 |
| 191 | width: grid.cw |
| 192 | height: 34 |
| 193 | Rectangle { |
| 194 | visible: cell.isToday |
| 195 | anchors.centerIn: parent |
| 196 | width: 30 |
| 197 | height: 30 |
| 198 | radius: 15 |
| 199 | color: Theme.fill |
| 200 | } |
| 201 | Text { |
| 202 | anchors.centerIn: parent |
| 203 | text: cell.isWeek ? win.isoWeek(cell.d) : cell.d.getDate() |
| 204 | color: cell.isToday ? Theme.fillFg |
| 205 | : cell.isWeek ? Theme.mute |
| 206 | : !cell.inMonth ? Theme.mute |
| 207 | : cell.weekend ? Theme.grey : Theme.fg |
| 208 | font.family: Theme.mono |
| 209 | font.pixelSize: cell.isWeek ? 10 : 13 |
| 210 | font.bold: cell.isToday |
| 211 | } |
| 212 | } |
| 213 | } |
| 214 | } |
| 215 | } |
| 216 | } |
| 217 | |
| 218 | Sep { h: win.h } |
| 219 | |
| 220 | // ---------------- weather ---------------- |
| 221 | ColumnLayout { |
| 222 | width: 208 |
| 223 | height: win.h |
| 224 | spacing: 0 |
| 225 | |
| 226 | Lb { |
| 227 | Layout.fillWidth: true |
| 228 | elide: Text.ElideRight |
| 229 | px: 10 |
| 230 | color: Weather.error !== "" && !Weather.ok ? Theme.err : Theme.grey |
| 231 | text: !Weather.ok ? (Weather.busy ? "looking outside" : Weather.error || "no weather") |
| 232 | : (Weather.place !== "" ? Weather.place + " · " : "") + Weather.name(Weather.now.code) |
| 233 | } |
| 234 | RowLayout { |
| 235 | Layout.topMargin: 14 |
| 236 | spacing: 12 |
| 237 | Icon { |
| 238 | text: Weather.ok ? Weather.icon(Weather.now.code, Weather.now.day) : Theme.iCloudy |
| 239 | font.pixelSize: 46 |
| 240 | opacity: Weather.ok ? 1 : 0.4 |
| 241 | layer.enabled: true |
| 242 | layer.effect: DotMask { step: 3 } |
| 243 | } |
| 244 | Num { |
| 245 | text: Weather.ok ? Math.round(Weather.now.temp) + "°" : "--" |
| 246 | font.pixelSize: 52 |
| 247 | } |
| 248 | } |
| 249 | Lb { |
| 250 | Layout.topMargin: 12 |
| 251 | visible: Weather.ok |
| 252 | px: 10 |
| 253 | text: Weather.ok ? "feels " + Math.round(Weather.now.feels) + "° · " + Math.round(Weather.now.wind) + " km/h" : "" |
| 254 | } |
| 255 | Lb { |
| 256 | Layout.topMargin: 4 |
| 257 | visible: Weather.ok |
| 258 | px: 10 |
| 259 | text: Weather.ok ? "humid " + Math.round(Weather.now.humidity) + "% · rain " + Math.round(Weather.now.rain || 0) + "%" : "" |
| 260 | } |
| 261 | Item { Layout.fillHeight: true } |
| 262 | Row { |
| 263 | spacing: 8 |
| 264 | Repeater { |
| 265 | model: win.wNext |
| 266 | Column { |
| 267 | required property var modelData |
| 268 | width: 46 |
| 269 | Lb { |
| 270 | anchors.horizontalCenter: parent.horizontalCenter |
| 271 | px: 10 |
| 272 | text: win.en.toString(modelData.date, "ddd") |
| 273 | } |
| 274 | Icon { |
| 275 | anchors.horizontalCenter: parent.horizontalCenter |
| 276 | topPadding: lead + 6 |
| 277 | text: Weather.icon(modelData.code, true) |
| 278 | font.pixelSize: 24 |
| 279 | layer.enabled: true |
| 280 | layer.effect: DotMask { step: 2 } |
| 281 | } |
| 282 | Lb { |
| 283 | anchors.horizontalCenter: parent.horizontalCenter |
| 284 | topPadding: 6 |
| 285 | px: 11 |
| 286 | color: Theme.fg |
| 287 | text: Math.round(modelData.max) + "°" |
| 288 | } |
| 289 | Lb { |
| 290 | anchors.horizontalCenter: parent.horizontalCenter |
| 291 | px: 10 |
| 292 | text: Math.round(modelData.min) + "°" |
| 293 | } |
| 294 | } |
| 295 | } |
| 296 | } |
| 297 | // daylight: lit from sunrise to now |
| 298 | Seg { |
| 299 | Layout.fillWidth: true |
| 300 | Layout.topMargin: 16 |
| 301 | Layout.preferredHeight: 6 |
| 302 | n: 24 |
| 303 | f: win.wToday ? (win.now - win.wToday.rise) / (win.wToday.set - win.wToday.rise) : 0 |
| 304 | } |
| 305 | RowLayout { |
| 306 | Layout.fillWidth: true |
| 307 | Layout.topMargin: 10 |
| 308 | Meta { icon: Theme.iSunrise; text: win.wToday ? win.hm(win.wToday.rise) : "--:--" } |
| 309 | Item { Layout.fillWidth: true } |
| 310 | Meta { icon: Theme.iSunset; text: win.wToday ? win.hm(win.wToday.set) : "--:--" } |
| 311 | } |
| 312 | } |
| 313 | |
| 314 | Sep { h: win.h } |
| 315 | |
| 316 | // ---------------- system ---------------- |
| 317 | ColumnLayout { |
| 318 | width: 320 |
| 319 | height: win.h |
| 320 | spacing: 0 |
| 321 | |
| 322 | RowLayout { |
| 323 | Layout.fillWidth: true |
| 324 | spacing: 0 |
| 325 | Gauge { |
| 326 | on: win.opened |
| 327 | f: SysStats.cpu |
| 328 | label: "cpu" + (SysStats.cpuTemp > 0 ? " · " + Math.round(SysStats.cpuTemp) + "°" : "") |
| 329 | } |
| 330 | Item { Layout.fillWidth: true } |
| 331 | Gauge { |
| 332 | on: win.opened |
| 333 | f: SysStats.memTotal > 0 ? SysStats.memUsed / SysStats.memTotal : 0 |
| 334 | label: "mem · " + SysStats.memUsed.toFixed(1) + "g" |
| 335 | } |
| 336 | Item { Layout.fillWidth: true } |
| 337 | // the dGPU if there is one (asleep = idle, never woken for this), else the root disk |
| 338 | Gauge { |
| 339 | readonly property var disk: SysStats.disks.length ? SysStats.disks[0] : null |
| 340 | on: win.opened |
| 341 | f: SysStats.gpuDev !== "" ? SysStats.gpu : disk ? disk.used / disk.size : 0 |
| 342 | label: SysStats.gpuDev === "" ? "disk · " + (disk ? disk.label : "") |
| 343 | : SysStats.gpuAwake ? "gpu · " + Math.round(SysStats.gpuTemp) + "°" : "gpu · asleep" |
| 344 | } |
| 345 | } |
| 346 | Item { Layout.fillHeight: true } |
| 347 | RowLayout { |
| 348 | Layout.fillWidth: true |
| 349 | spacing: 16 |
| 350 | ColumnLayout { |
| 351 | Layout.preferredWidth: 160 |
| 352 | Layout.alignment: Qt.AlignTop |
| 353 | spacing: 5 |
| 354 | Lb { px: 10; text: "processes" } |
| 355 | Repeater { |
| 356 | model: SysStats.procs |
| 357 | RowLayout { |
| 358 | required property var modelData |
| 359 | spacing: 6 |
| 360 | Lb { |
| 361 | Layout.preferredWidth: 78 |
| 362 | elide: Text.ElideRight |
| 363 | px: 10 |
| 364 | color: Theme.fg |
| 365 | text: modelData[0] |
| 366 | } |
| 367 | Lb { |
| 368 | Layout.preferredWidth: 34 |
| 369 | horizontalAlignment: Text.AlignRight |
| 370 | px: 10 |
| 371 | text: Math.round(modelData[1]) + "%" |
| 372 | } |
| 373 | Seg { |
| 374 | Layout.preferredWidth: 36 |
| 375 | Layout.preferredHeight: 5 |
| 376 | n: 6 |
| 377 | spacing: 3 |
| 378 | f: modelData[1] / 100 |
| 379 | } |
| 380 | } |
| 381 | } |
| 382 | } |
| 383 | ColumnLayout { |
| 384 | Layout.fillWidth: true |
| 385 | Layout.alignment: Qt.AlignTop |
| 386 | spacing: 8 |
| 387 | Lb { |
| 388 | px: 10 |
| 389 | text: "threads" + (SysStats.cpuFreq > 0 ? " · " + SysStats.cpuFreq.toFixed(1) + " ghz" : "") |
| 390 | } |
| 391 | // a column per thread: the top dot is the level, the ones below it trail |
| 392 | Grid { |
| 393 | id: cores |
| 394 | readonly property int n: Math.max(1, SysStats.cores.length) |
| 395 | readonly property real d: (width - (n - 1) * spacing) / n |
| 396 | Layout.fillWidth: true |
| 397 | columns: n |
| 398 | spacing: 2 |
| 399 | Repeater { |
| 400 | model: cores.n * 8 |
| 401 | Rectangle { |
| 402 | required property int index |
| 403 | readonly property int up: 8 - Math.floor(index / cores.n) // 8 top .. 1 bottom |
| 404 | readonly property int lvl: Math.round(1 + 7 * (SysStats.cores[index % cores.n] || 0)) |
| 405 | width: cores.d |
| 406 | height: cores.d |
| 407 | radius: cores.d / 2 |
| 408 | color: up === lvl ? Theme.fill : up < lvl ? Theme.fade(Theme.fill, 0.4) : Theme.s3 |
| 409 | } |
| 410 | } |
| 411 | } |
| 412 | } |
| 413 | } |
| 414 | } |
| 415 | |
| 416 | Sep { h: win.h } |
| 417 | |
| 418 | // ---------------- net ---------------- |
| 419 | ColumnLayout { |
| 420 | width: 180 |
| 421 | height: win.h |
| 422 | spacing: 0 |
| 423 | |
| 424 | Lb { |
| 425 | Layout.fillWidth: true |
| 426 | elide: Text.ElideRight |
| 427 | px: 10 |
| 428 | text: "net · " + (Net.connected !== "" ? Net.connected : SysStats.iface !== "" ? SysStats.iface : "offline") |
| 429 | } |
| 430 | Speed { Layout.topMargin: 16; icon: Theme.iArrowDown; b: SysStats.rx } |
| 431 | Speed { Layout.topMargin: 10; icon: Theme.iArrowUp; b: SysStats.tx } |
| 432 | Item { Layout.fillHeight: true } |
| 433 | // the last 40s: download grows up from the middle, upload (dimmer) down |
| 434 | Grid { |
| 435 | id: hist |
| 436 | readonly property var pts: SysStats.netHistory |
| 437 | readonly property real peak: { |
| 438 | var p = 10240 |
| 439 | for (var i = 0; i < pts.length; i++) p = Math.max(p, pts[i][0], pts[i][1]) |
| 440 | return p |
| 441 | } |
| 442 | readonly property real d: (width - 19 * spacing) / 20 |
| 443 | Layout.fillWidth: true |
| 444 | columns: 20 |
| 445 | spacing: 3 |
| 446 | Repeater { |
| 447 | model: 20 * 10 |
| 448 | Rectangle { |
| 449 | required property int index |
| 450 | readonly property int r: Math.floor(index / 20) // 0..9 top to bottom |
| 451 | readonly property bool up: r >= 5 |
| 452 | readonly property var p: hist.pts[index % 20 - 20 + hist.pts.length] || [0, 0] |
| 453 | readonly property real v: up ? p[1] : p[0] |
| 454 | readonly property bool lit: v > 0 && (up ? r - 4 : 5 - r) <= Math.ceil(5 * v / hist.peak) |
| 455 | width: hist.d |
| 456 | height: hist.d |
| 457 | radius: hist.d / 2 |
| 458 | color: lit ? (up ? Theme.grey : Theme.fill) : Theme.s3 |
| 459 | } |
| 460 | } |
| 461 | } |
| 462 | } |
| 463 | |
| 464 | Sep { h: win.h; visible: Media.player !== null } |
| 465 | |
| 466 | // ---------------- media ---------------- |
| 467 | ColumnLayout { |
| 468 | visible: Media.player !== null |
| 469 | width: 250 |
| 470 | height: win.h |
| 471 | spacing: 0 |
| 472 | |
| 473 | Lb { |
| 474 | Layout.fillWidth: true |
| 475 | elide: Text.ElideRight |
| 476 | px: 10 |
| 477 | text: Media.player ? Media.player.identity : "" |
| 478 | } |
| 479 | RowLayout { |
| 480 | Layout.fillWidth: true |
| 481 | Layout.topMargin: 16 |
| 482 | spacing: 14 |
| 483 | // the cover as dots, turning while it plays |
| 484 | DotArt { |
| 485 | Layout.preferredWidth: 84 |
| 486 | Layout.preferredHeight: 84 |
| 487 | step: 5 |
| 488 | url: Media.player ? Media.player.trackArtUrl : "" |
| 489 | RotationAnimation on rotation { |
| 490 | from: 0 |
| 491 | to: 360 |
| 492 | duration: 14000 |
| 493 | loops: Animation.Infinite |
| 494 | running: win.visible && Theme.motion |
| 495 | paused: running && !Media.playing |
| 496 | } |
| 497 | } |
| 498 | ColumnLayout { |
| 499 | Layout.fillWidth: true |
| 500 | spacing: 2 |
| 501 | Text { |
| 502 | Layout.fillWidth: true |
| 503 | wrapMode: Text.Wrap |
| 504 | maximumLineCount: 2 |
| 505 | elide: Text.ElideRight |
| 506 | text: Media.player && Media.player.trackTitle ? Media.player.trackTitle : "nothing on" |
| 507 | color: Theme.fg |
| 508 | font.family: Theme.ui |
| 509 | font.pixelSize: 17 |
| 510 | font.weight: Font.DemiBold |
| 511 | } |
| 512 | Text { |
| 513 | Layout.fillWidth: true |
| 514 | elide: Text.ElideRight |
| 515 | text: Media.player ? Media.player.trackArtist : "" |
| 516 | color: Theme.fgDim |
| 517 | font.family: Theme.ui |
| 518 | font.pixelSize: 13 |
| 519 | } |
| 520 | } |
| 521 | } |
| 522 | Item { Layout.fillHeight: true } |
| 523 | Seg { |
| 524 | Layout.fillWidth: true |
| 525 | Layout.preferredHeight: 6 |
| 526 | n: 20 |
| 527 | f: Media.seekable ? Media.player.position / Media.player.length : 0 |
| 528 | } |
| 529 | RowLayout { |
| 530 | Layout.fillWidth: true |
| 531 | Layout.topMargin: 8 |
| 532 | Lb { px: 10; text: Media.seekable ? Media.fmt(Media.player.position) : "" } |
| 533 | Item { Layout.fillWidth: true } |
| 534 | Lb { px: 10; text: Media.seekable ? Media.fmt(Media.player.length) : "" } |
| 535 | } |
| 536 | PlayControls { |
| 537 | Layout.alignment: Qt.AlignHCenter |
| 538 | Layout.topMargin: 12 |
| 539 | size: 20 |
| 540 | play: 48 |
| 541 | } |
| 542 | } |
| 543 | } |
| 544 | } |
| 545 | |
| 546 | // dotted line between sections (inline components can't see the file's ids: everything comes in as properties) |
| 547 | component Sep: Column { |
| 548 | id: sep |
| 549 | property real h |
| 550 | spacing: 7 |
| 551 | Repeater { |
| 552 | model: Math.floor((sep.h + 7) / 10) |
| 553 | Rectangle { width: 3; height: 3; radius: 1.5; color: Theme.s3 } |
| 554 | } |
| 555 | } |
| 556 | // dot ring with the percentage inside and a label under it; lights in a wave when the popup opens |
| 557 | component Gauge: Column { |
| 558 | id: g |
| 559 | property real f |
| 560 | property string label |
| 561 | property bool on |
| 562 | spacing: 8 |
| 563 | Item { |
| 564 | anchors.horizontalCenter: parent.horizontalCenter |
| 565 | width: 88 |
| 566 | height: 88 |
| 567 | Ring { anchors.fill: parent; anchors.margins: 2; n: 32; dot: 4; f: g.on ? g.f : 0 } |
| 568 | Num { anchors.centerIn: parent; text: Math.round(g.f * 100); font.pixelSize: 30 } |
| 569 | } |
| 570 | Lb { anchors.horizontalCenter: parent.horizontalCenter; px: 10; text: g.label } |
| 571 | } |
| 572 | // arrow, Doto number, unit |
| 573 | component Speed: RowLayout { |
| 574 | id: sp |
| 575 | property string icon |
| 576 | property real b |
| 577 | readonly property var v: SysStats.rate(b) |
| 578 | spacing: 8 |
| 579 | Icon { text: sp.icon; font.pixelSize: 18 } |
| 580 | Num { text: sp.v[0]; font.pixelSize: 44 } |
| 581 | Lb { px: 10; text: sp.v[1] } |
| 582 | } |
| 583 | component Meta: RowLayout { |
| 584 | id: m |
| 585 | property string icon |
| 586 | property string text |
| 587 | spacing: 8 |
| 588 | Icon { text: m.icon; font.pixelSize: 16; color: Theme.grey } |
| 589 | Lb { px: 10; text: m.text } |
| 590 | } |
| 591 | } |