-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathService.qml
More file actions
348 lines (301 loc) · 13.7 KB
/
Copy pathService.qml
File metadata and controls
348 lines (301 loc) · 13.7 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
import QtQuick
import Quickshell
import Quickshell.Io
// GitLab dashboard data service. The helper owns API pagination and aggregation;
// this item schedules it and exposes one stable, defensive model to the panel.
Item {
id: root
property var settings: ({
})
property bool loading: false
property string state: "loading"
property string message: "Loading GitLab…"
property string login: ""
property string instanceHostname: configuredHostname()
property string webBaseUrl: configuredWebBaseUrl()
property string fetchedRepositoryScope: "owned"
property string fetchedAt: ""
property var notifications: []
property int notificationsRevision: 0
property var reviewRequests: []
property var assignedIssues: []
property var myPullRequests: []
property int myPullRequestsTotal: 0
property var actions: []
property var failedActions: []
property var repositories: []
property var warnings: []
property var rateLimit: null
property string _stdout: ""
property string _stderr: ""
property bool refreshQueued: false
property string markingNotificationId: ""
property bool markingAllNotifications: false
property string notificationActionStatus: ""
property string _markStdout: ""
property string _markStderr: ""
// Single-thread and bulk marking share one process, so the panel gates every
// entry point on this rather than on whichever flag a given call happens to
// set. A caller added later inherits the guard instead of having to know.
readonly property bool marking: markProcess.running
readonly property int refreshIntervalSec: intSetting("refreshIntervalSec", 900, 60, 3600)
readonly property int unreadCount: notifications.length
readonly property int actionCount: actions.length
// A broken check on your own pull request is the kind of thing the bar icon
// exists to surface, so it counts toward the alarming state. Drafts are
// excluded: a red check on work you have not offered up yet is expected,
// and it would leave the icon permanently lit.
readonly property int failingPullRequestCount: myPullRequests.filter(function(item) {
return !item.draft && root.isBrokenCheck(item.checks);
}).length
readonly property bool iconAlwaysUnlit: boolSetting("iconAlwaysUnlit", false)
// Unknown values keep the safer web-app default rather than silently
// switching an existing setup back to a browser tab.
readonly property string linkBehavior: String(setting("linkBehavior", "Web app window")).toLowerCase() === "browser tab" ? "Browser tab" : "Web app window"
readonly property bool alarming: !iconAlwaysUnlit && (unreadCount > 0 || actionCount > 0 || reviewRequests.length > 0 || failingPullRequestCount > 0)
// StatusCheckRollup groupings live here so the alarming count, the row label
// and the row glyph cannot drift apart when a state is reclassified.
function isBrokenCheck(checks) {
var value = String(checks || "");
return value === "FAILURE" || value === "ERROR";
}
function isRunningCheck(checks) {
var value = String(checks || "");
return value === "PENDING" || value === "EXPECTED";
}
function setting(name, fallback) {
var value = settings ? settings[name] : undefined;
return value === undefined || value === null ? fallback : value;
}
function configuredHostname() {
var value = String(setting("hostname", "gitlab.com")).trim();
value = value.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
value = value.replace(/\/+$/, "");
var slash = value.indexOf("/");
if (slash >= 0)
value = value.substring(0, slash);
return value || "gitlab.com";
}
function configuredWebBaseUrl() {
var value = String(setting("hostname", "gitlab.com")).trim();
var scheme = /^http:\/\//i.test(value) ? "http" : "https";
return scheme + "://" + configuredHostname();
}
function intSetting(name, fallback, minimum, maximum) {
var value = parseInt(String(setting(name, fallback)), 10);
if (!isFinite(value))
value = fallback;
return Math.max(minimum, Math.min(maximum, value));
}
function boolSetting(name, fallback) {
var value = setting(name, fallback);
if (value === true || value === false)
return value;
var text = String(value).toLowerCase();
return text === "true" || text === "yes" || text === "on" || text === "1";
}
// Matched against the known options rather than by substring, so an option
// added later falls back to the narrower scope instead of silently widening
// it. `fetchedRepositoryScope` reports what the last payload contained.
function repositoryMode() {
return String(setting("repositoryScope", "Owned")).toLowerCase() === "owned and groups" ? "membership" : "owned";
}
function actionMode() {
var value = String(setting("actionScanBehavior", "Recent repositories")).toLowerCase();
if (value === "off")
return "off";
if (value === "all repositories")
return "all";
return "recent";
}
function helperPath() {
return Qt.resolvedUrl("omarchy-gitlab-fetch").toString().replace(/^file:\/\//, "");
}
function command() {
return [helperPath(), "--hostname", String(setting("hostname", "gitlab.com")).trim(), "--include-archived", boolSetting("includeArchived", false) ? "true" : "false", "--include-forks", boolSetting("includeForks", false) ? "true" : "false", "--project-scope", repositoryMode(), "--include-archived-reviews", boolSetting("includeArchivedReviewRequests", false) ? "true" : "false", "--include-draft-reviews", boolSetting("includeDraftReviewRequests", false) ? "true" : "false", "--action-scan", actionMode(), "--action-repo-limit", String(intSetting("actionScanRepoLimit", 15, 5, 200)), "--concurrency", String(intSetting("actionScanConcurrency", 6, 1, 12)), "--failed-days", String(intSetting("failedActionDays", 7, 1, 30)), "--failed-limit", String(intSetting("failedActionLimit", 20, 1, 100))];
}
function refresh() {
if (fetchProcess.running || markProcess.running) {
refreshQueued = true;
return ;
}
refreshQueued = false;
loading = true;
_stdout = "";
_stderr = "";
fetchProcess.command = command();
fetchProcess.running = true;
}
function apply(raw) {
try {
var data = JSON.parse(String(raw || ""));
state = String(data.state || "error");
message = String(data.message || "");
login = String(data.login || "");
instanceHostname = String(data.hostname || configuredHostname());
webBaseUrl = String(data.webBaseUrl || configuredWebBaseUrl());
fetchedRepositoryScope = String(data.repositoryScope || "owned");
fetchedAt = String(data.fetchedAt || "");
notifications = Array.isArray(data.notifications) ? data.notifications : [];
notificationsRevision++;
reviewRequests = Array.isArray(data.reviewRequests) ? data.reviewRequests : [];
assignedIssues = Array.isArray(data.assignedIssues) ? data.assignedIssues : [];
myPullRequests = Array.isArray(data.myPullRequests) ? data.myPullRequests : [];
myPullRequestsTotal = Number(data.myPullRequestsTotal) || myPullRequests.length;
actions = Array.isArray(data.actions) ? data.actions : [];
failedActions = Array.isArray(data.failedActions) ? data.failedActions : [];
repositories = Array.isArray(data.repositories) ? data.repositories : [];
warnings = Array.isArray(data.warnings) ? data.warnings : [];
rateLimit = data.rateLimit || null;
} catch (error) {
state = "error";
message = "GitLab returned an unreadable response.";
warnings = [String(error)];
}
}
function markNotificationRead(id) {
var value = String(id || "");
if (value === "" || loading || fetchProcess.running || markProcess.running)
return ;
actionStatusTimer.stop();
markingNotificationId = value;
notificationActionStatus = "Marking todo done…";
_markStdout = "";
_markStderr = "";
markProcess.command = [helperPath(), "--mark-todo-done", value];
markProcess.running = true;
}
// Capture the exact on-screen todo IDs on the first click. The panel passes
// the rendered row count (5 collapsed / 25 expanded), so the snapshot is
// bounded to what is actually shown. The panel binds confirmation to
// notificationsRevision, so any refresh invalidates this prepared value
// before the destructive second click can run.
function prepareMarkAllNotificationsRead(limit) {
if (notifications.length === 0 || loading || fetchProcess.running || markProcess.running)
return "";
var count = parseInt(String(limit), 10);
if (!isFinite(count) || count <= 0)
count = notifications.length;
count = Math.min(count, notifications.length);
var ids = [];
for (var i = 0; i < count; i++) {
var id = String(notifications[i].id || "");
if (!/^\d+$/.test(id)) {
notificationActionStatus = "Refresh before marking everything done.";
actionStatusTimer.restart();
return "";
}
ids.push(id);
}
return JSON.stringify({ids: ids, limit: count, revision: notificationsRevision});
}
function markAllNotificationsRead(prepared) {
var confirmed = String(prepared || "");
if (confirmed === "" || loading || fetchProcess.running || markProcess.running)
return ;
var snapshot;
try {
snapshot = JSON.parse(confirmed);
} catch (error) {
notificationActionStatus = "Refresh before marking everything done.";
actionStatusTimer.restart();
return ;
}
// Recompute immediately before starting, using the same display bound.
// This protects non-panel callers as well as the panel's revision-bound
// confirmation.
if (confirmed !== prepareMarkAllNotificationsRead(snapshot.limit)) {
notificationActionStatus = "Todos changed. Confirm again.";
actionStatusTimer.restart();
return ;
}
actionStatusTimer.stop();
markingAllNotifications = true;
notificationActionStatus = "Marking all todos done…";
_markStdout = "";
_markStderr = "";
var commandLine = [helperPath(), "--mark-todos-done", String((snapshot.ids || []).join(","))];
markProcess.command = commandLine;
markProcess.running = true;
}
visible: false
Timer {
interval: root.refreshIntervalSec * 1000
repeat: true
running: true
triggeredOnStart: true
onTriggered: root.refresh()
}
Timer {
id: actionStatusTimer
interval: 3000
repeat: false
onTriggered: root.notificationActionStatus = ""
}
Process {
id: fetchProcess
running: false
command: []
onExited: function(exitCode) {
root.loading = false;
var stdout = String(output.text || root._stdout || "");
var stderr = String(errors.text || root._stderr || "").trim();
if (stdout.trim() !== "") {
root.apply(stdout);
} else {
root.state = "error";
root.message = stderr !== "" ? stderr : "GitLab data refresh failed.";
}
if (root.refreshQueued) {
root.refreshQueued = false;
Qt.callLater(root.refresh);
}
}
stdout: StdioCollector {
id: output
waitForEnd: true
onStreamFinished: root._stdout = text
}
stderr: StdioCollector {
id: errors
waitForEnd: true
onStreamFinished: root._stderr = text
}
}
Process {
id: markProcess
running: false
command: []
onExited: function(exitCode) {
var response = null;
try {
response = JSON.parse(String(markOutput.text || root._markStdout || ""));
} catch (error) {
}
var all = root.markingAllNotifications;
if (exitCode === 0 && response && response.state === "ready") {
root.notificationActionStatus = all ? "Todos marked done. Refreshing…" : "Todo marked done. Refreshing…";
} else {
var fallback = all ? "Could not mark all todos done." : "Could not mark todo done.";
root.notificationActionStatus = response && response.message ? String(response.message) : String(markErrors.text || root._markStderr || fallback).trim();
}
root.markingNotificationId = "";
root.markingAllNotifications = false;
actionStatusTimer.restart();
// GitLab is authoritative after every attempt. This reconciles
// successful, failed, and partially completed bulk operations.
root.refreshQueued = false;
Qt.callLater(root.refresh);
}
stdout: StdioCollector {
id: markOutput
waitForEnd: true
onStreamFinished: root._markStdout = text
}
stderr: StdioCollector {
id: markErrors
waitForEnd: true
onStreamFinished: root._markStderr = text
}
}
}