import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.plasma.components as PlasmaComponents3
import org.kde.kirigami as Kirigami
import "Status.js" as Status
// The live CI wall: every job running right now, a card each, tiled to
// fill the space — GitGud's /users/ci/live, as a window of its own or as
// the widget itself on the desktop (wall mode).
//
// Up to six cards side by side (fewer when they'd get too narrow to read
// a log line in); past that the columns fill up from the right in pairs,
// as on the web and in the Android app. The order is the operator's:
// oldest-started first, or newest (the header's buttons, or R). So is
// what it shows: your own jobs, or one organization's.
//
// All state lives in `store` (main.qml): it polls /ci/live while any
// wall is on screen, so a window and a desktop wall share one feed.
Item {
id: wall
required property var store
// Leave the header's window-only buttons out on the desktop.
property bool inWindow: false
property bool fullScreen: false
property bool translucent: false
signal toggleFullScreen()
readonly property var cards: store.wallCards
readonly property var ordered: cards ? (store.wallNewestFirst ? cards.slice().reverse() : cards) : null
// One column per 13 grid units of width, at most six; more (up to six)
// when a column would stack more cards than fit at a readable height
// — a small desktop widget would otherwise draw slivers.
readonly property int widthCols: Math.max(1, Math.min(6, Math.floor(grid.width / (Kirigami.Units.gridUnit * 13))))
readonly property int rowsFit: Math.max(1, Math.floor(grid.height / (Kirigami.Units.gridUnit * 3.5)))
readonly property int maxCols: cards ? Math.min(6, Math.max(widthCols, Math.ceil(cards.length / rowsFit))) : widthCols
readonly property var columns: ordered ? Status.wallSplit(ordered, maxCols) : []
// A small desktop widget: the header keeps only what it must.
readonly property bool narrow: width < Kirigami.Units.gridUnit * 34
// Tint: every bit of text — and the clock, borders and shimmer that
// share its colours — in one hue (store.wallTintHue, green unless
// set), a terminal look that suits the translucent window. Done by
// overriding the theme's text colours on the layout below, which
// every Label, icon and card under it inherits. Status colours —
// a failed step, "offline" — keep their meaning and aren't touched.
//
// Lightness comes from the theme's background, not its text colour:
// reading the text colour the layout itself overrides would be a
// loop. A dark theme gets a bright tint, a light one a deep one.
readonly property bool tinted: store.wallTint
readonly property real tintHue: store.wallTintHue / 360
readonly property bool darkTheme: Kirigami.Theme.backgroundColor.hslLightness < 0.5
// The accent (clock, live dot, borders) is the most saturated, so it
// still stands out from the body text the way the neutral colour does.
readonly property color tintText: Qt.hsla(tintHue, 0.70, darkTheme ? 0.62 : 0.30, 1)
readonly property color tintDim: Qt.hsla(tintHue, 0.40, darkTheme ? 0.45 : 0.50, 1)
readonly property color tintAccent: Qt.hsla(tintHue, 1.0, darkTheme ? 0.55 : 0.28, 1)
Component.onCompleted: store.wallViewers += 1
Component.onDestruction: store.wallViewers -= 1
ColumnLayout {
Kirigami.Theme.textColor: wall.tinted ? wall.tintText : wall.Kirigami.Theme.textColor
Kirigami.Theme.disabledTextColor: wall.tinted ? wall.tintDim : wall.Kirigami.Theme.disabledTextColor
Kirigami.Theme.neutralTextColor: wall.tinted ? wall.tintAccent : wall.Kirigami.Theme.neutralTextColor
anchors.fill: parent
anchors.margins: Kirigami.Units.smallSpacing
spacing: Kirigami.Units.smallSpacing
// ── header ──────────────────────────────────────────────────────
RowLayout {
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
Rectangle {
id: liveDot
Layout.preferredWidth: Kirigami.Units.smallSpacing * 2.5
Layout.preferredHeight: width
radius: width / 2
color: wall.store.wallError === "" ? Kirigami.Theme.neutralTextColor
: Kirigami.Theme.negativeTextColor
SequentialAnimation on opacity {
loops: Animation.Infinite
NumberAnimation { from: 1; to: 0.3; duration: 900 }
NumberAnimation { from: 0.3; to: 1; duration: 900 }
}
}
Kirigami.Heading {
level: 3
text: i18n("Live CI")
}
PlasmaComponents3.ComboBox {
id: scopeBox
// "" is the token owner's own wall; otherwise an org handle.
readonly property var scopes: [""].concat(wall.store.wallOrgs.map(function(o) { return o.handle }))
model: scopes.map(function(s) { return s === "" ? i18n("Mine") : "@" + s })
currentIndex: Math.max(0, scopes.indexOf(wall.store.wallScope))
onActivated: function(index) { wall.store.setWallScope(scopes[index]) }
PlasmaComponents3.ToolTip { text: i18n("Whose running jobs to show") }
}
PlasmaComponents3.Label {
visible: wall.cards !== null && !wall.narrow
opacity: 0.7
text: wall.cards ? i18np("1 job", "%1 jobs", wall.cards.length) : ""
}
// The tiling at a glance: a bar per column, split by its cards.
Row {
visible: wall.columns.length > 0 && !wall.narrow
spacing: 2
Repeater {
model: wall.columns
Column {
id: glyphColumn
required property var modelData
readonly property int cards: modelData.length
spacing: 1
Repeater {
model: glyphColumn.cards
Rectangle {
width: 5
height: (14 - (glyphColumn.cards - 1)) / glyphColumn.cards
radius: 1
color: Kirigami.Theme.disabledTextColor
}
}
}
}
}
PlasmaComponents3.Label {
visible: wall.store.wallError !== "" && wall.cards !== null
color: Kirigami.Theme.negativeTextColor
font: Kirigami.Theme.smallFont
text: i18n("offline · retrying")
PlasmaComponents3.ToolTip { text: wall.store.wallError }
}
Item { Layout.fillWidth: true }
QQC2.ButtonGroup { id: orderGroup }
PlasmaComponents3.ToolButton {
checkable: true
checked: !wall.store.wallNewestFirst
QQC2.ButtonGroup.group: orderGroup
icon.name: "view-sort-ascending"
text: i18n("Oldest first")
display: wall.narrow ? QQC2.AbstractButton.IconOnly : QQC2.AbstractButton.TextOnly
onClicked: wall.store.setWallNewestFirst(false)
PlasmaComponents3.ToolTip { text: i18n("Oldest first (R flips the order)") }
}
PlasmaComponents3.ToolButton {
checkable: true
checked: wall.store.wallNewestFirst
QQC2.ButtonGroup.group: orderGroup
icon.name: "view-sort-descending"
text: i18n("Newest first")
display: wall.narrow ? QQC2.AbstractButton.IconOnly : QQC2.AbstractButton.TextOnly
onClicked: wall.store.setWallNewestFirst(true)
PlasmaComponents3.ToolTip { text: i18n("Newest first (R flips the order)") }
}
PlasmaComponents3.ToolButton {
visible: wall.inWindow
checkable: true
checked: wall.translucent
icon.name: "transform-opacity"
text: i18n("Translucent")
display: QQC2.AbstractButton.IconOnly
onClicked: wall.store.setWallTranslucent(!wall.translucent)
PlasmaComponents3.ToolTip { text: i18n("See-through background (T)") }
}
PlasmaComponents3.ToolButton {
checkable: true
checked: wall.tinted
icon.name: "fill-color"
text: i18n("Tint")
display: QQC2.AbstractButton.IconOnly
onClicked: wall.store.setWallTint(!wall.tinted)
PlasmaComponents3.ToolTip {
text: wall.inWindow ? i18n("All text in one hue (H) — pick the hue in the widget's settings")
: i18n("All text in one hue — pick the hue in the widget's settings")
}
}
PlasmaComponents3.ToolButton {
visible: wall.inWindow
icon.name: wall.fullScreen ? "view-restore" : "view-fullscreen"
text: wall.fullScreen ? i18n("Exit Full Screen") : i18n("Full Screen")
display: QQC2.AbstractButton.IconOnly
onClicked: wall.toggleFullScreen()
PlasmaComponents3.ToolTip { text: wall.fullScreen ? i18n("Exit Full Screen (Esc)") : i18n("Full Screen (F11)") }
}
PlasmaComponents3.ToolButton {
icon.name: "internet-web-browser"
text: i18n("Open in Browser")
display: QQC2.AbstractButton.IconOnly
onClicked: Qt.openUrlExternally(wall.store.wallWebUrl())
PlasmaComponents3.ToolTip { text: i18n("Open in Browser") }
}
}
// ── cards ───────────────────────────────────────────────────────
Item {
id: grid
Layout.fillWidth: true
Layout.fillHeight: true
PlasmaComponents3.Label {
anchors.centerIn: parent
width: parent.width - Kirigami.Units.gridUnit * 2
visible: wall.ordered === null || wall.ordered.length === 0
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
opacity: 0.7
text: !wall.store.wallConfigured
? i18n("Configure the widget to connect to your GitGud instance")
: wall.ordered === null
? (wall.store.wallError !== "" ? wall.store.wallError : i18n("Loading…"))
: i18n("Nothing running. Jobs appear here the moment a runner picks them up.")
}
RowLayout {
anchors.fill: parent
spacing: Kirigami.Units.smallSpacing
Repeater {
model: wall.columns
ColumnLayout {
id: column
required property var modelData
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredWidth: 1 // equal shares
spacing: Kirigami.Units.smallSpacing
Repeater {
model: column.modelData
WallCard {
required property var modelData
Layout.fillWidth: true
Layout.fillHeight: true
Layout.preferredHeight: 1 // equal shares
card: modelData
translucent: wall.translucent
// The theme's own, from above the tint:
// a running step stays told apart from
// a passed one whatever the hue.
runningColor: wall.Kirigami.Theme.neutralTextColor
now: wall.store.now
}
}
}
}
}
}
}
}
neiam
/gitgud-plasmoid
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
12.5 KiB · text
History
e49521d