| 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 signature = JSON.stringify([kind, !!pending[kind.id], runner.link]); |
| 311 | if (signature === card.signature) return; |
| 312 | card.signature = signature; |
| 313 | |
| 314 | var body = card.body; |
| 315 | body.textContent = ""; |
| 316 | if (kind.job) body.appendChild(jobRow(kind.job)); |
| 317 | if (kind.state === "downloading" && kind.progress != null) body.appendChild(progressRow(kind)); |
| 318 | if (kind.headline) body.appendChild(element("p", "kind-headline", kind.headline)); |
| 319 | if (kind.cause) body.appendChild(prose(element("p", "kind-cause"), kind.cause)); |
| 320 | if (kind.fix) body.appendChild(prose(element("p", "kind-fix"), kind.fix)); |
| 321 | body.hidden = !body.firstChild; |
| 322 | |
| 323 | var details = card.details; |
| 324 | details.textContent = ""; |
| 325 | // Its name on g1t opens the workspace's Runners page, where it is listed. |
| 326 | var named = null; |
| 327 | if (kind.runner && runner.link) { |
| 328 | named = element("button", "link", kind.runner); |
| 329 | named.type = "button"; |
| 330 | named.setAttribute("aria-label", "View " + kind.runner + " on g1t"); |
| 331 | named.appendChild(outward()); |
| 332 | named.addEventListener("click", function () { |
| 333 | invoke("runner_control", { action: "view_runners" }).catch(fail); |
| 334 | }); |
| 335 | } |
| 336 | detail(details, "On g1t", kind.runner, named); |
| 337 | detail(details, "Version", kind.version ? "g1t-runner " + kind.version : null); |
| 338 | detail(details, "Isolation", ISOLATION[kind.isolation] || kind.isolation); |
| 339 | details.hidden = !details.firstChild; |
| 340 | |
| 341 | var actions = card.actions; |
| 342 | actions.textContent = ""; |
| 343 | // Its first fix stands out when it is the person's to do; while g1t |
| 344 | // sees to it (waiting for the release's images), none does. |
| 345 | var urgent = kind.tone === "problem" || kind.tone === "warning"; |
| 346 | kind.actions.forEach(function (action, at) { |
| 347 | actions.appendChild(actionButton(action, at === 0 && urgent && kind.state !== "waiting")); |
| 348 | }); |
| 349 | if (kind.can_toggle) { |
| 350 | var toggle = toggleButton(kind, kind.actions.length === 0); |
| 351 | toggle.classList.add("end"); |
| 352 | actions.appendChild(toggle); |
| 353 | } |
| 354 | actions.hidden = !actions.firstChild; |
| 355 | } |
| 356 | |
| 357 | var lastKinds = null; |
| 358 | |
| 359 | function renderKinds(runner) { |
| 360 | lastKinds = runner; |
| 361 | var list = byId("runner-kinds"); |
| 362 | var kinds = (runner && runner.diagnosis.capabilities) || []; |
| 363 | list.hidden = kinds.length === 0; |
| 364 | var seen = {}; |
| 365 | kinds.forEach(function (kind, at) { |
| 366 | seen[kind.id] = true; |
| 367 | var card = cards[kind.id] || (cards[kind.id] = makeCard(kind)); |
| 368 | if (list.children[at] !== card.root) list.insertBefore(card.root, list.children[at] || null); |
| 369 | fillCard(card, kind, runner); |
| 370 | }); |
| 371 | Object.keys(cards).forEach(function (id) { |
| 372 | if (seen[id]) return; |
| 373 | cards[id].root.remove(); |
| 374 | delete cards[id]; |
| 375 | }); |
| 376 | list.classList.toggle("single", kinds.length === 1); |
| 377 | } |
| 378 | |
| 379 | function renderRunner(runner) { |
| 380 | if (!runner) return; |
| 381 | var diagnosis = runner.diagnosis; |
| 382 | // The whole runner's line, when the cards don't already say it. |
| 383 | var cardsSayIt = diagnosis.in_capabilities && diagnosis.capabilities && diagnosis.capabilities.length > 0; |
| 384 | byId("runner-diagnosis").hidden = !!cardsSayIt; |
| 385 | byId("runner-dot").className = "dot " + diagnosis.tone; |
| 386 | byId("runner-title").textContent = diagnosis.title; |
| 387 | prose(byId("runner-cause"), diagnosis.cause); |
| 388 | prose(byId("runner-fix"), diagnosis.fix); |
| 389 | var actions = byId("runner-actions"); |
| 390 | actions.textContent = ""; |
| 391 | diagnosis.actions.forEach(function (action, at) { |
| 392 | actions.appendChild(actionButton(action, at === 0)); |
| 393 | }); |
| 394 | renderKinds(runner); |
| 395 | byId("runner").checked = !!runner.enabled; |
| 396 | |
| 397 | var backend = byId("runnerBackend"); |
| 398 | backend.value = runner.backend; |
| 399 | var about = BACKENDS[runner.backend] || BACKENDS.sandboxed; |
| 400 | byId("runner-backend-note").textContent = about.text; |
| 401 | byId("runner-backend-note").className = about.warn ? "warning-note" : "muted"; |
| 402 | |
| 403 | var workspaces = byId("runnerWorkspace"); |
| 404 | workspaces.textContent = ""; |
| 405 | var known = runner.workspaces.slice(); |
| 406 | var listed = known.some(function (w) { |
| 407 | return w.slug === runner.workspace; |
| 408 | }); |
| 409 | if (runner.workspace && !listed) known.unshift({ slug: runner.workspace, name: null }); |
| 410 | known.forEach(function (w) { |
| 411 | workspaces.appendChild(option(w.slug, w.name ? w.name + " (" + w.slug + ")" : w.slug)); |
| 412 | }); |
| 413 | if (known.length === 0) workspaces.appendChild(option("", "Open g1t, signed in, to choose")); |
| 414 | workspaces.value = runner.workspace || ""; |
| 415 | workspaces.disabled = known.length === 0; |
| 416 | |
| 417 | byId("runner-machine").textContent = "This computer: " + runner.machine; |
| 418 | byId("runner-version").textContent = runner.version ? "g1t-runner " + runner.version : " "; |
| 419 | byId("runner-view").hidden = !runner.link; |
| 420 | byId("runner-log").textContent = runner.log.length ? runner.log.join("\n") : "Nothing yet."; |
| 421 | } |
| 422 | |
| 423 | byId("runnerBackend").addEventListener("change", save); |
| 424 | byId("runnerWorkspace").addEventListener("change", save); |
| 425 | byId("runner-view").addEventListener("click", function () { |
| 426 | invoke("runner_control", { action: "view_runners" }).catch(fail); |
| 427 | }); |
| 428 | byId("runner-open-log").addEventListener("click", function () { |
| 429 | invoke("runner_control", { action: "view_log" }).catch(fail); |
| 430 | }); |
| 431 | |
| 432 | function save() { |
| 433 | invoke("save_settings", { settings: settingsOf() }).then(render, function (error) { |
| 434 | fail(error); |
| 435 | if (current) render(current); |
| 436 | }); |
| 437 | } |
| 438 | |
| 439 | SWITCHES.forEach(function (key) { |
| 440 | byId(key).addEventListener("change", save); |
| 441 | }); |
| 442 | byId("zoom").addEventListener("change", save); |
| 443 | byId("openAtLogin").addEventListener("change", function (event) { |
| 444 | invoke("set_open_at_login", { on: event.target.checked }).then(render, function (error) { |
| 445 | fail(error); |
| 446 | if (current) render(current); |
| 447 | }); |
| 448 | }); |
| 449 | |
| 450 | // Updates: where this copy stands, said in the window, so a check from |
| 451 | // the menu or the tray is answered here and cannot be missed. |
| 452 | var checking = false; |
| 453 | |
| 454 | function when(ms) { |
| 455 | var date = new Date(ms); |
| 456 | var today = new Date().toDateString() === date.toDateString(); |
| 457 | var time = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }); |
| 458 | return today ? "today at " + time : date.toLocaleDateString([], { month: "short", day: "numeric" }) + " at " + time; |
| 459 | } |
| 460 | |
| 461 | function ready(version) { |
| 462 | byId("update-status").textContent = "Version " + version + " is downloaded and installs when g1t restarts."; |
| 463 | byId("restart").hidden = false; |
| 464 | } |
| 465 | |
| 466 | function updateStatus(view) { |
| 467 | if (view.updateReady) return ready(view.updateReady); |
| 468 | byId("restart").hidden = true; |
| 469 | byId("update-status").textContent = view.checkedAt |
| 470 | ? "Up to date. Last checked " + when(view.checkedAt) + "." |
| 471 | : view.autoUpdate |
| 472 | ? "Not checked yet since g1t started; it checks on its own shortly." |
| 473 | : "Checking on its own is off."; |
| 474 | } |
| 475 | |
| 476 | function check() { |
| 477 | if (checking) return; |
| 478 | checking = true; |
| 479 | var button = byId("check"); |
| 480 | var status = byId("update-status"); |
| 481 | button.disabled = true; |
| 482 | status.textContent = "Checking, and downloading any new version…"; |
| 483 | invoke("check_for_updates") |
| 484 | .then(function (answer) { |
| 485 | if (answer.indexOf("ready:") === 0) ready(answer.slice(6)); |
| 486 | else status.textContent = "Up to date. Last checked " + when(Date.now()) + "."; |
| 487 | }) |
| 488 | .catch(function (error) { |
| 489 | var text = typeof error === "string" ? error : ""; |
| 490 | status.textContent = /sending request|connect|dns|timed out/i.test(text) || !text |
| 491 | ? "Could not reach g1t.sh to check. Check your connection and try again." |
| 492 | : "Could not check: " + text; |
| 493 | }) |
| 494 | .then(function () { |
| 495 | checking = false; |
| 496 | button.disabled = false; |
| 497 | }); |
| 498 | } |
| 499 | |
| 500 | byId("check").addEventListener("click", check); |
| 501 | byId("restart").addEventListener("click", function () { |
| 502 | byId("restart").disabled = true; |
| 503 | byId("update-status").textContent = "Installing…"; |
| 504 | invoke("restart_to_update").catch(function (error) { |
| 505 | byId("restart").disabled = false; |
| 506 | fail(error); |
| 507 | }); |
| 508 | }); |
| 509 | |
| 510 | // The menu's or tray's "Check for updates…" with this window open, and a |
| 511 | // download finishing in the background (src/settings.rs). |
| 512 | var events = window.__TAURI__ && window.__TAURI__.event; |
| 513 | if (events && typeof events.listen === "function") { |
| 514 | events.listen("g1t-check-updates", check).catch(function () {}); |
| 515 | events |
| 516 | .listen("g1t-runner-status", function (event) { |
| 517 | if (current) current.runnerView = event.payload; |
| 518 | pending = {}; |
| 519 | renderRunner(event.payload); |
| 520 | }) |
| 521 | .catch(function () {}); |
| 522 | events |
| 523 | .listen("g1t-update-ready", function (event) { |
| 524 | if (!checking && typeof event.payload === "string") ready(event.payload); |
| 525 | }) |
| 526 | .catch(function () {}); |
| 527 | } |
| 528 | |
| 529 | byId("restart-app").addEventListener("click", function () { |
| 530 | byId("restart-app").disabled = true; |
| 531 | invoke("restart").catch(function (error) { |
| 532 | byId("restart-app").disabled = false; |
| 533 | fail(error); |
| 534 | }); |
| 535 | }); |
| 536 | |
| 537 | byId("change-server").addEventListener("click", function () { |
| 538 | invoke("change_server").catch(fail); |
| 539 | }); |
| 540 | |
| 541 | // Links out, in the system's browser (src/bridge.js does the same on the site). |
| 542 | document.addEventListener("click", function (event) { |
| 543 | var anchor = event.target && event.target.closest ? event.target.closest("a[href]") : null; |
| 544 | if (!anchor) return; |
| 545 | event.preventDefault(); |
| 546 | invoke("open_url", { url: anchor.href }).catch(function () {}); |
| 547 | }); |
| 548 | |
| 549 | // g1t's own title bar, when the window has no system frame. |
| 550 | function frame(view) { |
| 551 | if (!view.customFrame) return; |
| 552 | if (view.platform === "macos") document.documentElement.classList.add("mac"); |
| 553 | var bar = byId("titlebar"); |
| 554 | bar.hidden = false; |
| 555 | document.body.classList.add("framed"); |
| 556 | bar.addEventListener("mousedown", function (event) { |
| 557 | if (event.button !== 0 || event.target.closest("button")) return; |
| 558 | invoke("window_control", { action: "drag" }).catch(function () {}); |
| 559 | }); |
| 560 | byId("close-window").addEventListener("click", function () { |
| 561 | invoke("window_control", { action: "close" }).catch(function () {}); |
| 562 | }); |
| 563 | } |
| 564 | |
| 565 | invoke("get_settings") |
| 566 | .then(function (view) { |
| 567 | frame(view); |
| 568 | platform(view.platform); |
| 569 | render(view); |
| 570 | // Opened by "Check for updates…" in a menu. |
| 571 | if (new URLSearchParams(location.search).has("check")) { |
| 572 | byId("updates-row").scrollIntoView({ block: "center" }); |
| 573 | check(); |
| 574 | } |
| 575 | }) |
| 576 | .catch(fail); |
| 577 | })(); |