1 var current_model
= {};
4 if (typeof query
=== "string") {
5 return document
.querySelector(query
);
11 function show(query
) {
12 $(query
).style
.display
= "block";
15 function hide(query
) {
16 $(query
).style
.display
= "none";
20 return str
.match(/[^\s,]+/g) || [];
23 function get_model_titles(titles
) {
41 function build_asu_request() {
42 if (!current_model
|| !current_model
.id
) {
47 function showStatus(message
, url
) {
49 var tr
= message
.startsWith("tr-") ? message
: "";
51 $("#buildstatus").innerHTML
=
52 '<a href="' + url
+ '" class="' + tr
+ '">' + message
+ "</a>";
54 $("#buildstatus").innerHTML
= '<span class="' + tr
+ '"></span>';
62 show("#buildspinner");
63 showStatus("tr-request-image");
66 target
: current_model
.target
,
67 profile
: current_model
.id
,
68 packages
: split($("#packages").value
),
69 version
: $("#versions").value
,
72 fetch(config
.asu_url
+ "/api/build", {
74 headers
: { "Content-Type": "application/json" },
75 body
: JSON
.stringify(request_data
),
78 switch (response
.status
) {
80 hide("#buildspinner");
81 showStatus("tr-build-successful");
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");
90 get_model_titles(mobj
.titles
),
98 showStatus("tr-check-again");
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"
112 showStatus(message
, url
);
118 hide("#buildspinner");
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
);
130 select
.addEventListener("change", (e
) => {
131 onselection(items
[select
.selectedIndex
]);
134 if (select
.selectedIndex
>= 0) {
135 onselection(items
[select
.selectedIndex
]);
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
];
149 function setupAutocompleteList(input
, items
, as_list
, onbegin
, onend
) {
150 var currentFocus
= -1;
152 // sort numbers and other characters separately
153 var collator
= new Intl
.Collator(undefined, {
158 items
.sort(collator
.compare
);
160 input
.oninput = function (e
) {
164 var value
= this.value
;
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
));
174 // close any already open lists of autocompleted values
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
);
188 function normalize(s
) {
189 return s
.toUpperCase().replace(/[-_.]/g, " ");
192 var match
= normalize(value
);
194 for (var i
= 0; i
< items
.length
; i
+= 1) {
198 var j
= normalize(item
).indexOf(match
);
203 // do not offer a duplicate item
204 if (as_list
&& value_list
.indexOf(item
) != -1) {
210 var div
= document
.createElement("DIV");
211 div
.innerHTML
= "...";
212 list
.appendChild(div
);
215 var div
= document
.createElement("DIV");
216 // make the matching letters bold:
220 item
.substr(j
, value
.length
) +
222 item
.substr(j
+ value
.length
) +
223 '<input type="hidden" value="' +
227 div
.addEventListener("click", function (e
) {
228 // include selected value
229 var selected
= this.getElementsByTagName("input")[0].value
;
231 input
.value
= value_list
.join(" ") + " " + selected
;
233 input
.value
= selected
;
235 // close the list of autocompleted values,
240 list
.appendChild(div
);
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) {
251 // and and make the current item more visible:
253 } else if (e
.keyCode
== 38) {
256 // and and make the current item more visible:
258 } else if (e
.keyCode
== 13) {
259 // If the ENTER key is pressed, prevent the form from being submitted,
261 if (currentFocus
> -1) {
262 // and simulate a click on the 'active' item:
263 if (x
) x
[currentFocus
].click();
268 input
.onfocus = function () {
273 input
.onblur = function () {
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");
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");
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
]);
301 // execute a function when someone clicks in the document:
302 document
.addEventListener("click", (e
) => {
303 closeAllLists(e
.target
);
307 // for attended sysupgrade
308 function updatePackageList(version
, target
) {
309 // set available packages
313 config
.versions
[version
] +
318 .then((response
) => response
.json())
319 .then((all_packages
) => {
320 setupAutocompleteList(
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;
332 // limit to available packages, ignore minus
335 all_packages
.indexOf(value
.replace(/^\-/, "")) !== -1
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");
349 url
.replace("{target}", mobj
.target
).replace("{version}", version
) +
352 var span
= document
.createElement("SPAN");
353 span
.appendChild(document
.createTextNode(""));
355 a
.appendChild(document
.createTextNode(type
.toUpperCase()));
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")
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");
369 lc
.includes("kernel") ||
370 lc
.includes("zimage") ||
371 lc
.includes("uimage")
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")) {
386 $("#download-links").appendChild(a
);
389 function switchClass(query
, from_class
, to_class
) {
390 $(query
).classList
.remove(from_class
);
391 $(query
).classList
.add(to_class
);
394 // remove all download links
395 Array
.from(document
.getElementsByClassName("download-link")).forEach((e
) =>
399 // hide all help texts
400 Array
.from(document
.getElementsByClassName("download-help")).forEach(
401 (e
) => (e
.style
.display
= "none")
404 if (model
&& url
&& mobj
) {
405 var target
= mobj
.target
;
406 var images
= mobj
.images
;
408 // change between "version" and "custom" title
410 switchClass("#build-title", "tr-version-build", "tr-custom-build");
413 "tr-version-downloads",
414 "tr-custom-downloads"
417 switchClass("#build-title", "tr-custom-build", "tr-version-build");
420 "tr-custom-downloads",
421 "tr-version-downloads"
425 // update title translation
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
;
435 images
.sort((a
, b
) => a
.name
.localeCompare(b
.name
));
437 for (var i
in images
) {
438 addLink(images
[i
].type
, images
[i
].name
);
441 if (config
.asu_url
) {
442 updatePackageList(version
, target
);
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";
460 build_date
= obj
.headers
.get("last-modified");
464 // handle native openwrt json format
465 if ("profiles" in obj
) {
467 for (const [key
, value
] of Object
.entries(obj
["profiles"])) {
469 obj
["models"][get_model_titles(value
.titles
)] = value
;
475 setupAutocompleteList(
477 Object
.keys(obj
["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
;
495 // trigger model update when selected version changes
496 $("#models").onfocus();
500 if (config
.asu_url
) {
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
;
516 $("#language-selection").onclick = function () {
517 config
.language
= this.children
[this.selectedIndex
].value
;