Skip to content
603 linesCodeBlameRaw
1// The settings window's work: show what the app has, and keep each change
2// the moment it is made. Everything it asks is a command in
3// src/bridge.rs, answered only to this window.
4(function () {
5 var SWITCHES = ["startHidden", "closeToTray", "notifications", "flash", "badge", "systemTitleBar", "autoUpdate", "runner"];
6 // What each way of running jobs can reach, said plainly (src/runner/backend.rs).
7 var BACKENDS = {
8 sandboxed: { text: "The runner and every job run in a sandbox of their own. Nothing a job does reaches this computer.", warn: false },
9 docker: {
10 text: "Each job runs in a container of its own, but the runner controls this computer's Docker, which can reach your files. Not a sandbox.",
11 warn: true,
12 },
13 host: {
14 text: "Jobs run directly on this computer, as you: they can read, change and delete anything your account can. For jobs that need this system itself, such as building a Windows installer.",
15 warn: true,
16 },
17 };
18 var current = null;
19
20 function byId(id) {
21 return document.getElementById(id);
22 }
23
24 function invoke(command, args) {
25 var core = window.__TAURI__ && window.__TAURI__.core;
26 if (core && typeof core.invoke === "function") return core.invoke(command, args || {});
27 return Promise.reject(new Error("This page belongs inside the g1t desktop app."));
28 }
29
30 function fail(error) {
31 var box = byId("error");
32 box.textContent = typeof error === "string" ? error : (error && error.message) || "That could not be changed.";
33 box.hidden = false;
34 }
35
36 function hostOf(server) {
37 try {
38 return new URL(server).host;
39 } catch (error) {
40 return server;
41 }
42 }
43
44 // What changes between Windows, macOS and Linux.
45 function platform(name) {
46 byId("flash-label").textContent =
47 name === "macos" ? "Bounce the dock icon" : name === "windows" ? "Flash the taskbar icon" : "Ask for attention";
48 // macOS keeps its menu bar at the top of the screen, whichever title bar the window has.
49 if (name === "macos") byId("system-title-bar-note").textContent = "In place of g1t's own, which sits under the window's buttons.";
50 if (name === "macos") byId("zoom-note").textContent = "The View menu changes it too: ⌘+, ⌘− and ⌘0.";
51 else byId("zoom-note").textContent = "The View menu changes it too: Ctrl+=, Ctrl+− and Ctrl+0.";
52 }
53
54 function render(view) {
55 current = view;
56 byId("error").hidden = true;
57 byId("openAtLogin").checked = !!view.openAtLogin;
58 SWITCHES.forEach(function (key) {
59 byId(key).checked = !!view[key];
60 });
61 // Starting in the tray only means something when g1t starts at sign-in.
62 byId("startHidden").disabled = !view.openAtLogin;
63 byId("startHidden").closest(".setting").classList.toggle("off", !view.openAtLogin);
64 // The title bar changes when g1t restarts.
65 byId("restart-row").hidden = !view.titleBarChanged;
66 var zoom = byId("zoom");
67 zoom.value = String(view.zoom);
68 if (zoom.value === "") zoom.value = "1";
69 byId("version").textContent = "g1t " + view.version;
70 if (!checking) updateStatus(view);
71 byId("server").textContent = hostOf(view.server);
72 renderRunner(view.runnerView);
73 }
74
75 function settingsOf() {
76 var settings = {};
77 SWITCHES.forEach(function (key) {
78 settings[key] = byId(key).checked;
79 });
80 settings.zoom = Number(byId("zoom").value) || 1;
81 settings.runnerBackend = byId("runnerBackend").value || "sandboxed";
82 settings.runnerWorkspace = byId("runnerWorkspace").value || null;
83 // Which kinds of job are on is changed by their cards' Start and Stop, and kept as it is here.
84 if (current && current.runnerCapabilities) settings.runnerCapabilities = current.runnerCapabilities;
85 return settings;
86 }
87
88 // ── The runner ──────────────────────────────────────────────────────
89 // Its diagnosis, from src/runner/diagnosis.rs, as the tray and the
90 // site's Runners page show it; its buttons are the fixes.
91 function option(value, text) {
92 var item = document.createElement("option");
93 item.value = value;
94 item.textContent = text;
95 return item;
96 }
97
98 // A fix's button. Choosing is done here, in this window; a copy says it was copied.
99 function actionButton(action, primary) {
100 var button = document.createElement("button");
101 button.type = "button";
102 button.textContent = action.label;
103 if (primary) button.className = "primary";
104 button.addEventListener("click", function () {
105 if (action.id === "choose_workspace") return byId("runnerWorkspace").focus();
106 if (action.id === "choose_backend") return byId("runnerBackend").focus();
107 invoke("runner_control", { action: action.id }).then(function () {
108 if (action.id.indexOf("copy_") !== 0) return;
109 button.textContent = "Copied";
110 setTimeout(function () {
111 button.textContent = action.label;
112 }, 2000);
113 }, fail);
114 });
115 return button;
116 }
117
118 function element(tag, className, text) {
119 var item = document.createElement(tag);
120 if (className) item.className = className;
121 if (text != null) item.textContent = text;
122 return item;
123 }
124
125 // The catalogue's sentences name commands in backticks: `wsl --install`.
126 // Said here as code, which can be selected and copied.
127 function prose(into, text) {
128 into.textContent = "";
129 String(text || "")
130 .split("`")
131 .forEach(function (part, at) {
132 if (!part) return;
133 into.appendChild(at % 2 ? element("code", null, part) : document.createTextNode(part));
134 });
135 return into;
136 }
137
138 // ── Each kind of job, a card of its own ───────────────────────────────
139 // Linux jobs and Windows jobs (macOS jobs once they are released): what
140 // each is doing, why one isn't running and its fix, and its own Start or
141 // Stop, which leaves the other as it is. The words are the app's
142 // (CapabilityLine in src/runner/diagnosis.rs); the card only lays them out.
143 var SVG = "http://www.w3.org/2000/svg";
144 // Plain line icons, 16px on a 24px grid: a terminal for Linux jobs, a
145 // window for Windows jobs, a laptop for macOS jobs.
146 var ICONS = {
147 linux: ["M4 5h16a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z", "M7 10l3 2.5L7 15", "M12.5 15h4"],
148 windows: ["M4 4h16a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1z", "M3 9h18", "M6.5 6.5h.01", "M9 6.5h.01"],
149 macos: ["M5 6a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v9H5z", "M2.5 18.5h19", "M3.5 18.5L5 15h14l1.5 3.5"],
150 };
151 // How each runner keeps a job from this computer, said plainly.
152 var ISOLATION = { "nested-container": "A fresh container in the VM", vm: "A VM of its own", container: "A container of its own", host: "None" };
153 var BUSY = { busy: true, starting: true, downloading: true, updating: true };
154 var cards = {};
155 var pending = {};
156
157 // A small arrow: the link goes out to g1t.
158 function outward() {
159 var svg = document.createElementNS(SVG, "svg");
160 svg.setAttribute("viewBox", "0 0 24 24");
161 svg.setAttribute("width", "11");
162 svg.setAttribute("height", "11");
163 svg.setAttribute("fill", "none");
164 svg.setAttribute("stroke", "currentColor");
165 svg.setAttribute("stroke-width", "2");
166 svg.setAttribute("stroke-linecap", "round");
167 svg.setAttribute("aria-hidden", "true");
168 var path = document.createElementNS(SVG, "path");
169 path.setAttribute("d", "M7 17L17 7M9 7h8v8");
170 svg.appendChild(path);
171 return svg;
172 }
173
174 function icon(id) {
175 var svg = document.createElementNS(SVG, "svg");
176 svg.setAttribute("viewBox", "0 0 24 24");
177 svg.setAttribute("width", "16");
178 svg.setAttribute("height", "16");
179 svg.setAttribute("fill", "none");
180 svg.setAttribute("stroke", "currentColor");
181 svg.setAttribute("stroke-width", "1.75");
182 svg.setAttribute("stroke-linecap", "round");
183 svg.setAttribute("stroke-linejoin", "round");
184 svg.setAttribute("aria-hidden", "true");
185 (ICONS[id] || ICONS.linux).forEach(function (d) {
186 var path = document.createElementNS(SVG, "path");
187 path.setAttribute("d", d);
188 svg.appendChild(path);
189 });
190 return svg;
191 }
192
193 // A card's parts, made once and filled in on every status.
194 function makeCard(kind) {
195 var card = element("article", "kind");
196 card.setAttribute("role", "listitem");
197 card.setAttribute("aria-labelledby", "kind-" + kind.id + "-title");
198 var head = element("header", "kind-head");
199 var badge = element("span", "kind-icon");
200 badge.appendChild(icon(kind.id));
201 var name = element("span", "kind-name");
202 var title = element("h3", null, kind.label);
203 title.id = "kind-" + kind.id + "-title";
204 name.appendChild(title);
205 var pill = element("span", "pill");
206 // Its state is said when it changes, as "Linux jobs: Waiting for jobs".
207 pill.setAttribute("role", "status");
208 pill.setAttribute("aria-live", "polite");
209 pill.setAttribute("aria-atomic", "true");
210 var said = element("span", "visually-hidden", kind.label + ": ");
211 pill.appendChild(said);
212 pill.appendChild(element("span", "pill-dot"));
213 pill.appendChild(element("span", "pill-text"));
214 name.appendChild(pill);
215 head.appendChild(badge);
216 head.appendChild(name);
217 card.appendChild(head);
218 card.appendChild(element("p", "kind-about"));
219 var body = element("div", "kind-body");
220 card.appendChild(body);
221 var details = element("dl", "kind-details");
222 card.appendChild(details);
223 var actions = element("div", "kind-actions");
224 card.appendChild(actions);
225 return { root: card, pill: pill, about: card.querySelector(".kind-about"), body: body, details: details, actions: actions, signature: "" };
226 }
227
228 // What it is running, with where to see it on g1t.
229 function jobRow(job) {
230 var row = element("div", "kind-job");
231 row.appendChild(element("span", "kind-job-label", job.kind === "agent" ? "Agent work" : "Running"));
232 var what = element("span", "kind-job-what");
233 if (job.link) {
234 var link = element("a", null, job.name);
235 link.href = job.link;
236 what.appendChild(link);
237 } else {
238 what.appendChild(element("strong", null, job.name));
239 }
240 if (job.repo) what.appendChild(element("span", "muted", " in " + job.repo));
241 row.appendChild(what);
242 return row;
243 }
244
245 function progressRow(kind) {
246 var row = element("div", "kind-progress");
247 var bar = element("div", "bar");
248 bar.setAttribute("role", "progressbar");
249 bar.setAttribute("aria-valuemin", "0");
250 bar.setAttribute("aria-valuemax", "100");
251 bar.setAttribute("aria-valuenow", String(kind.progress));
252 bar.setAttribute("aria-label", kind.label + ": the sandbox's image");
253 var fill = element("span");
254 fill.style.width = Math.max(2, Math.min(100, kind.progress)) + "%";
255 bar.appendChild(fill);
256 row.appendChild(bar);
257 row.appendChild(element("span", "muted small", kind.progress + "%"));
258 return row;
259 }
260
261 function detail(list, term, value, given) {
262 if (!value) return;
263 var row = element("div");
264 row.appendChild(element("dt", null, term));
265 var dd = element("dd", null, given ? null : value);
266 if (given) dd.appendChild(given);
267 row.appendChild(dd);
268 list.appendChild(row);
269 }
270
271 // Start or Stop for one kind of job, said for which, and what it is doing meanwhile.
272 function toggleButton(kind, onFirst) {
273 var on = kind.enabled;
274 var waiting = pending[kind.id];
275 var button = element("button", on ? "quiet" : onFirst ? "primary" : null);
276 button.type = "button";
277 button.textContent = waiting ? (on ? "Stopping…" : "Starting…") : on ? "Stop" : "Start";
278 button.setAttribute("aria-label", (on ? "Stop " : "Start ") + kind.label);
279 button.disabled = !!waiting;
280 button.addEventListener("click", function () {
281 pending[kind.id] = true;
282 renderKinds(lastKinds);
283 invoke("runner_control", { action: on ? "stop" : "start", capability: kind.id })
284 .catch(fail)
285 .then(function () {
286 // The status that follows says how it is; until then it says what was asked.
287 setTimeout(function () {
288 delete pending[kind.id];
289 renderKinds(lastKinds);
290 }, 1500);
291 });
292 });
293 return button;
294 }
295
296 function fillCard(card, kind, runner) {
297 var root = card.root;
298 root.dataset.state = kind.state;
299 root.dataset.tone = kind.tone;
300 root.classList.toggle("off", !kind.enabled);
301 card.pill.dataset.tone = kind.tone;
302 card.pill.classList.toggle("live", !!BUSY[kind.state]);
303 // Said again only when it changes, so a screen reader hears each change once.
304 var text = card.pill.querySelector(".pill-text");
305 if (text.textContent !== kind.title) text.textContent = kind.title;
306 if (card.about.textContent !== kind.about) card.about.textContent = kind.about;
307
308 // What changes the buttons and the words is drawn again only when it changes,
309 // so a button keeps its focus while the status ticks.
310 var update = runner.diagnosis.update || null;
311 var signature = JSON.stringify([kind, !!pending[kind.id], runner.link, update && update.checked]);
312 if (signature === card.signature) return;
313 card.signature = signature;
314
315 var body = card.body;
316 body.textContent = "";
317 if (kind.job) body.appendChild(jobRow(kind.job));
318 if (kind.state === "downloading" && kind.progress != null) body.appendChild(progressRow(kind));
319 if (kind.headline) body.appendChild(element("p", "kind-headline", kind.headline));
320 if (kind.cause) body.appendChild(prose(element("p", "kind-cause"), kind.cause));
321 if (kind.fix) body.appendChild(prose(element("p", "kind-fix"), kind.fix));
322 body.hidden = !body.firstChild;
323
324 var details = card.details;
325 details.textContent = "";
326 // Its name on g1t opens the workspace's Runners page, where it is listed.
327 var named = null;
328 if (kind.runner && runner.link) {
329 named = element("button", "link", kind.runner);
330 named.type = "button";
331 named.setAttribute("aria-label", "View " + kind.runner + " on g1t");
332 named.appendChild(outward());
333 named.addEventListener("click", function () {
334 invoke("runner_control", { action: "view_runners" }).catch(fail);
335 });
336 }
337 detail(details, "On g1t", kind.runner, named);
338 detail(details, "Version", kind.version ? "g1t-runner " + kind.version : null);
339 // A newer version waiting for this kind of job, and when g1t last looked.
340 if (update && (kind.update || kind.version)) {
341 var updates = element("span", "kind-update");
342 if (kind.update) updates.appendChild(element("span", "kind-update-ready", kind.update));
343 updates.appendChild(element("span", "muted", update.checked));
344 detail(details, "Updates", true, updates);
345 }
346 detail(details, "Isolation", ISOLATION[kind.isolation] || kind.isolation);
347 details.hidden = !details.firstChild;
348
349 var actions = card.actions;
350 actions.textContent = "";
351 // Its first fix stands out when it is the person's to do; while g1t
352 // sees to it (waiting for the release's images), none does.
353 var urgent = kind.tone === "problem" || kind.tone === "warning";
354 kind.actions.forEach(function (action, at) {
355 actions.appendChild(actionButton(action, at === 0 && urgent && kind.state !== "waiting"));
356 });
357 if (kind.can_toggle) {
358 var toggle = toggleButton(kind, kind.actions.length === 0);
359 toggle.classList.add("end");
360 actions.appendChild(toggle);
361 }
362 actions.hidden = !actions.firstChild;
363 }
364
365 var lastKinds = null;
366
367 function renderKinds(runner) {
368 lastKinds = runner;
369 var list = byId("runner-kinds");
370 var kinds = (runner && runner.diagnosis.capabilities) || [];
371 list.hidden = kinds.length === 0;
372 var seen = {};
373 kinds.forEach(function (kind, at) {
374 seen[kind.id] = true;
375 var card = cards[kind.id] || (cards[kind.id] = makeCard(kind));
376 if (list.children[at] !== card.root) list.insertBefore(card.root, list.children[at] || null);
377 fillCard(card, kind, runner);
378 });
379 Object.keys(cards).forEach(function (id) {
380 if (seen[id]) return;
381 cards[id].root.remove();
382 delete cards[id];
383 });
384 list.classList.toggle("single", kinds.length === 1);
385 }
386
387 function renderRunner(runner) {
388 if (!runner) return;
389 var diagnosis = runner.diagnosis;
390 // The whole runner's line, when the cards don't already say it.
391 var cardsSayIt = diagnosis.in_capabilities && diagnosis.capabilities && diagnosis.capabilities.length > 0;
392 byId("runner-diagnosis").hidden = !!cardsSayIt;
393 byId("runner-dot").className = "dot " + diagnosis.tone;
394 byId("runner-title").textContent = diagnosis.title;
395 prose(byId("runner-cause"), diagnosis.cause);
396 prose(byId("runner-fix"), diagnosis.fix);
397 var actions = byId("runner-actions");
398 actions.textContent = "";
399 diagnosis.actions.forEach(function (action, at) {
400 actions.appendChild(actionButton(action, at === 0));
401 });
402 renderKinds(runner);
403 byId("runner").checked = !!runner.enabled;
404
405 var backend = byId("runnerBackend");
406 backend.value = runner.backend;
407 var about = BACKENDS[runner.backend] || BACKENDS.sandboxed;
408 byId("runner-backend-note").textContent = about.text;
409 byId("runner-backend-note").className = about.warn ? "warning-note" : "muted";
410
411 var workspaces = byId("runnerWorkspace");
412 workspaces.textContent = "";
413 var known = runner.workspaces.slice();
414 var listed = known.some(function (w) {
415 return w.slug === runner.workspace;
416 });
417 if (runner.workspace && !listed) known.unshift({ slug: runner.workspace, name: null });
418 known.forEach(function (w) {
419 workspaces.appendChild(option(w.slug, w.name ? w.name + " (" + w.slug + ")" : w.slug));
420 });
421 if (known.length === 0) workspaces.appendChild(option("", "Open g1t, signed in, to choose"));
422 workspaces.value = runner.workspace || "";
423 workspaces.disabled = known.length === 0;
424
425 byId("runner-machine").textContent = "This computer: " + runner.machine;
426 // The runner's version, when g1t last looked for a newer one, and, with
427 // no cards to say it, what becomes of one waiting.
428 var update = diagnosis.update;
429 var about = [runner.version ? "g1t-runner " + runner.version : null, update ? update.checked : null, update && update.note].filter(Boolean);
430 byId("runner-version").textContent = about.length ? about.join(" · ") : " ";
431 var check = byId("runner-check");
432 check.hidden = !update;
433 if (update) {
434 check.textContent = update.check.label;
435 check.disabled = update.checking;
436 }
437 byId("runner-view").hidden = !runner.link;
438 byId("runner-log").textContent = runner.log.length ? runner.log.join("\n") : "Nothing yet.";
439 }
440
441 byId("runnerBackend").addEventListener("change", save);
442 byId("runnerWorkspace").addEventListener("change", save);
443 byId("runner-view").addEventListener("click", function () {
444 invoke("runner_control", { action: "view_runners" }).catch(fail);
445 });
446 byId("runner-open-log").addEventListener("click", function () {
447 invoke("runner_control", { action: "view_log" }).catch(fail);
448 });
449 // Looks now; what it finds installs as each kind of job is idle, and the status that follows says so.
450 byId("runner-check").addEventListener("click", function () {
451 byId("runner-check").disabled = true;
452 invoke("runner_control", { action: "check_updates" }).catch(function (error) {
453 byId("runner-check").disabled = false;
454 fail(error);
455 });
456 });
457
458 function save() {
459 invoke("save_settings", { settings: settingsOf() }).then(render, function (error) {
460 fail(error);
461 if (current) render(current);
462 });
463 }
464
465 SWITCHES.forEach(function (key) {
466 byId(key).addEventListener("change", save);
467 });
468 byId("zoom").addEventListener("change", save);
469 byId("openAtLogin").addEventListener("change", function (event) {
470 invoke("set_open_at_login", { on: event.target.checked }).then(render, function (error) {
471 fail(error);
472 if (current) render(current);
473 });
474 });
475
476 // Updates: where this copy stands, said in the window, so a check from
477 // the menu or the tray is answered here and cannot be missed.
478 var checking = false;
479
480 function when(ms) {
481 var date = new Date(ms);
482 var today = new Date().toDateString() === date.toDateString();
483 var time = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" });
484 return today ? "today at " + time : date.toLocaleDateString([], { month: "short", day: "numeric" }) + " at " + time;
485 }
486
487 function ready(version) {
488 byId("update-status").textContent = "Version " + version + " is downloaded and installs when g1t restarts.";
489 byId("restart").hidden = false;
490 }
491
492 function updateStatus(view) {
493 if (view.updateReady) return ready(view.updateReady);
494 byId("restart").hidden = true;
495 byId("update-status").textContent = view.checkedAt
496 ? "Up to date. Last checked " + when(view.checkedAt) + "."
497 : view.autoUpdate
498 ? "Not checked yet since g1t started; it checks on its own shortly."
499 : "Checking on its own is off.";
500 }
501
502 function check() {
503 if (checking) return;
504 checking = true;
505 var button = byId("check");
506 var status = byId("update-status");
507 button.disabled = true;
508 status.textContent = "Checking, and downloading any new version…";
509 invoke("check_for_updates")
510 .then(function (answer) {
511 if (answer.indexOf("ready:") === 0) ready(answer.slice(6));
512 else status.textContent = "Up to date. Last checked " + when(Date.now()) + ".";
513 })
514 .catch(function (error) {
515 var text = typeof error === "string" ? error : "";
516 status.textContent = /sending request|connect|dns|timed out/i.test(text) || !text
517 ? "Could not reach g1t.sh to check. Check your connection and try again."
518 : "Could not check: " + text;
519 })
520 .then(function () {
521 checking = false;
522 button.disabled = false;
523 });
524 }
525
526 byId("check").addEventListener("click", check);
527 byId("restart").addEventListener("click", function () {
528 byId("restart").disabled = true;
529 byId("update-status").textContent = "Installing…";
530 invoke("restart_to_update").catch(function (error) {
531 byId("restart").disabled = false;
532 fail(error);
533 });
534 });
535
536 // The menu's or tray's "Check for updates…" with this window open, and a
537 // download finishing in the background (src/settings.rs).
538 var events = window.__TAURI__ && window.__TAURI__.event;
539 if (events && typeof events.listen === "function") {
540 events.listen("g1t-check-updates", check).catch(function () {});
541 events
542 .listen("g1t-runner-status", function (event) {
543 if (current) current.runnerView = event.payload;
544 pending = {};
545 renderRunner(event.payload);
546 })
547 .catch(function () {});
548 events
549 .listen("g1t-update-ready", function (event) {
550 if (!checking && typeof event.payload === "string") ready(event.payload);
551 })
552 .catch(function () {});
553 }
554
555 byId("restart-app").addEventListener("click", function () {
556 byId("restart-app").disabled = true;
557 invoke("restart").catch(function (error) {
558 byId("restart-app").disabled = false;
559 fail(error);
560 });
561 });
562
563 byId("change-server").addEventListener("click", function () {
564 invoke("change_server").catch(fail);
565 });
566
567 // Links out, in the system's browser (src/bridge.js does the same on the site).
568 document.addEventListener("click", function (event) {
569 var anchor = event.target && event.target.closest ? event.target.closest("a[href]") : null;
570 if (!anchor) return;
571 event.preventDefault();
572 invoke("open_url", { url: anchor.href }).catch(function () {});
573 });
574
575 // g1t's own title bar, when the window has no system frame.
576 function frame(view) {
577 if (!view.customFrame) return;
578 if (view.platform === "macos") document.documentElement.classList.add("mac");
579 var bar = byId("titlebar");
580 bar.hidden = false;
581 document.body.classList.add("framed");
582 bar.addEventListener("mousedown", function (event) {
583 if (event.button !== 0 || event.target.closest("button")) return;
584 invoke("window_control", { action: "drag" }).catch(function () {});
585 });
586 byId("close-window").addEventListener("click", function () {
587 invoke("window_control", { action: "close" }).catch(function () {});
588 });
589 }
590
591 invoke("get_settings")
592 .then(function (view) {
593 frame(view);
594 platform(view.platform);
595 render(view);
596 // Opened by "Check for updates…" in a menu.
597 if (new URLSearchParams(location.search).has("check")) {
598 byId("updates-row").scrollIntoView({ block: "center" });
599 check();
600 }
601 })
602 .catch(fail);
603})();