-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathUpdateCard.qml
More file actions
520 lines (458 loc) · 21.9 KB
/
Copy pathUpdateCard.qml
File metadata and controls
520 lines (458 loc) · 21.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Widgets
// Rich update entry: logo, name, summary, version transition and a live
// progress bar during installation. Clicking the card asks the host to open
// the shared details popup (release notes, reviews, actions).
Rectangle {
id: card
required property var pkg // daemon package: {name, repo, fromVersion, toVersion}
property var info: null // enrichment: {name, summary, homepage, icon, releases}
property var itemState: null // engine: {status, fraction, detail}
property var store: null
property bool showUpdateButton: false
property bool engineBusy: false
property bool held: false
property string holdReason: ""
property bool isIgnored: false
property bool canHold: false
// Verbatim tool output behind the short failure reason, revealed on request
property string errorDetail: ""
// {type, severity, ids} from updateinfo, null when the update carries no
// advisory or the distro has none to give
property var advisory: null
readonly property bool isSecurity: advisory !== null && advisory.type === "security"
// updateinfo grades its security advisories, and the grade is the part
// that decides whether this waits until tonight. It was being fetched and
// dropped: every fix read "Security", from a moderate library bump to a
// critical hole in the browser you are reading this in.
readonly property string severityLabel: {
if (!isSecurity)
return "";
switch ((advisory.severity || "").toLowerCase()) {
case "critical":
return Tr.t("Critical");
case "important":
return Tr.t("Important");
case "moderate":
return Tr.t("Moderate");
case "low":
return Tr.t("Low");
}
return Tr.t("Security");
}
// Only the top two earn the full red. Below that it is still a security
// fix, but shouting about all of them is how nobody hears any of them.
readonly property bool isUrgent: isSecurity && ["critical", "important"].indexOf((advisory.severity || "").toLowerCase()) !== -1
signal updateRequested
signal holdToggleRequested
signal detailsRequested
readonly property string baseName: (pkg.name || "").replace(/\.(x86_64|i686|noarch|aarch64|armv7hl|ppc64le|s390x)$/, "")
readonly property bool isFlatpak: pkg.repo === "flatpak"
// Off under a heading that already names the source — see the list that
// decides it. The card still knows what it is; it just stops saying so
// twice within one screenful.
property bool showSource: true
// AppStream first, then whatever the row was built with: a formula from a
// tap, a plugin from its manifest and an AppImage all arrive with a name
// meant to be read, and the card was falling through to the raw one — so
// the list said dsd/test/dsd-demo while the popup one click away, which
// asks the store for the same thing, said dsd-demo.
readonly property string prettyName: (info && info.name) ? info.name : (pkg.displayName || baseName)
readonly property string summary: (info && info.summary) ? info.summary : ""
readonly property string homepage: (info && info.homepage) ? info.homepage : ""
// The packages the shell is made of have no AppStream entry and so no
// icon of their own, and the generic chip glyph told you nothing about
// what they are. The host hands over DMS's own logo for those.
property string shellIconPath: ""
readonly property string iconPath: (info && info.icon) ? info.icon : shellIconPath
readonly property var newReleases: {
if (!info || !info.releases)
return [];
const newer = info.releases.filter(r => r.newer && (r.notesHtml || r.version));
return newer.slice(0, 5);
}
readonly property string toVersionDisplay: {
if (pkg.toVersion)
return pkg.toVersion;
if (newReleases.length > 0 && newReleases[0].version)
return newReleases[0].version;
return "";
}
readonly property string status: itemState ? itemState.status : "pending"
radius: Theme.cornerRadius
color: Theme.surfaceContainerHigh
opacity: held ? 0.65 : 1
border.width: 1
border.color: status === "error" ? Theme.withAlpha(Theme.error, 0.5) : Theme.withAlpha(Theme.outline, 0.12)
clip: true
implicitHeight: contentColumn.implicitHeight + Theme.spacingM * 2
Behavior on implicitHeight {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
// Clicking the card opens the details popup (buttons sit on top)
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: card.detailsRequested()
}
ColumnLayout {
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingM
// ── Logo ────────────────────────────────────────────────────────
Rectangle {
Layout.preferredWidth: 44
Layout.preferredHeight: 44
Layout.alignment: Qt.AlignTop
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.primary, 0.08)
Image {
id: logoImage
anchors.fill: parent
anchors.margins: card.iconPath.endsWith(".svg") ? 6 : 4
source: card.iconPath ? "file://" + card.iconPath : ""
// Themed icons, tuned in TintedIconEffect
layer.enabled: Ui.tintAppIcons
layer.effect: TintedIconEffect {}
}
DankIcon {
anchors.centerIn: parent
visible: logoImage.status !== Image.Ready
// A plugin has no icon file to fail to load — it names a
// glyph in its manifest, and the generic one stands in
// when it names nothing
name: card.pkg.repo === "dmsplugin" ? (card.pkg.icon || "extension") : Ui.sourceIcon(card.pkg.repo)
size: 24
color: Theme.primary
}
}
// ── Name, summary, versions ─────────────────────────────────────
ColumnLayout {
Layout.fillWidth: true
spacing: 2
StyledText {
Layout.fillWidth: true
text: card.prettyName
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
elide: Text.ElideRight
}
StyledText {
Layout.fillWidth: true
visible: card.summary.length > 0
text: card.summary
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
maximumLineCount: 1
}
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingXS
// Also shown without version info when there is status text
// to carry (hold reason, run status)
visible: (card.pkg.fromVersion || card.toVersionDisplay) !== "" || (card.held && card.holdReason !== "") || card.itemState !== null
StyledText {
visible: (card.pkg.fromVersion || "") !== ""
text: card.pkg.fromVersion || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideMiddle
Layout.maximumWidth: 200
}
DankIcon {
visible: (card.pkg.fromVersion || "") !== "" && card.toVersionDisplay !== ""
name: "arrow_forward"
size: 12
color: Theme.surfaceVariantText
}
StyledText {
visible: card.toVersionDisplay !== ""
text: card.toVersionDisplay
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.primary
elide: Text.ElideMiddle
Layout.maximumWidth: 220
}
// Run status as text where an icon alone would be unclear
StyledText {
visible: card.itemState !== null && (card.status === "pending" || card.status === "done" || card.status === "error")
text: {
switch (card.status) {
case "pending":
return "· " + Tr.t("queued");
case "done":
return "· " + Tr.t("completed");
case "error":
return "· " + Tr.t("failed");
default:
return "";
}
}
font.pixelSize: Theme.fontSizeSmall
color: card.status === "error" ? Ui.failColor : (card.status === "done" ? Theme.success : Theme.surfaceVariantText)
}
// Hold reason, e.g. "versionlock (via freerdp)"
StyledText {
visible: card.held && card.holdReason !== ""
text: "· " + card.holdReason
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.warning
elide: Text.ElideRight
Layout.maximumWidth: 220
}
Item {
Layout.fillWidth: true
}
}
}
// ── Chips, actions and status ──────────────────────────────────
// One cluster packed against the right edge: the chips and the
// action slot share a row, so they line up with each other
// instead of drifting apart across the card's right side. The
// action slot keeps a fixed width whenever the card can show
// something there, so buttons line up down the list.
RowLayout {
Layout.alignment: Qt.AlignTop
spacing: Theme.spacingXS
Rectangle {
visible: card.held
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: heldChipRow.implicitWidth + 14
Layout.preferredHeight: 18
radius: 9
color: Theme.withAlpha(Theme.warning, 0.18)
RowLayout {
id: heldChipRow
anchors.centerIn: parent
spacing: 3
DankIcon {
name: "lock"
size: 11
color: Theme.warning
}
StyledText {
text: Tr.t("Held")
font.pixelSize: Theme.fontSizeSmall - 2
font.weight: Font.Medium
color: Theme.warning
}
}
}
// Security first: the one distinction that changes whether
// this can wait until tonight
Rectangle {
visible: card.isSecurity
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: securityChipText.implicitWidth + 14
Layout.preferredHeight: 18
radius: 9
color: card.isUrgent ? Theme.withAlpha(Theme.error, 0.18) : Theme.withAlpha(Theme.warning, 0.18)
StyledText {
id: securityChipText
anchors.centerIn: parent
text: card.severityLabel
font.pixelSize: Theme.fontSizeSmall - 2
font.weight: Font.Medium
color: card.isUrgent ? Ui.failColor : Theme.warning
}
}
Rectangle {
visible: card.showSource
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: repoChipText.implicitWidth + 14
Layout.preferredHeight: 18
radius: 9
color: card.isFlatpak ? Theme.withAlpha(Theme.tertiary, 0.15) : Theme.withAlpha(Theme.secondary, 0.15)
StyledText {
id: repoChipText
anchors.centerIn: parent
// "System" is what a package is; a plugin, an
// AppImage, a firmware blob and a brew formula are
// not, and calling them that was the chip claiming
// the wrong thing about where they came from
text: Ui.sourceLabel(card.pkg.repo)
font.pixelSize: Theme.fontSizeSmall - 2
font.weight: Font.Medium
color: card.isFlatpak ? Theme.tertiary : Theme.secondary
}
}
Item {
// Collapses on cards that have neither a button nor a run
// status, so the chips stay flush against the edge
readonly property bool reserved: card.showUpdateButton || card.itemState !== null
Layout.alignment: Qt.AlignVCenter
Layout.preferredWidth: reserved ? 28 : 0
Layout.preferredHeight: 28
DankActionButton {
anchors.centerIn: parent
visible: card.showUpdateButton && card.status === "pending" && !card.engineBusy && !card.isIgnored
buttonSize: 28
iconName: "download"
iconSize: 17
iconColor: Theme.primary
tooltipText: Tr.t("Update only this app")
onClicked: card.updateRequested()
}
DankIcon {
anchors.centerIn: parent
visible: card.status === "done"
name: "check_circle"
size: 20
color: Theme.success
}
DankIcon {
anchors.centerIn: parent
visible: card.status === "error"
name: "error"
size: 20
color: Theme.error
}
}
}
}
// ── Live progress ───────────────────────────────────────────────────
ColumnLayout {
Layout.fillWidth: true
visible: card.status === "active"
spacing: 2
RowLayout {
Layout.fillWidth: true
StyledText {
Layout.fillWidth: true
text: card.itemState ? card.itemState.detail : ""
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
elide: Text.ElideRight
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 5
radius: 2.5
color: Theme.withAlpha(Theme.surfaceVariant, 0.4)
Rectangle {
id: progressFill
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
radius: 2.5
color: Theme.primary
width: parent.width * (card.itemState ? Math.max(0.02, card.itemState.fraction) : 0)
// The animation is for a bar that grows while you watch
// it. A delegate scrolled back into view is not that: it
// is created at the width it already had, and animating
// to it replays a download that finished minutes ago —
// every card, every time the list moves.
//
// So it is off until the row has drawn once. Qt.callLater
// rather than Component.onCompleted, which still runs
// inside the frame that sets the initial width.
property bool _grown: false
Component.onCompleted: Qt.callLater(() => _grown = true)
Behavior on width {
enabled: progressFill._grown
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
}
}
}
// ── A hold sitting on top of a security fix ─────────────────────────
// Holding a package back is a legitimate thing this app encourages,
// right up until the update being held is the one that closes a hole.
// Nothing said so: the "Held" chip and the "Security" chip sat side by
// side on the same card and left the reader to draw the conclusion.
RowLayout {
Layout.fillWidth: true
visible: card.held && card.isSecurity
spacing: Theme.spacingXS
DankIcon {
Layout.alignment: Qt.AlignTop
name: "warning"
size: 14
color: card.isUrgent ? Ui.failColor : Theme.warning
}
StyledText {
Layout.fillWidth: true
text: card.isUrgent ? Tr.t("You are holding back a security fix rated %1.").arg(card.severityLabel.toLowerCase()) : Tr.t("You are holding back a security fix.")
font.pixelSize: Theme.fontSizeSmall - 1
color: card.isUrgent ? Ui.failColor : Theme.warning
wrapMode: Text.WordWrap
}
}
// ── Error detail ────────────────────────────────────────────────────
// The short reason is what the card says; the tool's own words are
// one click away rather than on display, because they are long,
// untranslated and only interesting when reporting a problem.
ColumnLayout {
Layout.fillWidth: true
visible: card.status === "error" && card.itemState && (card.itemState.detail || "") !== ""
spacing: 2
RowLayout {
Layout.fillWidth: true
spacing: Theme.spacingXS
StyledText {
Layout.fillWidth: true
text: card.itemState ? card.itemState.detail : ""
font.pixelSize: Theme.fontSizeSmall - 1
color: Ui.failColor
wrapMode: Text.WordWrap
maximumLineCount: 3
elide: Text.ElideRight
}
StyledText {
id: errorToggle
property bool expanded: false
visible: card.errorDetail !== ""
text: expanded ? Tr.t("Hide details") : Tr.t("Show details")
font.pixelSize: Theme.fontSizeSmall - 1
font.underline: toggleArea.containsMouse
color: Ui.failColor
MouseArea {
id: toggleArea
anchors.fill: parent
anchors.margins: -4
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: errorToggle.expanded = !errorToggle.expanded
}
}
}
Rectangle {
Layout.fillWidth: true
visible: errorToggle.expanded && card.errorDetail !== ""
implicitHeight: rawErrorText.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadius / 2
color: Theme.withAlpha(Theme.surfaceVariant, 0.5)
SelectableText {
id: rawErrorText
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.margins: Theme.spacingS
text: card.errorDetail
font.family: Theme.monoFontFamily
font.pixelSize: Theme.fontSizeSmall - 1
color: Theme.surfaceVariantText
wrapMode: Text.Wrap
}
}
}
}
}