| 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 | return settings; |
| 84 | } |
| 85 | |
| 86 | // ── The runner ────────────────────────────────────────────────────── |
| 87 | // Its diagnosis, from src/runner/diagnosis.rs, as the tray and the |
| 88 | // site's Runners page show it; its buttons are the fixes. |
| 89 | function option(value, text) { |
| 90 | var item = document.createElement("option"); |
| 91 | item.value = value; |
| 92 | item.textContent = text; |
| 93 | return item; |
| 94 | } |
| 95 | |
| 96 | // A fix's button. Choosing is done here, in this window; a copy says it was copied. |
| 97 | function actionButton(action, primary) { |
| 98 | var button = document.createElement("button"); |
| 99 | button.type = "button"; |
| 100 | button.textContent = action.label; |
| 101 | if (primary) button.className = "primary"; |
| 102 | button.addEventListener("click", function () { |
| 103 | if (action.id === "choose_workspace") return byId("runnerWorkspace").focus(); |
| 104 | if (action.id === "choose_backend") return byId("runnerBackend").focus(); |
| 105 | invoke("runner_control", { action: action.id }).then(function () { |
| 106 | if (action.id.indexOf("copy_") !== 0) return; |
| 107 | button.textContent = "Copied"; |
| 108 | setTimeout(function () { |
| 109 | button.textContent = action.label; |
| 110 | }, 2000); |
| 111 | }, fail); |
| 112 | }); |
| 113 | return button; |
| 114 | } |
| 115 | |
| 116 | function span(className, text) { |
| 117 | var item = document.createElement("span"); |
| 118 | if (className) item.className = className; |
| 119 | item.textContent = text; |
| 120 | return item; |
| 121 | } |
| 122 | |
| 123 | // Each kind of job the sandbox runs, when there is more than one: Linux |
| 124 | // jobs can run while Windows jobs cannot. |
| 125 | function renderCapabilities(capabilities) { |
| 126 | var list = byId("runner-capabilities"); |
| 127 | list.textContent = ""; |
| 128 | list.hidden = !capabilities || capabilities.length < 2; |
| 129 | if (list.hidden) return; |
| 130 | capabilities.forEach(function (capability) { |
| 131 | var item = document.createElement("li"); |
| 132 | item.appendChild(span("dot " + capability.tone, "")); |
| 133 | var text = span("diagnosis-text", ""); |
| 134 | var title = document.createElement("strong"); |
| 135 | title.textContent = capability.label + ": " + capability.title; |
| 136 | text.appendChild(title); |
| 137 | if (capability.cause) text.appendChild(span("muted", capability.cause)); |
| 138 | if (capability.fix) text.appendChild(span("", capability.fix)); |
| 139 | var buttons = span("buttons", ""); |
| 140 | capability.actions.forEach(function (action) { |
| 141 | buttons.appendChild(actionButton(action, false)); |
| 142 | }); |
| 143 | text.appendChild(buttons); |
| 144 | item.appendChild(text); |
| 145 | list.appendChild(item); |
| 146 | }); |
| 147 | } |
| 148 | |
| 149 | function renderRunner(runner) { |
| 150 | if (!runner) return; |
| 151 | var diagnosis = runner.diagnosis; |
| 152 | byId("runner-dot").className = "dot " + diagnosis.tone; |
| 153 | byId("runner-title").textContent = diagnosis.title; |
| 154 | byId("runner-cause").textContent = diagnosis.cause || ""; |
| 155 | byId("runner-fix").textContent = diagnosis.fix || ""; |
| 156 | var actions = byId("runner-actions"); |
| 157 | actions.textContent = ""; |
| 158 | diagnosis.actions.forEach(function (action, at) { |
| 159 | actions.appendChild(actionButton(action, at === 0)); |
| 160 | }); |
| 161 | renderCapabilities(diagnosis.capabilities); |
| 162 | byId("runner").checked = !!runner.enabled; |
| 163 | var on = runner.enabled && !runner.paused; |
| 164 | byId("runner-toggle").textContent = on ? "Stop" : "Start"; |
| 165 | byId("runner-toggle").dataset.action = on ? "stop" : "start"; |
| 166 | |
| 167 | var backend = byId("runnerBackend"); |
| 168 | backend.value = runner.backend; |
| 169 | var about = BACKENDS[runner.backend] || BACKENDS.sandboxed; |
| 170 | byId("runner-backend-note").textContent = about.text; |
| 171 | byId("runner-backend-note").className = about.warn ? "warning-note" : "muted"; |
| 172 | |
| 173 | var workspaces = byId("runnerWorkspace"); |
| 174 | workspaces.textContent = ""; |
| 175 | var known = runner.workspaces.slice(); |
| 176 | var listed = known.some(function (w) { |
| 177 | return w.slug === runner.workspace; |
| 178 | }); |
| 179 | if (runner.workspace && !listed) known.unshift({ slug: runner.workspace, name: null }); |
| 180 | known.forEach(function (w) { |
| 181 | workspaces.appendChild(option(w.slug, w.name ? w.name + " (" + w.slug + ")" : w.slug)); |
| 182 | }); |
| 183 | if (known.length === 0) workspaces.appendChild(option("", "Open g1t, signed in, to choose")); |
| 184 | workspaces.value = runner.workspace || ""; |
| 185 | workspaces.disabled = known.length === 0; |
| 186 | |
| 187 | byId("runner-machine").textContent = "This computer: " + runner.machine; |
| 188 | byId("runner-version").textContent = runner.version ? "g1t-runner " + runner.version : " "; |
| 189 | byId("runner-view").hidden = !runner.link; |
| 190 | byId("runner-log").textContent = runner.log.length ? runner.log.join("\n") : "Nothing yet."; |
| 191 | } |
| 192 | |
| 193 | byId("runnerBackend").addEventListener("change", save); |
| 194 | byId("runnerWorkspace").addEventListener("change", save); |
| 195 | byId("runner-toggle").addEventListener("click", function (event) { |
| 196 | invoke("runner_control", { action: event.target.dataset.action || "start" }).catch(fail); |
| 197 | }); |
| 198 | byId("runner-view").addEventListener("click", function () { |
| 199 | invoke("runner_control", { action: "view_runners" }).catch(fail); |
| 200 | }); |
| 201 | byId("runner-open-log").addEventListener("click", function () { |
| 202 | invoke("runner_control", { action: "view_log" }).catch(fail); |
| 203 | }); |
| 204 | |
| 205 | function save() { |
| 206 | invoke("save_settings", { settings: settingsOf() }).then(render, function (error) { |
| 207 | fail(error); |
| 208 | if (current) render(current); |
| 209 | }); |
| 210 | } |
| 211 | |
| 212 | SWITCHES.forEach(function (key) { |
| 213 | byId(key).addEventListener("change", save); |
| 214 | }); |
| 215 | byId("zoom").addEventListener("change", save); |
| 216 | byId("openAtLogin").addEventListener("change", function (event) { |
| 217 | invoke("set_open_at_login", { on: event.target.checked }).then(render, function (error) { |
| 218 | fail(error); |
| 219 | if (current) render(current); |
| 220 | }); |
| 221 | }); |
| 222 | |
| 223 | // Updates: where this copy stands, said in the window, so a check from |
| 224 | // the menu or the tray is answered here and cannot be missed. |
| 225 | var checking = false; |
| 226 | |
| 227 | function when(ms) { |
| 228 | var date = new Date(ms); |
| 229 | var today = new Date().toDateString() === date.toDateString(); |
| 230 | var time = date.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }); |
| 231 | return today ? "today at " + time : date.toLocaleDateString([], { month: "short", day: "numeric" }) + " at " + time; |
| 232 | } |
| 233 | |
| 234 | function ready(version) { |
| 235 | byId("update-status").textContent = "Version " + version + " is downloaded and installs when g1t restarts."; |
| 236 | byId("restart").hidden = false; |
| 237 | } |
| 238 | |
| 239 | function updateStatus(view) { |
| 240 | if (view.updateReady) return ready(view.updateReady); |
| 241 | byId("restart").hidden = true; |
| 242 | byId("update-status").textContent = view.checkedAt |
| 243 | ? "Up to date. Last checked " + when(view.checkedAt) + "." |
| 244 | : view.autoUpdate |
| 245 | ? "Not checked yet since g1t started; it checks on its own shortly." |
| 246 | : "Checking on its own is off."; |
| 247 | } |
| 248 | |
| 249 | function check() { |
| 250 | if (checking) return; |
| 251 | checking = true; |
| 252 | var button = byId("check"); |
| 253 | var status = byId("update-status"); |
| 254 | button.disabled = true; |
| 255 | status.textContent = "Checking, and downloading any new version…"; |
| 256 | invoke("check_for_updates") |
| 257 | .then(function (answer) { |
| 258 | if (answer.indexOf("ready:") === 0) ready(answer.slice(6)); |
| 259 | else status.textContent = "Up to date. Last checked " + when(Date.now()) + "."; |
| 260 | }) |
| 261 | .catch(function (error) { |
| 262 | var text = typeof error === "string" ? error : ""; |
| 263 | status.textContent = /sending request|connect|dns|timed out/i.test(text) || !text |
| 264 | ? "Could not reach g1t.sh to check. Check your connection and try again." |
| 265 | : "Could not check: " + text; |
| 266 | }) |
| 267 | .then(function () { |
| 268 | checking = false; |
| 269 | button.disabled = false; |
| 270 | }); |
| 271 | } |
| 272 | |
| 273 | byId("check").addEventListener("click", check); |
| 274 | byId("restart").addEventListener("click", function () { |
| 275 | byId("restart").disabled = true; |
| 276 | byId("update-status").textContent = "Installing…"; |
| 277 | invoke("restart_to_update").catch(function (error) { |
| 278 | byId("restart").disabled = false; |
| 279 | fail(error); |
| 280 | }); |
| 281 | }); |
| 282 | |
| 283 | // The menu's or tray's "Check for updates…" with this window open, and a |
| 284 | // download finishing in the background (src/settings.rs). |
| 285 | var events = window.__TAURI__ && window.__TAURI__.event; |
| 286 | if (events && typeof events.listen === "function") { |
| 287 | events.listen("g1t-check-updates", check).catch(function () {}); |
| 288 | events |
| 289 | .listen("g1t-runner-status", function (event) { |
| 290 | if (current) current.runnerView = event.payload; |
| 291 | renderRunner(event.payload); |
| 292 | }) |
| 293 | .catch(function () {}); |
| 294 | events |
| 295 | .listen("g1t-update-ready", function (event) { |
| 296 | if (!checking && typeof event.payload === "string") ready(event.payload); |
| 297 | }) |
| 298 | .catch(function () {}); |
| 299 | } |
| 300 | |
| 301 | byId("restart-app").addEventListener("click", function () { |
| 302 | byId("restart-app").disabled = true; |
| 303 | invoke("restart").catch(function (error) { |
| 304 | byId("restart-app").disabled = false; |
| 305 | fail(error); |
| 306 | }); |
| 307 | }); |
| 308 | |
| 309 | byId("change-server").addEventListener("click", function () { |
| 310 | invoke("change_server").catch(fail); |
| 311 | }); |
| 312 | |
| 313 | // Links out, in the system's browser (src/bridge.js does the same on the site). |
| 314 | document.addEventListener("click", function (event) { |
| 315 | var anchor = event.target && event.target.closest ? event.target.closest("a[href]") : null; |
| 316 | if (!anchor) return; |
| 317 | event.preventDefault(); |
| 318 | invoke("open_url", { url: anchor.href }).catch(function () {}); |
| 319 | }); |
| 320 | |
| 321 | // g1t's own title bar, when the window has no system frame. |
| 322 | function frame(view) { |
| 323 | if (!view.customFrame) return; |
| 324 | if (view.platform === "macos") document.documentElement.classList.add("mac"); |
| 325 | var bar = byId("titlebar"); |
| 326 | bar.hidden = false; |
| 327 | document.body.classList.add("framed"); |
| 328 | bar.addEventListener("mousedown", function (event) { |
| 329 | if (event.button !== 0 || event.target.closest("button")) return; |
| 330 | invoke("window_control", { action: "drag" }).catch(function () {}); |
| 331 | }); |
| 332 | byId("close-window").addEventListener("click", function () { |
| 333 | invoke("window_control", { action: "close" }).catch(function () {}); |
| 334 | }); |
| 335 | } |
| 336 | |
| 337 | invoke("get_settings") |
| 338 | .then(function (view) { |
| 339 | frame(view); |
| 340 | platform(view.platform); |
| 341 | render(view); |
| 342 | // Opened by "Check for updates…" in a menu. |
| 343 | if (new URLSearchParams(location.search).has("check")) { |
| 344 | byId("updates-row").scrollIntoView({ block: "center" }); |
| 345 | check(); |
| 346 | } |
| 347 | }) |
| 348 | .catch(fail); |
| 349 | })(); |