1 /* global translations, config */
2 /* exported build_asu_request, init */
4 let current_model
= {};
5 let url_params
= undefined;
8 if (typeof query
=== "string") {
9 return document
.querySelector(query
);
15 function show(query
) {
16 $(query
).style
.display
= "block";
19 function hide(query
) {
20 $(query
).style
.display
= "none";
24 return str
.match(/[^\s,]+/g) || [];
27 function get_model_titles(titles
) {
45 function build_asu_request() {
46 if (!current_model
|| !current_model
.id
) {
51 function showStatus(message
, url
) {
53 const tr
= message
.startsWith("tr-") ? message
: "";
55 $("#buildstatus").innerHTML
=
56 '<a href="' + url
+ '" class="' + tr
+ '">' + message
+ "</a>";
58 $("#buildstatus").innerHTML
= '<span class="' + tr
+ '"></span>';
66 show("#buildspinner");
67 showStatus("tr-request-image");
69 const request_data
= {
70 target
: current_model
.target
,
71 profile
: current_model
.id
,
72 packages
: split($("#packages").value
),
73 version
: $("#versions").value
,
76 fetch(config
.asu_url
+ "/api/build", {
78 headers
: { "Content-Type": "application/json" },
79 body
: JSON
.stringify(request_data
),
82 switch (response
.status
) {
84 hide("#buildspinner");
85 showStatus("tr-build-successful");
87 response
.json().then((mobj
) => {
88 const download_url
= config
.asu_url
+ "/store/" + mobj
.bin_dir
;
89 showStatus("tr-build-successful", download_url
+ "/buildlog.txt");
94 get_model_titles(mobj
.titles
),
102 showStatus("tr-check-again");
107 case 400: // bad request
108 case 422: // bad package
109 case 500: // build failed
110 hide("#buildspinner");
111 response
.json().then((mobj
) => {
112 const message
= mobj
["message"] || "tr-build-failed";
113 const url
= mobj
.buildlog
114 ? config
.asu_url
+ "/store/" + mobj
.bin_dir
+ "/buildlog.txt"
116 showStatus(message
, url
);
122 hide("#buildspinner");
127 function setupSelectList(select
, items
, onselection
) {
128 for (const item
of items
.sort().reverse()) {
129 const option
= document
.createElement("OPTION");
130 option
.innerHTML
= item
;
131 select
.appendChild(option
);
134 // pre-select version from URL or config.json
135 const preselect
= url_params
.get("version") || config
.default_version
;
137 $("#versions").value
= preselect
;
140 select
.addEventListener("change", () => {
141 onselection(items
[select
.selectedIndex
]);
144 if (select
.selectedIndex
>= 0) {
145 onselection(items
[select
.selectedIndex
]);
149 // Change the translation of the entire document
150 function translate() {
151 const mapping
= translations
[config
.language
];
152 for (const tr
in mapping
) {
153 Array
.from(document
.getElementsByClassName(tr
)).forEach((e
) => {
154 e
.innerText
= mapping
[tr
];
159 function setupAutocompleteList(input
, items
, as_list
, onbegin
, onend
) {
160 let currentFocus
= -1;
162 // sort numbers and other characters separately
163 const collator
= new Intl
.Collator(undefined, {
168 items
.sort(collator
.compare
);
170 input
.oninput = function () {
174 let value
= this.value
;
178 // automcomplete last text item
179 offset
= this.value
.lastIndexOf(" ") + 1;
180 value
= this.value
.substr(offset
);
181 value_list
= split(this.value
.substr(0, offset
));
184 // close any already open lists of autocompleted values
191 // create a DIV element that will contain the items (values):
192 const list
= document
.createElement("DIV");
193 list
.setAttribute("id", this.id
+ "-autocomplete-list");
194 list
.setAttribute("class", "autocomplete-items");
195 // append the DIV element as a child of the autocomplete container:
196 this.parentNode
.appendChild(list
);
198 function normalize(s
) {
199 return s
.toUpperCase().replace(/[-_.]/g, " ");
202 const match
= normalize(value
);
204 for (const item
of items
) {
206 let j
= normalize(item
).indexOf(match
);
211 // do not offer a duplicate item
212 if (as_list
&& value_list
.indexOf(item
) != -1) {
218 let div
= document
.createElement("DIV");
219 div
.innerHTML
= "...";
220 list
.appendChild(div
);
223 let div
= document
.createElement("DIV");
224 // make the matching letters bold:
228 item
.substr(j
, value
.length
) +
230 item
.substr(j
+ value
.length
) +
231 '<input type="hidden" value="' +
235 div
.addEventListener("click", function () {
236 // include selected value
237 const selected
= this.getElementsByTagName("input")[0].value
;
239 input
.value
= value_list
.join(" ") + " " + selected
;
241 input
.value
= selected
;
243 // close the list of autocompleted values,
248 list
.appendChild(div
);
253 input
.onkeydown = function (e
) {
254 let x
= document
.getElementById(this.id
+ "-autocomplete-list");
255 if (x
) x
= x
.getElementsByTagName("div");
256 if (e
.keyCode
== 40) {
259 // and and make the current item more visible:
261 } else if (e
.keyCode
== 38) {
264 // and and make the current item more visible:
266 } else if (e
.keyCode
== 13) {
267 // If the ENTER key is pressed, prevent the form from being submitted,
269 if (currentFocus
> -1) {
270 // and simulate a click on the 'active' item:
271 if (x
) x
[currentFocus
].click();
276 input
.onfocus = function () {
281 input
.onblur = function () {
285 function setActive(xs
) {
286 // a function to classify an item as 'active':
287 if (!xs
) return false;
288 // start by removing the 'active' class on all items:
289 for (const x
of xs
) {
290 x
.classList
.remove("autocomplete-active");
292 if (currentFocus
>= xs
.length
) currentFocus
= 0;
293 if (currentFocus
< 0) currentFocus
= xs
.length
- 1;
294 // add class 'autocomplete-active':
295 xs
[currentFocus
].classList
.add("autocomplete-active");
298 function closeAllLists(elmnt
) {
299 // close all autocomplete lists in the document,
300 // except the one passed as an argument:
301 const xs
= document
.getElementsByClassName("autocomplete-items");
302 for (const x
of xs
) {
303 if (elmnt
!= x
&& elmnt
!= input
) {
304 x
.parentNode
.removeChild(x
);
309 // execute a function when someone clicks in the document:
310 document
.addEventListener("click", (e
) => {
311 closeAllLists(e
.target
);
315 // for attended sysupgrade
316 function updatePackageList(version
, target
) {
317 // set available packages
321 config
.versions
[version
] +
326 .then((response
) => response
.json())
327 .then((all_packages
) => {
328 setupAutocompleteList(
334 textarea
.value
= split(textarea
.value
)
335 // make list unique, ignore minus
336 .filter((value
, index
, self
) => {
337 const i
= self
.indexOf(value
.replace(/^-/, ""));
338 return i
=== index
|| i
< 0;
340 // limit to available packages, ignore minus
342 (value
) => all_packages
.indexOf(value
.replace(/^-/, "")) !== -1
350 function updateImages(version
, code
, date
, model
, url
, mobj
, is_custom
) {
351 // add download button for image
352 function addLink(type
, file
) {
353 const a
= document
.createElement("A");
354 a
.classList
.add("download-link");
356 url
.replace("{target}", mobj
.target
).replace("{version}", version
) +
359 const span
= document
.createElement("SPAN");
360 span
.appendChild(document
.createTextNode(""));
362 a
.appendChild(document
.createTextNode(type
.toUpperCase()));
364 if (config
.showHelp
) {
365 a
.onmouseover = function () {
366 // hide all help texts
367 Array
.from(document
.getElementsByClassName("download-help")).forEach(
368 (e
) => (e
.style
.display
= "none")
370 const lc
= type
.toLowerCase();
371 if (lc
.includes("sysupgrade")) {
372 show("#sysupgrade-help");
373 } else if (lc
.includes("factory") || lc
== "trx" || lc
== "chk") {
374 show("#factory-help");
376 lc
.includes("kernel") ||
377 lc
.includes("zimage") ||
378 lc
.includes("uimage")
380 show("#kernel-help");
381 } else if (lc
.includes("root")) {
382 show("#rootfs-help");
383 } else if (lc
.includes("sdcard")) {
384 show("#sdcard-help");
385 } else if (lc
.includes("tftp")) {
393 $("#download-links").appendChild(a
);
396 function switchClass(query
, from_class
, to_class
) {
397 $(query
).classList
.remove(from_class
);
398 $(query
).classList
.add(to_class
);
401 // remove all download links
402 Array
.from(document
.getElementsByClassName("download-link")).forEach((e
) =>
406 // hide all help texts
407 Array
.from(document
.getElementsByClassName("download-help")).forEach(
408 (e
) => (e
.style
.display
= "none")
411 if (model
&& url
&& mobj
) {
412 const target
= mobj
.target
;
413 const images
= mobj
.images
;
415 // change between "version" and "custom" title
417 switchClass("#build-title", "tr-version-build", "tr-custom-build");
420 "tr-version-downloads",
421 "tr-custom-downloads"
424 switchClass("#build-title", "tr-custom-build", "tr-version-build");
427 "tr-custom-downloads",
428 "tr-version-downloads"
432 // update title translation
435 // fill out build info
436 $("#image-model").innerText
= model
;
437 $("#image-target").innerText
= target
;
438 $("#image-version").innerText
= version
;
439 $("#image-code").innerText
= mobj
["code"] || code
;
440 $("#image-date").innerText
= date
;
442 images
.sort((a
, b
) => a
.name
.localeCompare(b
.name
));
444 for (const i
in images
) {
445 addLink(images
[i
].type
, images
[i
].name
);
448 if (config
.asu_url
) {
449 updatePackageList(version
, target
);
452 // set current selection in URL
456 document
.location
.href
.split("?")[0] +
458 encodeURIComponent(version
) +
460 encodeURIComponent(mobj
["id"])
469 // Update model title in search box.
470 // Device id and model title might change between releases.
471 function setModel(obj
, id
, model
) {
473 for (const mobj
of Object
.values(obj
["models"])) {
474 if (mobj
["id"] == id
) {
475 $("#models").value
= mobj
["model"];
482 for (const mobj
of Object
.values(obj
["models"])) {
483 if (mobj
["model"].toLowerCase() == model
.toLowerCase()) {
484 $("#models").value
= mobj
["model"];
492 url_params
= new URLSearchParams(window
.location
.search
);
493 let build_date
= "unknown";
495 setupSelectList($("#versions"), Object
.keys(config
.versions
), (version
) => {
496 // A new version was selected
497 let url
= config
.versions
[version
];
498 if (config
.asu_url
) {
499 url
= config
.asu_url
+ "/" + url
+ "/profiles.json";
504 build_date
= obj
.headers
.get("last-modified");
508 // handle native openwrt json format
509 if ("profiles" in obj
) {
511 for (const [key
, value
] of Object
.entries(obj
["profiles"])) {
513 obj
["models"][get_model_titles(value
.titles
)] = value
;
517 // add key (title) to each model object
518 for (const [title
, mobj
] of Object
.entries(obj
["models"])) {
519 mobj
["model"] = title
;
525 setupAutocompleteList(
527 Object
.keys(obj
["models"]),
531 const model
= models
.value
;
532 if (model
in obj
["models"]) {
533 const url
= obj
.download_url
|| "unknown";
534 const code
= obj
.version_code
|| "unknown";
535 const mobj
= obj
["models"][model
];
536 updateImages(version
, code
, build_date
, model
, url
, mobj
, false);
537 current_model
= mobj
;
545 // set model when selected version changes
548 current_model
["id"] || url_params
.get("id"),
549 current_model
["model"] || url_params
.get("model")
552 // trigger update of current selected model
553 $("#models").onfocus();
557 if (config
.asu_url
) {
564 // default to browser language
565 const user_lang
= (navigator
.language
|| navigator
.userLanguage
).split(
568 if (user_lang
in translations
) {
569 config
.language
= user_lang
;
570 $("#language-selection").value
= user_lang
;
575 $("#language-selection").onclick = function () {
576 config
.language
= this.children
[this.selectedIndex
].value
;