neiam
/gitgud-plasmoid
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
A live CI wall: a window, and wall mode on the desktop
9cdb602 · gmorell · 2026-11-03 19:38
Message
{commit_body(@commit)}
Files changed
modified
README.md
+19
−0
@@ -10,6 +10,7 @@ A Plasma 6 panel widget that shows [GitGud](https://gg.neiam.co) CI runs as a ro
| 10 | 10 | - **Circles**: one per watched repository, or one per organization/user if you fold it into a single circle. The colour shows the latest run (see [Statuses](#statuses)). Hover a circle for the workflow, run number, ref, SHA and age. Click it to open just that circle's runs. |
| 11 | 11 | - **Pin** (the pin button in the popup's header, *Keep Open*): the popup stays open when you click elsewhere, as the system tray's does. Click it again to unpin. The setting is remembered. |
| 12 | 12 | - **Run windows**: break a run out of the popup into its own window. See [Run windows](#run-windows). |
| 13 | +- **Live CI wall**: every job running right now as a grid of cards, in its own window or as the widget itself on the desktop. See [Live CI wall](#live-ci-wall). | |
| 13 | 14 | - **Notify when finished**: ring the bell on a run or a single job and get a desktop notification when it ends, or ask to hear about every job of a run as it finishes. See [Notifications](#notifications). |
| 14 | 15 | |
| 15 | 16 | ## Statuses |
@@ -61,6 +62,23 @@ Click the window button on a run (on hover, or the one next to **Open** once it'
| 61 | 62 | - While the run is in flight the window refreshes at the fast interval (*While a run is in flight*), even if the popup is closed and the run has dropped out of the list. Once the run finishes it stops. |
| 62 | 63 | - Each run gets one window. Opening it again brings the existing window forward. Open as many runs as you like. Closing a window discards it. |
| 63 | 64 | |
| 65 | +## Live CI wall | |
| 66 | + | |
| 67 | +Every job running right now, a card each, tiled to fill the space, the same as GitGud's `/users/ci/live` page and the Android app's wall: | |
| 68 | + | |
| 69 | +- Up to six cards side by side, one column per ~13 grid units of width. After that the columns fill up from the right in pairs: the 7th card shares the rightmost column, the 8th the next one in, and so on. The first cards in order keep the most room. When cards would get too short to read, the wall uses more columns instead (still at most six). | |
| 70 | +- Each card shows the repository, job, a running clock, the run number, workflow and `ref@sha`, a bar with a segment per step, the step it's on, the end of its log and the runner. Smaller cards drop the log first, then the details, and finally become one line with the job and its clock. | |
| 71 | +- **Mine / @org** (the drop-down in the header): your jobs, meaning repos you own or reach through an org or team plus runs you triggered, or one organization's. | |
| 72 | +- **Oldest first / Newest first**, or **R**: the order. Both settings are remembered. | |
| 73 | +- It polls every 3 s, and only while a wall is on screen. If a poll fails, the cards stay up and the header says *offline · retrying*. | |
| 74 | + | |
| 75 | +There are two ways to show it: | |
| 76 | + | |
| 77 | +- **A window**: the grid button in the popup's header, or right-click the widget → **Open Live CI Wall**. **F11** (or the button) toggles full screen and **Esc** leaves it. Put it on a spare monitor. | |
| 78 | +- **Wall mode on the desktop**: *Behavior → Live CI wall → Wall mode*. A widget on the desktop (not in a panel) then *is* the wall. Resize it as large as you like. In a panel the widget stays the circles. | |
| 79 | + | |
| 80 | +It needs a GitGud server with the `/api/mobile/v1/ci/live` endpoint and a token with `ci:read`. | |
| 81 | + | |
| 64 | 82 | ## Notifications |
| 65 | 83 | |
| 66 | 84 | Ring the **bell** on a run that is still going (on hover in the popup, in the run's window, or right-click → **Notify When Finished**). The bell stays lit while you're waiting. When the run ends you get a notification such as *neiam/ppl #12 failed*, with the workflow, ref and duration under it and an icon for the outcome. Click it to open the run's window, or use **Open in Browser**. A run is notified once and then stops being watched. Click a lit bell to stop waiting. |
@@ -104,6 +122,7 @@ Ring the **bell** on a run that is still going (on hover in the popup, in the ru
| 104 | 122 | | Runs listed per repository | 10 | | |
| 105 | 123 | | Hide repositories that have never run CI | on | | |
| 106 | 124 | | Notify when any run I triggered fails | off | See [Notifications](#notifications) | |
| 125 | +| Wall mode: on the desktop, show the wall | off | See [Live CI wall](#live-ci-wall) | | |
| 107 | 126 | |
| 108 | 127 | Each poll makes one `/repos` call, which carries every repo's latest CI status. It then calls `/repos/:owner/:name/runs` only for repos that need it, so the fast poll stays cheap even when you watch a large organization. |
| 109 | 128 |
modified
contents/config/main.xml
+16
−0
@@ -31,6 +31,22 @@
| 31 | 31 | <default>10</default> |
| 32 | 32 | </entry> |
| 33 | 33 | |
| 34 | + <!-- The live CI wall: "" for the token owner's own, else an org handle --> | |
| 35 | + <entry name="wallScope" type="String"> | |
| 36 | + <label>Whose running jobs the live wall shows</label> | |
| 37 | + <default></default> | |
| 38 | + </entry> | |
| 39 | + | |
| 40 | + <entry name="wallNewestFirst" type="Bool"> | |
| 41 | + <label>Live wall: newest-started job first</label> | |
| 42 | + <default>false</default> | |
| 43 | + </entry> | |
| 44 | + | |
| 45 | + <entry name="desktopWall" type="Bool"> | |
| 46 | + <label>On the desktop, the widget is the live CI wall</label> | |
| 47 | + <default>false</default> | |
| 48 | + </entry> | |
| 49 | + | |
| 34 | 50 | <entry name="pin" type="Bool"> |
| 35 | 51 | <label>Keep the popup open when it loses focus</label> |
| 36 | 52 | <default>false</default> |
modified
contents/ui/GitGudAPI.qml
+12
−0
@@ -171,6 +171,18 @@ QtObject {
| 171 | 171 | |
| 172 | 172 | // ── CI ────────────────────────────────────────────────────────────── |
| 173 | 173 | |
| 174 | + // The live CI wall: every running job the token's owner cares about, | |
| 175 | + // or one organization's (`org`), each with the plain tail of its log. | |
| 176 | + // {jobs: [{job, run, repository, runner, log: [line]}], polled_at} | |
| 177 | + function ciWall(org, lines, callback) { | |
| 178 | + var path = org ? "/orgs/" + encodeURIComponent(org) + "/ci/live" : "/ci/live" | |
| 179 | + request("GET", path + "?lines=" + (lines || 80), callback) | |
| 180 | + } | |
| 181 | + | |
| 182 | + function wallUrl(org) { | |
| 183 | + return normalizedBase() + (org ? "/orgs/" + org + "/actions/live" : "/users/ci/live") | |
| 184 | + } | |
| 185 | + | |
| 174 | 186 | function listRuns(fullName, limit, callback) { |
| 175 | 187 | request("GET", repoPath(fullName) + "/runs?limit=" + (limit || 10), callback) |
| 176 | 188 | } |
modified
contents/ui/Status.js
+53
−0
@@ -377,3 +377,56 @@ function worstOf(statuses) {
| 377 | 377 | } |
| 378 | 378 | return best |
| 379 | 379 | } |
| 380 | + | |
| 381 | +// ── the live CI wall ────────────────────────────────────────────────── | |
| 382 | + | |
| 383 | +// How many cards each column of the wall holds, left to right: the | |
| 384 | +// web's and the Android app's rule. Up to `maxCols` cards, a column | |
| 385 | +// each; past that the columns fill up from the right — the 7th (of 6) | |
| 386 | +// pairs the rightmost, the 8th the next one in — so the first cards in | |
| 387 | +// order keep the most room. | |
| 388 | +function wallColumns(n, maxCols) { | |
| 389 | + if (n <= 0 || maxCols <= 0) return [] | |
| 390 | + var cols = Math.min(n, maxCols) | |
| 391 | + var base = Math.floor(n / cols) | |
| 392 | + var extra = n % cols | |
| 393 | + var counts = [] | |
| 394 | + for (var i = 0; i < cols; i++) counts.push(i < cols - extra ? base : base + 1) | |
| 395 | + return counts | |
| 396 | +} | |
| 397 | + | |
| 398 | +// `items` split into those columns, in order. | |
| 399 | +function wallSplit(items, maxCols) { | |
| 400 | + var counts = wallColumns(items.length, maxCols) | |
| 401 | + var out = [] | |
| 402 | + var from = 0 | |
| 403 | + for (var i = 0; i < counts.length; i++) { | |
| 404 | + out.push(items.slice(from, from + counts[i])) | |
| 405 | + from += counts[i] | |
| 406 | + } | |
| 407 | + return out | |
| 408 | +} | |
| 409 | + | |
| 410 | +// A running job's clock: "4:07", or "1:02:09" past the hour. | |
| 411 | +function clock(startIso, now) { | |
| 412 | + if (!startIso) return "–" | |
| 413 | + var start = new Date(startIso).getTime() | |
| 414 | + if (isNaN(start)) return "–" | |
| 415 | + var s = Math.max(0, Math.floor(((now || Date.now()) - start) / 1000)) | |
| 416 | + function two(n) { return (n < 10 ? "0" : "") + n } | |
| 417 | + var h = Math.floor(s / 3600) | |
| 418 | + var m = Math.floor((s % 3600) / 60) | |
| 419 | + return h > 0 ? h + ":" + two(m) + ":" + two(s % 60) : m + ":" + two(s % 60) | |
| 420 | +} | |
| 421 | + | |
| 422 | +// The step a running job is on: the one running, else the last done. | |
| 423 | +function currentStep(steps) { | |
| 424 | + steps = steps || [] | |
| 425 | + for (var i = 0; i < steps.length; i++) { | |
| 426 | + if (steps[i].status === "running" || steps[i].status === "in_progress") return steps[i] | |
| 427 | + } | |
| 428 | + for (var j = steps.length - 1; j >= 0; j--) { | |
| 429 | + if (isFinished(effective(steps[j].status, steps[j].conclusion))) return steps[j] | |
| 430 | + } | |
| 431 | + return null | |
| 432 | +} |
added
contents/ui/WallCard.qml
+200
−0
@@ -0,0 +1,200 @@
| 1 | +import QtQuick | |
| 2 | +import QtQuick.Layouts | |
| 3 | +import org.kde.plasma.components as PlasmaComponents3 | |
| 4 | +import org.kde.kirigami as Kirigami | |
| 5 | + | |
| 6 | +import "Status.js" as Status | |
| 7 | + | |
| 8 | +// One running job on the wall. What fits stays: a card stacked four high | |
| 9 | +// drops its log, then its step bar and details, before its name and | |
| 10 | +// clock — the order the web wall drops them in. | |
| 11 | +Rectangle { | |
| 12 | + id: cardItem | |
| 13 | + | |
| 14 | + required property var card | |
| 15 | + property double now: Date.now() | |
| 16 | + | |
| 17 | + readonly property var job: card.job || {} | |
| 18 | + readonly property var run: card.run || {} | |
| 19 | + readonly property var steps: job.steps || [] | |
| 20 | + readonly property real gu: Kirigami.Units.gridUnit | |
| 21 | + readonly property bool roomForLog: height >= gu * 13 | |
| 22 | + readonly property bool roomForMeta: height >= gu * 8 | |
| 23 | + readonly property bool roomForSteps: height >= gu * 6 | |
| 24 | + // Too short for two lines: just the job and its clock. | |
| 25 | + readonly property bool tiny: height < gu * 3 | |
| 26 | + | |
| 27 | + radius: Kirigami.Units.cornerRadius | |
| 28 | + color: Kirigami.Theme.alternateBackgroundColor | |
| 29 | + border.width: 1 | |
| 30 | + border.color: Qt.alpha(Kirigami.Theme.neutralTextColor, 0.35) | |
| 31 | + clip: true | |
| 32 | + | |
| 33 | + // A light sweeping along the top edge: this one is moving. | |
| 34 | + Rectangle { | |
| 35 | + id: shimmerTrack | |
| 36 | + anchors { top: parent.top; left: parent.left; right: parent.right; margins: 1 } | |
| 37 | + height: 2 | |
| 38 | + color: Qt.alpha(Kirigami.Theme.neutralTextColor, 0.25) | |
| 39 | + clip: true | |
| 40 | + | |
| 41 | + Rectangle { | |
| 42 | + width: parent.width * 0.4 | |
| 43 | + height: parent.height | |
| 44 | + gradient: Gradient { | |
| 45 | + orientation: Gradient.Horizontal | |
| 46 | + GradientStop { position: 0; color: "transparent" } | |
| 47 | + GradientStop { position: 0.5; color: Kirigami.Theme.neutralTextColor } | |
| 48 | + GradientStop { position: 1; color: "transparent" } | |
| 49 | + } | |
| 50 | + NumberAnimation on x { | |
| 51 | + from: -shimmerTrack.width * 0.4 | |
| 52 | + to: shimmerTrack.width | |
| 53 | + duration: 1800 | |
| 54 | + loops: Animation.Infinite | |
| 55 | + } | |
| 56 | + } | |
| 57 | + } | |
| 58 | + | |
| 59 | + ColumnLayout { | |
| 60 | + anchors.fill: parent | |
| 61 | + anchors.margins: Kirigami.Units.smallSpacing * 2 | |
| 62 | + anchors.topMargin: Kirigami.Units.smallSpacing * 2 + 2 | |
| 63 | + spacing: Kirigami.Units.smallSpacing | |
| 64 | + | |
| 65 | + RowLayout { | |
| 66 | + Layout.fillWidth: true | |
| 67 | + PlasmaComponents3.Label { | |
| 68 | + Layout.fillWidth: true | |
| 69 | + elide: Text.ElideRight | |
| 70 | + font.pointSize: cardItem.tiny ? Kirigami.Theme.defaultFont.pointSize | |
| 71 | + : Kirigami.Theme.smallFont.pointSize | |
| 72 | + font.bold: cardItem.tiny | |
| 73 | + opacity: cardItem.tiny ? 1 : 0.7 | |
| 74 | + text: cardItem.tiny ? Status.jobLabel(cardItem.job) | |
| 75 | + : (cardItem.card.repository || {}).full_name || "" | |
| 76 | + } | |
| 77 | + PlasmaComponents3.Label { | |
| 78 | + font.family: "monospace" | |
| 79 | + font.bold: true | |
| 80 | + color: Kirigami.Theme.neutralTextColor | |
| 81 | + text: Status.clock(cardItem.job.started_at, cardItem.now) | |
| 82 | + } | |
| 83 | + } | |
| 84 | + | |
| 85 | + PlasmaComponents3.Label { | |
| 86 | + visible: !cardItem.tiny | |
| 87 | + Layout.fillWidth: true | |
| 88 | + elide: Text.ElideRight | |
| 89 | + font.bold: true | |
| 90 | + text: Status.jobLabel(cardItem.job) | |
| 91 | + } | |
| 92 | + | |
| 93 | + PlasmaComponents3.Label { | |
| 94 | + visible: cardItem.roomForMeta | |
| 95 | + Layout.fillWidth: true | |
| 96 | + elide: Text.ElideRight | |
| 97 | + font: Kirigami.Theme.smallFont | |
| 98 | + opacity: 0.7 | |
| 99 | + text: ["#" + cardItem.run.number, | |
| 100 | + Status.runTitle(cardItem.run), | |
| 101 | + [Status.shortRef(cardItem.run.head_ref), Status.shortSha(cardItem.run.head_sha)] | |
| 102 | + .filter(function(s) { return s !== "" }).join("@")] | |
| 103 | + .filter(function(s) { return s !== "" }).join(" · ") | |
| 104 | + } | |
| 105 | + | |
| 106 | + // A segment per step, coloured by how it went. | |
| 107 | + RowLayout { | |
| 108 | + visible: cardItem.roomForSteps && cardItem.steps.length > 0 | |
| 109 | + Layout.fillWidth: true | |
| 110 | + // Its segments fill its height; it mustn't fill the card's. | |
| 111 | + Layout.fillHeight: false | |
| 112 | + Layout.preferredHeight: 4 | |
| 113 | + Layout.maximumHeight: 4 | |
| 114 | + spacing: 2 | |
| 115 | + Repeater { | |
| 116 | + model: cardItem.steps | |
| 117 | + Rectangle { | |
| 118 | + required property var modelData | |
| 119 | + readonly property string status: Status.effective(modelData.status, modelData.conclusion) | |
| 120 | + Layout.fillWidth: true | |
| 121 | + Layout.fillHeight: true | |
| 122 | + radius: 2 | |
| 123 | + color: status === "running" || status === "in_progress" | |
| 124 | + ? Kirigami.Theme.neutralTextColor | |
| 125 | + : Status.isFinished(status) ? Status.color(status, Kirigami.Theme) | |
| 126 | + : Qt.alpha(Kirigami.Theme.disabledTextColor, 0.3) | |
| 127 | + } | |
| 128 | + } | |
| 129 | + } | |
| 130 | + | |
| 131 | + PlasmaComponents3.Label { | |
| 132 | + readonly property var step: Status.currentStep(cardItem.steps) | |
| 133 | + visible: cardItem.roomForSteps && step !== null | |
| 134 | + Layout.fillWidth: true | |
| 135 | + elide: Text.ElideRight | |
| 136 | + font: Kirigami.Theme.smallFont | |
| 137 | + text: step ? (step.name || ("Step " + step.number)) : "" | |
| 138 | + } | |
| 139 | + | |
| 140 | + // The newest lines, bottom-aligned; older ones run off the top | |
| 141 | + // under a fade. | |
| 142 | + Item { | |
| 143 | + id: logBox | |
| 144 | + visible: cardItem.roomForLog | |
| 145 | + Layout.fillWidth: true | |
| 146 | + Layout.fillHeight: true | |
| 147 | + clip: true | |
| 148 | + | |
| 149 | + readonly property real lineHeight: logText.font.pixelSize * 1.3 | |
| 150 | + readonly property int fits: Math.ceil(height / Math.max(1, lineHeight)) + 1 | |
| 151 | + | |
| 152 | + Text { | |
| 153 | + id: logText | |
| 154 | + anchors { left: parent.left; right: parent.right; bottom: parent.bottom } | |
| 155 | + font.family: "monospace" | |
| 156 | + font.pixelSize: Math.round(Kirigami.Theme.smallFont.pixelSize > 0 | |
| 157 | + ? Kirigami.Theme.smallFont.pixelSize * 0.95 : 10) | |
| 158 | + lineHeight: 1.3 | |
| 159 | + lineHeightMode: Text.ProportionalHeight | |
| 160 | + wrapMode: Text.NoWrap | |
| 161 | + elide: Text.ElideNone | |
| 162 | + textFormat: Text.PlainText | |
| 163 | + color: Kirigami.Theme.textColor | |
| 164 | + opacity: 0.85 | |
| 165 | + text: (cardItem.card.log || []).slice(-logBox.fits).join("\n") | |
| 166 | + } | |
| 167 | + | |
| 168 | + Rectangle { | |
| 169 | + anchors { left: parent.left; right: parent.right; top: parent.top } | |
| 170 | + height: parent.height * 0.3 | |
| 171 | + gradient: Gradient { | |
| 172 | + GradientStop { position: 0; color: cardItem.color } | |
| 173 | + GradientStop { position: 1; color: Qt.alpha(cardItem.color, 0) } | |
| 174 | + } | |
| 175 | + } | |
| 176 | + } | |
| 177 | + | |
| 178 | + Item { visible: !cardItem.roomForLog; Layout.fillHeight: true } | |
| 179 | + | |
| 180 | + RowLayout { | |
| 181 | + visible: cardItem.roomForMeta | |
| 182 | + Layout.fillWidth: true | |
| 183 | + spacing: Kirigami.Units.smallSpacing | |
| 184 | + Kirigami.Icon { | |
| 185 | + source: "cpu" | |
| 186 | + Layout.preferredWidth: Kirigami.Units.iconSizes.small | |
| 187 | + Layout.preferredHeight: Kirigami.Units.iconSizes.small | |
| 188 | + opacity: 0.6 | |
| 189 | + } | |
| 190 | + PlasmaComponents3.Label { | |
| 191 | + Layout.fillWidth: true | |
| 192 | + elide: Text.ElideRight | |
| 193 | + font: Kirigami.Theme.smallFont | |
| 194 | + opacity: 0.6 | |
| 195 | + text: [(cardItem.card.runner || {}).name || "runner", (cardItem.card.runner || {}).architecture || ""] | |
| 196 | + .filter(function(s) { return s !== "" }).join(" · ") | |
| 197 | + } | |
| 198 | + } | |
| 199 | + } | |
| 200 | +} |
added
contents/ui/WallView.qml
+218
−0
@@ -0,0 +1,218 @@
| 1 | +import QtQuick | |
| 2 | +import QtQuick.Layouts | |
| 3 | +import QtQuick.Controls as QQC2 | |
| 4 | +import org.kde.plasma.components as PlasmaComponents3 | |
| 5 | +import org.kde.kirigami as Kirigami | |
| 6 | + | |
| 7 | +import "Status.js" as Status | |
| 8 | + | |
| 9 | +// The live CI wall: every job running right now, a card each, tiled to | |
| 10 | +// fill the space — GitGud's /users/ci/live, as a window of its own or as | |
| 11 | +// the widget itself on the desktop (wall mode). | |
| 12 | +// | |
| 13 | +// Up to six cards side by side (fewer when they'd get too narrow to read | |
| 14 | +// a log line in); past that the columns fill up from the right in pairs, | |
| 15 | +// as on the web and in the Android app. The order is the operator's: | |
| 16 | +// oldest-started first, or newest (the header's buttons, or R). So is | |
| 17 | +// what it shows: your own jobs, or one organization's. | |
| 18 | +// | |
| 19 | +// All state lives in `store` (main.qml): it polls /ci/live while any | |
| 20 | +// wall is on screen, so a window and a desktop wall share one feed. | |
| 21 | +Item { | |
| 22 | + id: wall | |
| 23 | + | |
| 24 | + required property var store | |
| 25 | + // Leave the header's window-only buttons out on the desktop. | |
| 26 | + property bool inWindow: false | |
| 27 | + property bool fullScreen: false | |
| 28 | + signal toggleFullScreen() | |
| 29 | + | |
| 30 | + readonly property var cards: store.wallCards | |
| 31 | + readonly property var ordered: cards ? (store.wallNewestFirst ? cards.slice().reverse() : cards) : null | |
| 32 | + // One column per 13 grid units of width, at most six; more (up to six) | |
| 33 | + // when a column would stack more cards than fit at a readable height | |
| 34 | + // — a small desktop widget would otherwise draw slivers. | |
| 35 | + readonly property int widthCols: Math.max(1, Math.min(6, Math.floor(grid.width / (Kirigami.Units.gridUnit * 13)))) | |
| 36 | + readonly property int rowsFit: Math.max(1, Math.floor(grid.height / (Kirigami.Units.gridUnit * 3.5))) | |
| 37 | + readonly property int maxCols: cards ? Math.min(6, Math.max(widthCols, Math.ceil(cards.length / rowsFit))) : widthCols | |
| 38 | + readonly property var columns: ordered ? Status.wallSplit(ordered, maxCols) : [] | |
| 39 | + // A small desktop widget: the header keeps only what it must. | |
| 40 | + readonly property bool narrow: width < Kirigami.Units.gridUnit * 34 | |
| 41 | + | |
| 42 | + Component.onCompleted: store.wallViewers += 1 | |
| 43 | + Component.onDestruction: store.wallViewers -= 1 | |
| 44 | + | |
| 45 | + ColumnLayout { | |
| 46 | + anchors.fill: parent | |
| 47 | + anchors.margins: Kirigami.Units.smallSpacing | |
| 48 | + spacing: Kirigami.Units.smallSpacing | |
| 49 | + | |
| 50 | + // ── header ────────────────────────────────────────────────────── | |
| 51 | + RowLayout { | |
| 52 | + Layout.fillWidth: true | |
| 53 | + spacing: Kirigami.Units.smallSpacing | |
| 54 | + | |
| 55 | + Rectangle { | |
| 56 | + id: liveDot | |
| 57 | + Layout.preferredWidth: Kirigami.Units.smallSpacing * 2.5 | |
| 58 | + Layout.preferredHeight: width | |
| 59 | + radius: width / 2 | |
| 60 | + color: wall.store.wallError === "" ? Kirigami.Theme.neutralTextColor | |
| 61 | + : Kirigami.Theme.negativeTextColor | |
| 62 | + SequentialAnimation on opacity { | |
| 63 | + loops: Animation.Infinite | |
| 64 | + NumberAnimation { from: 1; to: 0.3; duration: 900 } | |
| 65 | + NumberAnimation { from: 0.3; to: 1; duration: 900 } | |
| 66 | + } | |
| 67 | + } | |
| 68 | + | |
| 69 | + Kirigami.Heading { | |
| 70 | + level: 3 | |
| 71 | + text: i18n("Live CI") | |
| 72 | + } | |
| 73 | + | |
| 74 | + PlasmaComponents3.ComboBox { | |
| 75 | + id: scopeBox | |
| 76 | + // "" is the token owner's own wall; otherwise an org handle. | |
| 77 | + readonly property var scopes: [""].concat(wall.store.wallOrgs.map(function(o) { return o.handle })) | |
| 78 | + model: scopes.map(function(s) { return s === "" ? i18n("Mine") : "@" + s }) | |
| 79 | + currentIndex: Math.max(0, scopes.indexOf(wall.store.wallScope)) | |
| 80 | + onActivated: function(index) { wall.store.setWallScope(scopes[index]) } | |
| 81 | + PlasmaComponents3.ToolTip { text: i18n("Whose running jobs to show") } | |
| 82 | + } | |
| 83 | + | |
| 84 | + PlasmaComponents3.Label { | |
| 85 | + visible: wall.cards !== null && !wall.narrow | |
| 86 | + opacity: 0.7 | |
| 87 | + text: wall.cards ? i18np("1 job", "%1 jobs", wall.cards.length) : "" | |
| 88 | + } | |
| 89 | + | |
| 90 | + // The tiling at a glance: a bar per column, split by its cards. | |
| 91 | + Row { | |
| 92 | + visible: wall.columns.length > 0 && !wall.narrow | |
| 93 | + spacing: 2 | |
| 94 | + Repeater { | |
| 95 | + model: wall.columns | |
| 96 | + Column { | |
| 97 | + id: glyphColumn | |
| 98 | + required property var modelData | |
| 99 | + readonly property int cards: modelData.length | |
| 100 | + spacing: 1 | |
| 101 | + Repeater { | |
| 102 | + model: glyphColumn.cards | |
| 103 | + Rectangle { | |
| 104 | + width: 5 | |
| 105 | + height: (14 - (glyphColumn.cards - 1)) / glyphColumn.cards | |
| 106 | + radius: 1 | |
| 107 | + color: Kirigami.Theme.disabledTextColor | |
| 108 | + } | |
| 109 | + } | |
| 110 | + } | |
| 111 | + } | |
| 112 | + } | |
| 113 | + | |
| 114 | + PlasmaComponents3.Label { | |
| 115 | + visible: wall.store.wallError !== "" && wall.cards !== null | |
| 116 | + color: Kirigami.Theme.negativeTextColor | |
| 117 | + font: Kirigami.Theme.smallFont | |
| 118 | + text: i18n("offline · retrying") | |
| 119 | + PlasmaComponents3.ToolTip { text: wall.store.wallError } | |
| 120 | + } | |
| 121 | + | |
| 122 | + Item { Layout.fillWidth: true } | |
| 123 | + | |
| 124 | + QQC2.ButtonGroup { id: orderGroup } | |
| 125 | + | |
| 126 | + PlasmaComponents3.ToolButton { | |
| 127 | + checkable: true | |
| 128 | + checked: !wall.store.wallNewestFirst | |
| 129 | + QQC2.ButtonGroup.group: orderGroup | |
| 130 | + icon.name: "view-sort-ascending" | |
| 131 | + text: i18n("Oldest first") | |
| 132 | + display: wall.narrow ? QQC2.AbstractButton.IconOnly : QQC2.AbstractButton.TextOnly | |
| 133 | + onClicked: wall.store.setWallNewestFirst(false) | |
| 134 | + PlasmaComponents3.ToolTip { text: i18n("Oldest first (R flips the order)") } | |
| 135 | + } | |
| 136 | + | |
| 137 | + PlasmaComponents3.ToolButton { | |
| 138 | + checkable: true | |
| 139 | + checked: wall.store.wallNewestFirst | |
| 140 | + QQC2.ButtonGroup.group: orderGroup | |
| 141 | + icon.name: "view-sort-descending" | |
| 142 | + text: i18n("Newest first") | |
| 143 | + display: wall.narrow ? QQC2.AbstractButton.IconOnly : QQC2.AbstractButton.TextOnly | |
| 144 | + onClicked: wall.store.setWallNewestFirst(true) | |
| 145 | + PlasmaComponents3.ToolTip { text: i18n("Newest first (R flips the order)") } | |
| 146 | + } | |
| 147 | + | |
| 148 | + PlasmaComponents3.ToolButton { | |
| 149 | + visible: wall.inWindow | |
| 150 | + icon.name: wall.fullScreen ? "view-restore" : "view-fullscreen" | |
| 151 | + text: wall.fullScreen ? i18n("Exit Full Screen") : i18n("Full Screen") | |
| 152 | + display: QQC2.AbstractButton.IconOnly | |
| 153 | + onClicked: wall.toggleFullScreen() | |
| 154 | + PlasmaComponents3.ToolTip { text: wall.fullScreen ? i18n("Exit Full Screen (Esc)") : i18n("Full Screen (F11)") } | |
| 155 | + } | |
| 156 | + | |
| 157 | + PlasmaComponents3.ToolButton { | |
| 158 | + icon.name: "internet-web-browser" | |
| 159 | + text: i18n("Open in Browser") | |
| 160 | + display: QQC2.AbstractButton.IconOnly | |
| 161 | + onClicked: Qt.openUrlExternally(wall.store.wallWebUrl()) | |
| 162 | + PlasmaComponents3.ToolTip { text: i18n("Open in Browser") } | |
| 163 | + } | |
| 164 | + } | |
| 165 | + | |
| 166 | + // ── cards ─────────────────────────────────────────────────────── | |
| 167 | + Item { | |
| 168 | + id: grid | |
| 169 | + Layout.fillWidth: true | |
| 170 | + Layout.fillHeight: true | |
| 171 | + | |
| 172 | + PlasmaComponents3.Label { | |
| 173 | + anchors.centerIn: parent | |
| 174 | + width: parent.width - Kirigami.Units.gridUnit * 2 | |
| 175 | + visible: wall.ordered === null || wall.ordered.length === 0 | |
| 176 | + horizontalAlignment: Text.AlignHCenter | |
| 177 | + wrapMode: Text.WordWrap | |
| 178 | + opacity: 0.7 | |
| 179 | + text: !wall.store.wallConfigured | |
| 180 | + ? i18n("Configure the widget to connect to your GitGud instance") | |
| 181 | + : wall.ordered === null | |
| 182 | + ? (wall.store.wallError !== "" ? wall.store.wallError : i18n("Loading…")) | |
| 183 | + : i18n("Nothing running. Jobs appear here the moment a runner picks them up.") | |
| 184 | + } | |
| 185 | + | |
| 186 | + RowLayout { | |
| 187 | + anchors.fill: parent | |
| 188 | + spacing: Kirigami.Units.smallSpacing | |
| 189 | + | |
| 190 | + Repeater { | |
| 191 | + model: wall.columns | |
| 192 | + | |
| 193 | + ColumnLayout { | |
| 194 | + id: column | |
| 195 | + required property var modelData | |
| 196 | + Layout.fillWidth: true | |
| 197 | + Layout.fillHeight: true | |
| 198 | + Layout.preferredWidth: 1 // equal shares | |
| 199 | + spacing: Kirigami.Units.smallSpacing | |
| 200 | + | |
| 201 | + Repeater { | |
| 202 | + model: column.modelData | |
| 203 | + | |
| 204 | + WallCard { | |
| 205 | + required property var modelData | |
| 206 | + Layout.fillWidth: true | |
| 207 | + Layout.fillHeight: true | |
| 208 | + Layout.preferredHeight: 1 // equal shares | |
| 209 | + card: modelData | |
| 210 | + now: wall.store.now | |
| 211 | + } | |
| 212 | + } | |
| 213 | + } | |
| 214 | + } | |
| 215 | + } | |
| 216 | + } | |
| 217 | + } | |
| 218 | +} |
added
contents/ui/WallWindow.qml
+55
−0
@@ -0,0 +1,55 @@
| 1 | +import QtQuick | |
| 2 | +import QtQuick.Window | |
| 3 | +import org.kde.kirigami as Kirigami | |
| 4 | + | |
| 5 | +// The live CI wall in a window of its own, for a second screen: F11 for | |
| 6 | +// full screen, Esc to leave it, R to flip the order. Created by | |
| 7 | +// store.openWallWindow(); there's one, and closing it destroys it. | |
| 8 | +Window { | |
| 9 | + id: wallWindow | |
| 10 | + | |
| 11 | + required property var store | |
| 12 | + | |
| 13 | + transientParent: null | |
| 14 | + flags: Qt.Window | |
| 15 | + width: Kirigami.Units.gridUnit * 64 | |
| 16 | + height: Kirigami.Units.gridUnit * 36 | |
| 17 | + minimumWidth: Kirigami.Units.gridUnit * 20 | |
| 18 | + minimumHeight: Kirigami.Units.gridUnit * 12 | |
| 19 | + title: i18n("Live CI") | |
| 20 | + | |
| 21 | + Kirigami.Theme.colorSet: Kirigami.Theme.Window | |
| 22 | + Kirigami.Theme.inherit: false | |
| 23 | + color: Kirigami.Theme.backgroundColor | |
| 24 | + | |
| 25 | + readonly property bool fullScreen: visibility === Window.FullScreen | |
| 26 | + | |
| 27 | + onClosing: { | |
| 28 | + store.forgetWallWindow(wallWindow) | |
| 29 | + wallWindow.destroy() | |
| 30 | + } | |
| 31 | + | |
| 32 | + function present() { | |
| 33 | + if (visibility === Window.Minimized) showNormal() | |
| 34 | + else show() | |
| 35 | + raise() | |
| 36 | + requestActivate() | |
| 37 | + } | |
| 38 | + | |
| 39 | + function toggleFullScreen() { | |
| 40 | + if (fullScreen) showNormal() | |
| 41 | + else showFullScreen() | |
| 42 | + } | |
| 43 | + | |
| 44 | + Shortcut { sequences: [StandardKey.FullScreen]; onActivated: wallWindow.toggleFullScreen() } | |
| 45 | + Shortcut { sequence: "Escape"; enabled: wallWindow.fullScreen; onActivated: wallWindow.showNormal() } | |
| 46 | + Shortcut { sequence: "R"; onActivated: wallWindow.store.setWallNewestFirst(!wallWindow.store.wallNewestFirst) } | |
| 47 | + | |
| 48 | + WallView { | |
| 49 | + anchors.fill: parent | |
| 50 | + store: wallWindow.store | |
| 51 | + inWindow: true | |
| 52 | + fullScreen: wallWindow.fullScreen | |
| 53 | + onToggleFullScreen: wallWindow.toggleFullScreen() | |
| 54 | + } | |
| 55 | +} |
modified
contents/ui/configBehavior.qml
+15
−0
@@ -12,6 +12,7 @@ KCM.SimpleKCM {
| 12 | 12 | property alias cfg_runsPerRepo: runsPerRepoSpinBox.value |
| 13 | 13 | property alias cfg_hideIdle: hideIdleCheckBox.checked |
| 14 | 14 | property alias cfg_notifyMyFailures: notifyMyFailuresCheckBox.checked |
| 15 | + property alias cfg_desktopWall: desktopWallCheckBox.checked | |
| 15 | 16 | |
| 16 | 17 | Kirigami.FormLayout { |
| 17 | 18 | QQC2.SpinBox { |
@@ -52,6 +53,20 @@ KCM.SimpleKCM {
| 52 | 53 | text: i18n("Hide repositories that have never run CI") |
| 53 | 54 | } |
| 54 | 55 | |
| 56 | + QQC2.CheckBox { | |
| 57 | + id: desktopWallCheckBox | |
| 58 | + Kirigami.FormData.label: i18n("Live CI wall:") | |
| 59 | + text: i18n("Wall mode: on the desktop, show the wall") | |
| 60 | + } | |
| 61 | + | |
| 62 | + QQC2.Label { | |
| 63 | + Layout.fillWidth: true | |
| 64 | + wrapMode: Text.WordWrap | |
| 65 | + font: Kirigami.Theme.smallFont | |
| 66 | + color: Kirigami.Theme.disabledTextColor | |
| 67 | + text: i18n("Every job running right now, a card each, tiled to fill the widget. Resize it, or give it a screen of its own. In a panel the widget stays the circles; open the wall from the popup or the right-click menu instead.") | |
| 68 | + } | |
| 69 | + | |
| 55 | 70 | QQC2.CheckBox { |
| 56 | 71 | id: notifyMyFailuresCheckBox |
| 57 | 72 | Kirigami.FormData.label: i18n("Notifications:") |
modified
contents/ui/main.qml
+233
−123
@@ -17,9 +17,11 @@ PlasmoidItem {
| 17 | 17 | |
| 18 | 18 | // In a panel always show the circles, however much room we're given; |
| 19 | 19 | // on the desktop let Plasma pick by size. |
| 20 | + // In wall mode on the desktop, the wall whatever the size. | |
| 20 | 21 | preferredRepresentation: Plasmoid.formFactor === PlasmaCore.Types.Horizontal |
| 21 | 22 | || Plasmoid.formFactor === PlasmaCore.Types.Vertical |
| 22 | − ? compactRepresentation : null | |
| 23 | + ? compactRepresentation | |
| 24 | + : Plasmoid.configuration.desktopWall ? fullRepresentation : null | |
| 23 | 25 | |
| 24 | 26 | Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground | PlasmaCore.Types.ConfigurableBackground |
| 25 | 27 |
@@ -74,6 +76,20 @@ PlasmoidItem {
| 74 | 76 | property double lastFullRefresh: 0 |
| 75 | 77 | property double now: Date.now() |
| 76 | 78 | |
| 79 | + // The live CI wall (WallView): polled only while one is on screen. | |
| 80 | + property var wallCards: null // [{job, run, repository, runner, log}], or null before the first answer | |
| 81 | + property string wallError: "" | |
| 82 | + property var wallOrgs: [] // namespaces of kind "org", for the wall's scope picker | |
| 83 | + property int wallViewers: 0 | |
| 84 | + property int wallGeneration: 0 | |
| 85 | + property var wallWindow: null | |
| 86 | + readonly property string wallScope: Plasmoid.configuration.wallScope | |
| 87 | + readonly property bool wallNewestFirst: Plasmoid.configuration.wallNewestFirst | |
| 88 | + readonly property bool wallConfigured: api.configured | |
| 89 | + readonly property bool onDesktop: Plasmoid.formFactor !== PlasmaCore.Types.Horizontal | |
| 90 | + && Plasmoid.formFactor !== PlasmaCore.Types.Vertical | |
| 91 | + readonly property bool wallMode: onDesktop && Plasmoid.configuration.desktopWall | |
| 92 | + | |
| 77 | 93 | // Following runs outside the list: windows and notifications. |
| 78 | 94 | property var runWindows: ({}) // "owner/name#n" -> RunWindow |
| 79 | 95 | property var notified: ({}) // "<run or job key>@completed_at" -> true, once per ending |
@@ -352,6 +368,7 @@ PlasmoidItem {
| 352 | 368 | if (rows[i].full_name) index[rows[i].full_name] = rows[i] |
| 353 | 369 | } |
| 354 | 370 | repoIndex = index |
| 371 | + wallOrgs = (data.namespaces || []).filter(function(n) { return n.kind === "org" }) | |
| 355 | 372 | lastError = "" |
| 356 | 373 | // Only a refresh that reached the server counts as full. |
| 357 | 374 | if (full) lastFullRefresh = Date.now() |
@@ -489,7 +506,7 @@ PlasmoidItem {
| 489 | 506 | // run window is open. |
| 490 | 507 | Timer { |
| 491 | 508 | interval: 1000 |
| 492 | − running: root.expanded || Object.keys(root.runWindows).length > 0 | |
| 509 | + running: root.expanded || Object.keys(root.runWindows).length > 0 || root.wallViewers > 0 | |
| 493 | 510 | repeat: true |
| 494 | 511 | onTriggered: root.now = Date.now() |
| 495 | 512 | } |
@@ -647,6 +664,76 @@ PlasmoidItem {
| 647 | 664 | if (runWindows[key] === win) runWindows = setKey(runWindows, key, undefined) |
| 648 | 665 | } |
| 649 | 666 | |
| 667 | + // ── the live CI wall ──────────────────────────────────────────────── | |
| 668 | + | |
| 669 | + Timer { | |
| 670 | + interval: 3000 | |
| 671 | + repeat: true | |
| 672 | + triggeredOnStart: true | |
| 673 | + running: root.wallViewers > 0 && root.hasValidConfig | |
| 674 | + onTriggered: root.pollWall() | |
| 675 | + } | |
| 676 | + | |
| 677 | + function pollWall() { | |
| 678 | + var generation = ++wallGeneration | |
| 679 | + // Enough log for a full-height card on a tall screen. | |
| 680 | + api.ciWall(wallScope, 160, function(ok, data) { | |
| 681 | + if (generation !== root.wallGeneration) return | |
| 682 | + if (ok) { | |
| 683 | + wallCards = data.jobs || [] | |
| 684 | + wallError = "" | |
| 685 | + } else { | |
| 686 | + // Keep the last cards up: a wall across the room going | |
| 687 | + // blank says less than one saying it's retrying. | |
| 688 | + wallError = data | |
| 689 | + } | |
| 690 | + }) | |
| 691 | + } | |
| 692 | + | |
| 693 | + function setWallScope(scope) { | |
| 694 | + if (scope === wallScope) return | |
| 695 | + Plasmoid.configuration.wallScope = scope | |
| 696 | + wallCards = null | |
| 697 | + wallError = "" | |
| 698 | + Qt.callLater(pollWall) | |
| 699 | + } | |
| 700 | + | |
| 701 | + function setWallNewestFirst(newest) { | |
| 702 | + Plasmoid.configuration.wallNewestFirst = newest | |
| 703 | + } | |
| 704 | + | |
| 705 | + function wallWebUrl() { | |
| 706 | + return api.wallUrl(wallScope) | |
| 707 | + } | |
| 708 | + | |
| 709 | + Component { | |
| 710 | + id: wallWindowComponent | |
| 711 | + WallWindow {} | |
| 712 | + } | |
| 713 | + | |
| 714 | + function openWallWindow() { | |
| 715 | + if (!wallWindow) { | |
| 716 | + wallWindow = wallWindowComponent.createObject(root, { store: root }) | |
| 717 | + if (!wallWindow) { | |
| 718 | + lastError = i18n("Could not open the live CI wall") | |
| 719 | + return | |
| 720 | + } | |
| 721 | + } | |
| 722 | + wallWindow.present() | |
| 723 | + } | |
| 724 | + | |
| 725 | + function forgetWallWindow(win) { | |
| 726 | + if (wallWindow === win) wallWindow = null | |
| 727 | + } | |
| 728 | + | |
| 729 | + Plasmoid.contextualActions: [ | |
| 730 | + PlasmaCore.Action { | |
| 731 | + text: i18n("Open Live CI Wall") | |
| 732 | + icon.name: "view-grid" | |
| 733 | + onTriggered: root.openWallWindow() | |
| 734 | + } | |
| 735 | + ] | |
| 736 | + | |
| 650 | 737 | // ── notify when finished ──────────────────────────────────────────── |
| 651 | 738 | // |
| 652 | 739 | // Three lists in the config, so they survive a restart: |
@@ -984,151 +1071,174 @@ PlasmoidItem {
| 984 | 1071 | inPanel: true |
| 985 | 1072 | } |
| 986 | 1073 | |
| 987 | − fullRepresentation: ColumnLayout { | |
| 988 | − Layout.minimumWidth: Kirigami.Units.gridUnit * 22 | |
| 989 | − Layout.minimumHeight: Kirigami.Units.gridUnit * 18 | |
| 990 | − Layout.preferredWidth: Kirigami.Units.gridUnit * 30 | |
| 991 | − Layout.preferredHeight: Kirigami.Units.gridUnit * 30 | |
| 992 | − spacing: Kirigami.Units.smallSpacing | |
| 1074 | + fullRepresentation: root.wallMode ? wallRepresentation : popupRepresentation | |
| 993 | 1075 | |
| 994 | − // Header | |
| 995 | − RowLayout { | |
| 996 | − Layout.fillWidth: true | |
| 997 | − spacing: Kirigami.Units.smallSpacing | |
| 1076 | + Component { | |
| 1077 | + id: wallRepresentation | |
| 1078 | + WallView { | |
| 1079 | + Layout.minimumWidth: Kirigami.Units.gridUnit * 16 | |
| 1080 | + Layout.minimumHeight: Kirigami.Units.gridUnit * 10 | |
| 1081 | + Layout.preferredWidth: Kirigami.Units.gridUnit * 48 | |
| 1082 | + Layout.preferredHeight: Kirigami.Units.gridUnit * 27 | |
| 1083 | + store: root | |
| 1084 | + } | |
| 1085 | + } | |
| 998 | 1086 | |
| 999 | − PlasmaComponents3.ToolButton { | |
| 1000 | − visible: root.focusKey !== "" | |
| 1001 | − icon.name: "go-previous" | |
| 1002 | − onClicked: root.focusKey = "" | |
| 1003 | − PlasmaComponents3.ToolTip { text: i18n("All runs") } | |
| 1004 | − } | |
| 1087 | + Component { | |
| 1088 | + id: popupRepresentation | |
| 1089 | + ColumnLayout { | |
| 1090 | + Layout.minimumWidth: Kirigami.Units.gridUnit * 22 | |
| 1091 | + Layout.minimumHeight: Kirigami.Units.gridUnit * 18 | |
| 1092 | + Layout.preferredWidth: Kirigami.Units.gridUnit * 30 | |
| 1093 | + Layout.preferredHeight: Kirigami.Units.gridUnit * 30 | |
| 1094 | + spacing: Kirigami.Units.smallSpacing | |
| 1005 | 1095 | |
| 1006 | − Kirigami.Heading { | |
| 1096 | + // Header | |
| 1097 | + RowLayout { | |
| 1007 | 1098 | Layout.fillWidth: true |
| 1008 | − level: 3 | |
| 1009 | − elide: Text.ElideRight | |
| 1010 | − text: root.focusKey === "" ? i18n("All runs") : root.focusLabel | |
| 1011 | − } | |
| 1099 | + spacing: Kirigami.Units.smallSpacing | |
| 1012 | 1100 | |
| 1013 | − PlasmaComponents3.BusyIndicator { | |
| 1014 | − visible: root.loading || root.busy | |
| 1015 | − Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium | |
| 1016 | − Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium | |
| 1017 | − } | |
| 1101 | + PlasmaComponents3.ToolButton { | |
| 1102 | + visible: root.focusKey !== "" | |
| 1103 | + icon.name: "go-previous" | |
| 1104 | + onClicked: root.focusKey = "" | |
| 1105 | + PlasmaComponents3.ToolTip { text: i18n("All runs") } | |
| 1106 | + } | |
| 1018 | 1107 | |
| 1019 | − PlasmaComponents3.ToolButton { | |
| 1020 | − visible: root.focusKey !== "" && root.focusKey.indexOf("repo:") === 0 | |
| 1021 | − icon.name: "internet-web-browser" | |
| 1022 | − onClicked: Qt.openUrlExternally(api.webUrl(root.focusKey.substring(5)) + "/actions") | |
| 1023 | − PlasmaComponents3.ToolTip { text: i18n("Open in browser") } | |
| 1024 | − } | |
| 1108 | + Kirigami.Heading { | |
| 1109 | + Layout.fillWidth: true | |
| 1110 | + level: 3 | |
| 1111 | + elide: Text.ElideRight | |
| 1112 | + text: root.focusKey === "" ? i18n("All runs") : root.focusLabel | |
| 1113 | + } | |
| 1025 | 1114 | |
| 1026 | − PlasmaComponents3.ToolButton { | |
| 1027 | − icon.name: "view-refresh" | |
| 1028 | − enabled: root.hasValidConfig && !root.loading | |
| 1029 | − onClicked: root.refresh(true) | |
| 1030 | − PlasmaComponents3.ToolTip { text: i18n("Refresh") } | |
| 1031 | − } | |
| 1115 | + PlasmaComponents3.BusyIndicator { | |
| 1116 | + visible: root.loading || root.busy | |
| 1117 | + Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium | |
| 1118 | + Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium | |
| 1119 | + } | |
| 1032 | 1120 | |
| 1033 | − PlasmaComponents3.ToolButton { | |
| 1034 | − icon.name: "configure" | |
| 1035 | − onClicked: Plasmoid.internalAction("configure").trigger() | |
| 1036 | − PlasmaComponents3.ToolTip { text: i18n("Configure") } | |
| 1037 | − } | |
| 1121 | + PlasmaComponents3.ToolButton { | |
| 1122 | + visible: root.focusKey !== "" && root.focusKey.indexOf("repo:") === 0 | |
| 1123 | + icon.name: "internet-web-browser" | |
| 1124 | + onClicked: Qt.openUrlExternally(api.webUrl(root.focusKey.substring(5)) + "/actions") | |
| 1125 | + PlasmaComponents3.ToolTip { text: i18n("Open in browser") } | |
| 1126 | + } | |
| 1038 | 1127 | |
| 1039 | − PlasmaComponents3.ToolButton { | |
| 1040 | − checkable: true | |
| 1041 | − checked: Plasmoid.configuration.pin | |
| 1042 | − onToggled: Plasmoid.configuration.pin = checked | |
| 1043 | − icon.name: "window-pin" | |
| 1044 | − text: i18n("Keep Open") | |
| 1045 | − display: QQC2.AbstractButton.IconOnly | |
| 1046 | − PlasmaComponents3.ToolTip { text: i18n("Keep Open") } | |
| 1047 | − } | |
| 1048 | − } | |
| 1128 | + PlasmaComponents3.ToolButton { | |
| 1129 | + icon.name: "view-refresh" | |
| 1130 | + enabled: root.hasValidConfig && !root.loading | |
| 1131 | + onClicked: root.refresh(true) | |
| 1132 | + PlasmaComponents3.ToolTip { text: i18n("Refresh") } | |
| 1133 | + } | |
| 1049 | 1134 | |
| 1050 | − // The same circles as the panel, as a filter bar. | |
| 1051 | − CircleStrip { | |
| 1052 | − visible: root.hasValidConfig && root.hasWatched && circlesModel.count > 0 | |
| 1053 | − Layout.fillWidth: true | |
| 1054 | − store: root | |
| 1055 | − inPanel: false | |
| 1056 | − } | |
| 1135 | + PlasmaComponents3.ToolButton { | |
| 1136 | + icon.name: "view-grid" | |
| 1137 | + enabled: root.hasValidConfig | |
| 1138 | + onClicked: root.openWallWindow() | |
| 1139 | + PlasmaComponents3.ToolTip { text: i18n("Open Live CI Wall") } | |
| 1140 | + } | |
| 1057 | 1141 | |
| 1058 | − PlasmaComponents3.Label { | |
| 1059 | − visible: root.lastError !== "" | |
| 1060 | − Layout.fillWidth: true | |
| 1061 | − wrapMode: Text.WordWrap | |
| 1062 | − color: Kirigami.Theme.negativeTextColor | |
| 1063 | − font: Kirigami.Theme.smallFont | |
| 1064 | − text: root.lastError | |
| 1065 | − } | |
| 1142 | + PlasmaComponents3.ToolButton { | |
| 1143 | + icon.name: "configure" | |
| 1144 | + onClicked: Plasmoid.internalAction("configure").trigger() | |
| 1145 | + PlasmaComponents3.ToolTip { text: i18n("Configure") } | |
| 1146 | + } | |
| 1066 | 1147 | |
| 1067 | − Kirigami.Separator { Layout.fillWidth: true } | |
| 1148 | + PlasmaComponents3.ToolButton { | |
| 1149 | + checkable: true | |
| 1150 | + checked: Plasmoid.configuration.pin | |
| 1151 | + onToggled: Plasmoid.configuration.pin = checked | |
| 1152 | + icon.name: "window-pin" | |
| 1153 | + text: i18n("Keep Open") | |
| 1154 | + display: QQC2.AbstractButton.IconOnly | |
| 1155 | + PlasmaComponents3.ToolTip { text: i18n("Keep Open") } | |
| 1156 | + } | |
| 1157 | + } | |
| 1068 | 1158 | |
| 1069 | − // Setup prompts | |
| 1070 | − ColumnLayout { | |
| 1071 | − visible: !root.hasValidConfig || !root.hasWatched | |
| 1072 | − Layout.fillWidth: true | |
| 1073 | − Layout.fillHeight: true | |
| 1074 | − spacing: Kirigami.Units.largeSpacing | |
| 1075 | − | |
| 1076 | − Item { Layout.fillHeight: true } | |
| 1077 | − | |
| 1078 | − Kirigami.Icon { | |
| 1079 | − source: root.logoSource | |
| 1080 | − isMask: true | |
| 1081 | − color: Kirigami.Theme.disabledTextColor | |
| 1082 | − Layout.alignment: Qt.AlignHCenter | |
| 1083 | − Layout.preferredWidth: Kirigami.Units.iconSizes.huge | |
| 1084 | − Layout.preferredHeight: Kirigami.Units.iconSizes.huge | |
| 1159 | + // The same circles as the panel, as a filter bar. | |
| 1160 | + CircleStrip { | |
| 1161 | + visible: root.hasValidConfig && root.hasWatched && circlesModel.count > 0 | |
| 1162 | + Layout.fillWidth: true | |
| 1163 | + store: root | |
| 1164 | + inPanel: false | |
| 1085 | 1165 | } |
| 1086 | 1166 | |
| 1087 | 1167 | PlasmaComponents3.Label { |
| 1168 | + visible: root.lastError !== "" | |
| 1088 | 1169 | Layout.fillWidth: true |
| 1089 | − horizontalAlignment: Text.AlignHCenter | |
| 1090 | 1170 | wrapMode: Text.WordWrap |
| 1091 | − text: !root.hasValidConfig | |
| 1092 | − ? i18n("Configure the widget to connect to your GitGud instance") | |
| 1093 | − : i18n("Pick repositories or namespaces to watch") | |
| 1094 | − } | |
| 1095 | − | |
| 1096 | − PlasmaComponents3.Button { | |
| 1097 | − Layout.alignment: Qt.AlignHCenter | |
| 1098 | − icon.name: "configure" | |
| 1099 | − text: !root.hasValidConfig ? i18n("Configure Connection") : i18n("Choose Repositories") | |
| 1100 | − onClicked: Plasmoid.internalAction("configure").trigger() | |
| 1171 | + color: Kirigami.Theme.negativeTextColor | |
| 1172 | + font: Kirigami.Theme.smallFont | |
| 1173 | + text: root.lastError | |
| 1101 | 1174 | } |
| 1102 | 1175 | |
| 1103 | − Item { Layout.fillHeight: true } | |
| 1104 | − } | |
| 1176 | + Kirigami.Separator { Layout.fillWidth: true } | |
| 1105 | 1177 | |
| 1106 | − PlasmaComponents3.ScrollView { | |
| 1107 | − visible: root.hasValidConfig && root.hasWatched | |
| 1108 | − Layout.fillWidth: true | |
| 1109 | − Layout.fillHeight: true | |
| 1110 | − | |
| 1111 | − contentWidth: availableWidth | |
| 1112 | − | |
| 1113 | − ListView { | |
| 1114 | − id: runList | |
| 1115 | − clip: true | |
| 1116 | − model: runsModel | |
| 1117 | − reuseItems: false | |
| 1118 | − | |
| 1119 | − delegate: RunItem { | |
| 1120 | − required property string runJson | |
| 1121 | − width: runList.width | |
| 1122 | − store: root | |
| 1123 | − run: JSON.parse(runJson) | |
| 1124 | − showRepo: root.focusKey === "" || root.focusKey.indexOf("ns:") === 0 | |
| 1178 | + // Setup prompts | |
| 1179 | + ColumnLayout { | |
| 1180 | + visible: !root.hasValidConfig || !root.hasWatched | |
| 1181 | + Layout.fillWidth: true | |
| 1182 | + Layout.fillHeight: true | |
| 1183 | + spacing: Kirigami.Units.largeSpacing | |
| 1184 | + | |
| 1185 | + Item { Layout.fillHeight: true } | |
| 1186 | + | |
| 1187 | + Kirigami.Icon { | |
| 1188 | + source: root.logoSource | |
| 1189 | + isMask: true | |
| 1190 | + color: Kirigami.Theme.disabledTextColor | |
| 1191 | + Layout.alignment: Qt.AlignHCenter | |
| 1192 | + Layout.preferredWidth: Kirigami.Units.iconSizes.huge | |
| 1193 | + Layout.preferredHeight: Kirigami.Units.iconSizes.huge | |
| 1125 | 1194 | } |
| 1126 | 1195 | |
| 1127 | 1196 | PlasmaComponents3.Label { |
| 1128 | − anchors.centerIn: parent | |
| 1129 | − visible: runList.count === 0 && !root.loading | |
| 1130 | − opacity: 0.7 | |
| 1131 | − text: i18n("No runs yet") | |
| 1197 | + Layout.fillWidth: true | |
| 1198 | + horizontalAlignment: Text.AlignHCenter | |
| 1199 | + wrapMode: Text.WordWrap | |
| 1200 | + text: !root.hasValidConfig | |
| 1201 | + ? i18n("Configure the widget to connect to your GitGud instance") | |
| 1202 | + : i18n("Pick repositories or namespaces to watch") | |
| 1203 | + } | |
| 1204 | + | |
| 1205 | + PlasmaComponents3.Button { | |
| 1206 | + Layout.alignment: Qt.AlignHCenter | |
| 1207 | + icon.name: "configure" | |
| 1208 | + text: !root.hasValidConfig ? i18n("Configure Connection") : i18n("Choose Repositories") | |
| 1209 | + onClicked: Plasmoid.internalAction("configure").trigger() | |
| 1210 | + } | |
| 1211 | + | |
| 1212 | + Item { Layout.fillHeight: true } | |
| 1213 | + } | |
| 1214 | + | |
| 1215 | + PlasmaComponents3.ScrollView { | |
| 1216 | + visible: root.hasValidConfig && root.hasWatched | |
| 1217 | + Layout.fillWidth: true | |
| 1218 | + Layout.fillHeight: true | |
| 1219 | + | |
| 1220 | + contentWidth: availableWidth | |
| 1221 | + | |
| 1222 | + ListView { | |
| 1223 | + id: runList | |
| 1224 | + clip: true | |
| 1225 | + model: runsModel | |
| 1226 | + reuseItems: false | |
| 1227 | + | |
| 1228 | + delegate: RunItem { | |
| 1229 | + required property string runJson | |
| 1230 | + width: runList.width | |
| 1231 | + store: root | |
| 1232 | + run: JSON.parse(runJson) | |
| 1233 | + showRepo: root.focusKey === "" || root.focusKey.indexOf("ns:") === 0 | |
| 1234 | + } | |
| 1235 | + | |
| 1236 | + PlasmaComponents3.Label { | |
| 1237 | + anchors.centerIn: parent | |
| 1238 | + visible: runList.count === 0 && !root.loading | |
| 1239 | + opacity: 0.7 | |
| 1240 | + text: i18n("No runs yet") | |
| 1241 | + } | |
| 1132 | 1242 | } |
| 1133 | 1243 | } |
| 1134 | 1244 | } |
modified
metadata.json
+1
−1
@@ -11,7 +11,7 @@
| 11 | 11 | "Icon": "run-build", |
| 12 | 12 | "Id": "org.neiam.kde.gitgud", |
| 13 | 13 | "Name": "GitGud CI", |
| 14 | − "Version": "0.4.0", | |
| 14 | + "Version": "0.5.0", | |
| 15 | 15 | "Website": "http://neiam.org" |
| 16 | 16 | }, |
| 17 | 17 | "KPackageStructure": "Plasma/Applet", |
Parents: 0fc3e53