2f9afd660d93446239bec7682c4ad47dae309630
[web/firmware-selector-openwrt-org.git] / www / index.js
1 var current_model = {};
2
3 function $(query) {
4 if (typeof query === "string") {
5 return document.querySelector(query);
6 } else {
7 return query;
8 }
9 }
10
11 function show(query) {
12 $(query).style.display = "block";
13 }
14
15 function hide(query) {
16 $(query).style.display = "none";
17 }
18
19 function split(str) {
20 return str.match(/[^\s,]+/g) || [];
21 }
22
23 function get_model_titles(titles) {
24 return titles
25 .map((e) => {
26 if (e.title) {
27 return e.title;
28 } else {
29 return (
30 (e.vendor || "") +
31 " " +
32 (e.model || "") +
33 " " +
34 (e.variant || "")
35 ).trim();
36 }
37 })
38 .join(" / ");
39 }
40
41 function build_asu_request() {
42 if (!current_model || !current_model.id) {
43 alert("bad profile");
44 return;
45 }
46
47 function showStatus(message, url) {
48 show("#buildstatus");
49 var tr = message.startsWith("tr-") ? message : "";
50 if (url) {
51 $("#buildstatus").innerHTML =
52 '<a href="' + url + '" class="' + tr + '">' + message + "</a>";
53 } else {
54 $("#buildstatus").innerHTML = '<span class="' + tr + '"></span>';
55 }
56 translate();
57 }
58
59 // hide image view
60 updateImages();
61
62 show("#buildspinner");
63 showStatus("tr-request-image");
64
65 var request_data = {
66 target: current_model.target,
67 profile: current_model.id,
68 packages: split($("#packages").value),
69 version: $("#versions").value,
70 };
71
72 fetch(config.asu_url + "/api/build", {
73 method: "POST",
74 headers: { "Content-Type": "application/json" },
75 body: JSON.stringify(request_data),
76 })
77 .then((response) => {
78 switch (response.status) {
79 case 200:
80 hide("#buildspinner");
81 showStatus("tr-build-successful");
82
83 response.json().then((mobj) => {
84 var download_url = config.asu_url + "/store/" + mobj.bin_dir;
85 showStatus("tr-build-successful", download_url + "/buildlog.txt");
86 updateImages(
87 mobj.version_number,
88 mobj.version_code,
89 mobj.build_at,
90 get_model_titles(mobj.titles),
91 download_url,
92 mobj,
93 true
94 );
95 });
96 break;
97 case 202:
98 showStatus("tr-check-again");
99 setTimeout((_) => {
100 build_asu_request();
101 }, 5000);
102 break;
103 case 400: // bad request
104 case 422: // bad package
105 case 500: // build failed
106 hide("#buildspinner");
107 response.json().then((mobj) => {
108 var message = mobj["message"] || "tr-build-failed";
109 var url = mobj.buildlog
110 ? config.asu_url + "/store/" + mobj.bin_dir + "/buildlog.txt"
111 : undefined;
112 showStatus(message, url);
113 });
114 break;
115 }
116 })
117 .catch((err) => {
118 hide("#buildspinner");
119 showStatus(err);
120 });
121 }
122
123 function setupSelectList(select, items, onselection) {
124 for (var i = 0; i < items.length; i += 1) {
125 var option = document.createElement("OPTION");
126 option.innerHTML = items[i];
127 select.appendChild(option);
128 }
129
130 select.addEventListener("change", (e) => {
131 onselection(items[select.selectedIndex]);
132 });
133
134 if (select.selectedIndex >= 0) {
135 onselection(items[select.selectedIndex]);
136 }
137 }
138
139 // Change the translation of the entire document
140 function translate() {
141 var mapping = translations[config.language];
142 for (var tr in mapping) {
143 Array.from(document.getElementsByClassName(tr)).forEach((e) => {
144 e.innerText = mapping[tr];
145 });
146 }
147 }
148
149 function setupAutocompleteList(input, items, as_list, onbegin, onend) {
150 var currentFocus = -1;
151
152 // sort numbers and other characters separately
153 var collator = new Intl.Collator(undefined, {
154 numeric: true,
155 sensitivity: "base",
156 });
157
158 items.sort(collator.compare);
159
160 input.oninput = function (e) {
161 onbegin();
162
163 var offset = 0;
164 var value = this.value;
165 var value_list = [];
166
167 if (as_list) {
168 // automcomplete last text item
169 offset = this.value.lastIndexOf(" ") + 1;
170 value = this.value.substr(offset);
171 value_list = split(this.value.substr(0, offset));
172 }
173
174 // close any already open lists of autocompleted values
175 closeAllLists();
176
177 if (!value) {
178 return false;
179 }
180
181 // create a DIV element that will contain the items (values):
182 var list = document.createElement("DIV");
183 list.setAttribute("id", this.id + "-autocomplete-list");
184 list.setAttribute("class", "autocomplete-items");
185 // append the DIV element as a child of the autocomplete container:
186 this.parentNode.appendChild(list);
187
188 function normalize(s) {
189 return s.toUpperCase().replace(/[-_.]/g, " ");
190 }
191
192 var match = normalize(value);
193 var c = 0;
194 for (var i = 0; i < items.length; i += 1) {
195 var item = items[i];
196
197 // match
198 var j = normalize(item).indexOf(match);
199 if (j < 0) {
200 continue;
201 }
202
203 // do not offer a duplicate item
204 if (as_list && value_list.indexOf(item) != -1) {
205 continue;
206 }
207
208 c += 1;
209 if (c >= 15) {
210 var div = document.createElement("DIV");
211 div.innerHTML = "...";
212 list.appendChild(div);
213 break;
214 } else {
215 var div = document.createElement("DIV");
216 // make the matching letters bold:
217 div.innerHTML =
218 item.substr(0, j) +
219 "<strong>" +
220 item.substr(j, value.length) +
221 "</strong>" +
222 item.substr(j + value.length) +
223 '<input type="hidden" value="' +
224 item +
225 '">';
226
227 div.addEventListener("click", function (e) {
228 // include selected value
229 var selected = this.getElementsByTagName("input")[0].value;
230 if (as_list) {
231 input.value = value_list.join(" ") + " " + selected;
232 } else {
233 input.value = selected;
234 }
235 // close the list of autocompleted values,
236 closeAllLists();
237 onend(input);
238 });
239
240 list.appendChild(div);
241 }
242 }
243 };
244
245 input.onkeydown = function (e) {
246 var x = document.getElementById(this.id + "-autocomplete-list");
247 if (x) x = x.getElementsByTagName("div");
248 if (e.keyCode == 40) {
249 // key down
250 currentFocus += 1;
251 // and and make the current item more visible:
252 setActive(x);
253 } else if (e.keyCode == 38) {
254 // key up
255 currentFocus -= 1;
256 // and and make the current item more visible:
257 setActive(x);
258 } else if (e.keyCode == 13) {
259 // If the ENTER key is pressed, prevent the form from being submitted,
260 e.preventDefault();
261 if (currentFocus > -1) {
262 // and simulate a click on the 'active' item:
263 if (x) x[currentFocus].click();
264 }
265 }
266 };
267
268 input.onfocus = function () {
269 onend(input);
270 };
271
272 // focus lost
273 input.onblur = function () {
274 onend(input);
275 };
276
277 function setActive(x) {
278 // a function to classify an item as 'active':
279 if (!x) return false;
280 // start by removing the 'active' class on all items:
281 for (var i = 0; i < x.length; i++) {
282 x[i].classList.remove("autocomplete-active");
283 }
284 if (currentFocus >= x.length) currentFocus = 0;
285 if (currentFocus < 0) currentFocus = x.length - 1;
286 // add class 'autocomplete-active':
287 x[currentFocus].classList.add("autocomplete-active");
288 }
289
290 function closeAllLists(elmnt) {
291 // close all autocomplete lists in the document,
292 // except the one passed as an argument:
293 var x = document.getElementsByClassName("autocomplete-items");
294 for (var i = 0; i < x.length; i++) {
295 if (elmnt != x[i] && elmnt != input) {
296 x[i].parentNode.removeChild(x[i]);
297 }
298 }
299 }
300
301 // execute a function when someone clicks in the document:
302 document.addEventListener("click", (e) => {
303 closeAllLists(e.target);
304 });
305 }
306
307 // for attended sysupgrade
308 function updatePackageList(version, target) {
309 // set available packages
310 fetch(
311 config.asu_url +
312 "/" +
313 config.versions[version] +
314 "/" +
315 target +
316 "/index.json"
317 )
318 .then((response) => response.json())
319 .then((all_packages) => {
320 setupAutocompleteList(
321 $("#packages"),
322 all_packages,
323 true,
324 (_) => {},
325 (textarea) => {
326 textarea.value = split(textarea.value)
327 // make list unique, ignore minus
328 .filter((value, index, self) => {
329 var i = self.indexOf(value.replace(/^\-/, ""));
330 return i === index || i < 0;
331 })
332 // limit to available packages, ignore minus
333 .filter(
334 (value, index) =>
335 all_packages.indexOf(value.replace(/^\-/, "")) !== -1
336 )
337 .join(" ");
338 }
339 );
340 });
341 }
342
343 function updateImages(version, code, date, model, url, mobj, is_custom) {
344 // add download button for image
345 function addLink(type, file) {
346 var a = document.createElement("A");
347 a.classList.add("download-link");
348 a.href =
349 url.replace("{target}", mobj.target).replace("{version}", version) +
350 "/" +
351 file;
352 var span = document.createElement("SPAN");
353 span.appendChild(document.createTextNode(""));
354 a.appendChild(span);
355 a.appendChild(document.createTextNode(type.toUpperCase()));
356
357 if (config.showHelp) {
358 a.onmouseover = function () {
359 // hide all help texts
360 Array.from(document.getElementsByClassName("download-help")).forEach(
361 (e) => (e.style.display = "none")
362 );
363 var lc = type.toLowerCase();
364 if (lc.includes("sysupgrade")) {
365 show("#sysupgrade-help");
366 } else if (lc.includes("factory") || lc == "trx" || lc == "chk") {
367 show("#factory-help");
368 } else if (
369 lc.includes("kernel") ||
370 lc.includes("zimage") ||
371 lc.includes("uimage")
372 ) {
373 show("#kernel-help");
374 } else if (lc.includes("root")) {
375 show("#rootfs-help");
376 } else if (lc.includes("sdcard")) {
377 show("#sdcard-help");
378 } else if (lc.includes("tftp")) {
379 show("#tftp-help");
380 } else {
381 show("#other-help");
382 }
383 };
384 }
385
386 $("#download-links").appendChild(a);
387 }
388
389 function switchClass(query, from_class, to_class) {
390 $(query).classList.remove(from_class);
391 $(query).classList.add(to_class);
392 }
393
394 // remove all download links
395 Array.from(document.getElementsByClassName("download-link")).forEach((e) =>
396 e.remove()
397 );
398
399 // hide all help texts
400 Array.from(document.getElementsByClassName("download-help")).forEach(
401 (e) => (e.style.display = "none")
402 );
403
404 if (model && url && mobj) {
405 var target = mobj.target;
406 var images = mobj.images;
407
408 // change between "version" and "custom" title
409 if (is_custom) {
410 switchClass("#build-title", "tr-version-build", "tr-custom-build");
411 switchClass(
412 "#downloads-title",
413 "tr-version-downloads",
414 "tr-custom-downloads"
415 );
416 } else {
417 switchClass("#build-title", "tr-custom-build", "tr-version-build");
418 switchClass(
419 "#downloads-title",
420 "tr-custom-downloads",
421 "tr-version-downloads"
422 );
423 }
424
425 // update title translation
426 translate();
427
428 // fill out build info
429 $("#image-model").innerText = model;
430 $("#image-target").innerText = target;
431 $("#image-version").innerText = version;
432 $("#image-code").innerText = mobj["code"] || code;
433 $("#image-date").innerText = date;
434
435 images.sort((a, b) => a.name.localeCompare(b.name));
436
437 for (var i in images) {
438 addLink(images[i].type, images[i].name);
439 }
440
441 if (config.asu_url) {
442 updatePackageList(version, target);
443 }
444
445 show("#images");
446 } else {
447 hide("#images");
448 }
449 }
450
451 function init() {
452 var build_date = "unknown";
453 setupSelectList($("#versions"), Object.keys(config.versions), (version) => {
454 var url = config.versions[version];
455 if (config.asu_url) {
456 url = config.asu_url + "/" + url + "/profiles.json";
457 }
458 fetch(url)
459 .then((obj) => {
460 build_date = obj.headers.get("last-modified");
461 return obj.json();
462 })
463 .then((obj) => {
464 // handle native openwrt json format
465 if ("profiles" in obj) {
466 obj["models"] = {};
467 for (const [key, value] of Object.entries(obj["profiles"])) {
468 value["id"] = key;
469 obj["models"][get_model_titles(value.titles)] = value;
470 }
471 }
472 return obj;
473 })
474 .then((obj) => {
475 setupAutocompleteList(
476 $("#models"),
477 Object.keys(obj["models"]),
478 false,
479 updateImages,
480 (models) => {
481 var model = models.value;
482 if (model in obj["models"]) {
483 var url = obj.download_url || "unknown";
484 var code = obj.version_code || "unknown";
485 var mobj = obj["models"][model];
486 updateImages(version, code, build_date, model, url, mobj, false);
487 current_model = mobj;
488 } else {
489 updateImages();
490 current_model = {};
491 }
492 }
493 );
494
495 // trigger model update when selected version changes
496 $("#models").onfocus();
497 });
498 });
499
500 if (config.asu_url) {
501 show("#custom");
502 }
503
504 // hide fields
505 updateImages();
506
507 // default to browser language
508 var user_lang = (navigator.language || navigator.userLanguage).split("-")[0];
509 if (user_lang in translations) {
510 config.language = user_lang;
511 $("#language-selection").value = user_lang;
512 }
513
514 translate();
515
516 $("#language-selection").onclick = function () {
517 config.language = this.children[this.selectedIndex].value;
518 translate();
519 };
520 }