1 /* global translations, config */
2 /* exported build_asu_request, init */
4 let current_model
= {};
7 if (typeof query
=== "string") {
8 return document
.querySelector(query
);
14 function show(query
) {
15 $(query
).style
.display
= "block";
18 function hide(query
) {
19 $(query
).style
.display
= "none";
23 return str
.match(/[^\s,]+/g) || [];
26 function get_model_titles(titles
) {
44 function build_asu_request() {
45 if (!current_model
|| !current_model
.id
) {
50 function showStatus(message
, url
) {
52 const tr
= message
.startsWith("tr-") ? message
: "";
54 $("#buildstatus").innerHTML
=
55 '<a href="' + url
+ '" class="' + tr
+ '">' + message
+ "</a>";
57 $("#buildstatus").innerHTML
= '<span class="' + tr
+ '"></span>';
65 show("#buildspinner");
66 showStatus("tr-request-image");
68 const request_data
= {
69 target
: current_model
.target
,
70 profile
: current_model
.id
,
71 packages
: split($("#packages").value
),
72 version
: $("#versions").value
,
75 fetch(config
.asu_url
+ "/api/build", {
77 headers
: { "Content-Type": "application/json" },
78 body
: JSON
.stringify(request_data
),
81 switch (response
.status
) {
83 hide("#buildspinner");
84 showStatus("tr-build-successful");
86 response
.json().then((mobj
) => {
87 const download_url
= config
.asu_url
+ "/store/" + mobj
.bin_dir
;
88 showStatus("tr-build-successful", download_url
+ "/buildlog.txt");
93 get_model_titles(mobj
.titles
),
101 showStatus("tr-check-again");
106 case 400: // bad request
107 case 422: // bad package
108 case 500: // build failed
109 hide("#buildspinner");
110 response
.json().then((mobj
) => {
111 const message
= mobj
["message"] || "tr-build-failed";
112 const url
= mobj
.buildlog
113 ? config
.asu_url
+ "/store/" + mobj
.bin_dir
+ "/buildlog.txt"
115 showStatus(message
, url
);
121 hide("#buildspinner");
126 function setupSelectList(select
, items
, onselection
) {
127 for (let i
= 0; i
< items
.length
; i
+= 1) {
128 const option
= document
.createElement("OPTION");
129 option
.innerHTML
= items
[i
];
130 select
.appendChild(option
);
133 select
.addEventListener("change", () => {
134 onselection(items
[select
.selectedIndex
]);
137 if (select
.selectedIndex
>= 0) {
138 onselection(items
[select
.selectedIndex
]);
142 // Change the translation of the entire document
143 function translate() {
144 const mapping
= translations
[config
.language
];
145 for (const tr
in mapping
) {
146 Array
.from(document
.getElementsByClassName(tr
)).forEach((e
) => {
147 e
.innerText
= mapping
[tr
];
152 function setupAutocompleteList(input
, items
, as_list
, onbegin
, onend
) {
153 let currentFocus
= -1;
155 // sort numbers and other characters separately
156 const collator
= new Intl
.Collator(undefined, {
161 items
.sort(collator
.compare
);
163 input
.oninput = function () {
167 let value
= this.value
;
171 // automcomplete last text item
172 offset
= this.value
.lastIndexOf(" ") + 1;
173 value
= this.value
.substr(offset
);
174 value_list
= split(this.value
.substr(0, offset
));
177 // close any already open lists of autocompleted values
184 // create a DIV element that will contain the items (values):
185 const list
= document
.createElement("DIV");
186 list
.setAttribute("id", this.id
+ "-autocomplete-list");
187 list
.setAttribute("class", "autocomplete-items");
188 // append the DIV element as a child of the autocomplete container:
189 this.parentNode
.appendChild(list
);
191 function normalize(s
) {
192 return s
.toUpperCase().replace(/[-_.]/g, " ");
195 const match
= normalize(value
);
197 for (let i
= 0; i
< items
.length
; i
+= 1) {
198 const item
= items
[i
];
201 let j
= normalize(item
).indexOf(match
);
206 // do not offer a duplicate item
207 if (as_list
&& value_list
.indexOf(item
) != -1) {
213 let div
= document
.createElement("DIV");
214 div
.innerHTML
= "...";
215 list
.appendChild(div
);
218 let div
= document
.createElement("DIV");
219 // make the matching letters bold:
223 item
.substr(j
, value
.length
) +
225 item
.substr(j
+ value
.length
) +
226 '<input type="hidden" value="' +
230 div
.addEventListener("click", function () {
231 // include selected value
232 const selected
= this.getElementsByTagName("input")[0].value
;
234 input
.value
= value_list
.join(" ") + " " + selected
;
236 input
.value
= selected
;
238 // close the list of autocompleted values,
243 list
.appendChild(div
);
248 input
.onkeydown = function (e
) {
249 let x
= document
.getElementById(this.id
+ "-autocomplete-list");
250 if (x
) x
= x
.getElementsByTagName("div");
251 if (e
.keyCode
== 40) {
254 // and and make the current item more visible:
256 } else if (e
.keyCode
== 38) {
259 // and and make the current item more visible:
261 } else if (e
.keyCode
== 13) {
262 // If the ENTER key is pressed, prevent the form from being submitted,
264 if (currentFocus
> -1) {
265 // and simulate a click on the 'active' item:
266 if (x
) x
[currentFocus
].click();
271 input
.onfocus = function () {
276 input
.onblur = function () {
280 function setActive(x
) {
281 // a function to classify an item as 'active':
282 if (!x
) return false;
283 // start by removing the 'active' class on all items:
284 for (let i
= 0; i
< x
.length
; i
++) {
285 x
[i
].classList
.remove("autocomplete-active");
287 if (currentFocus
>= x
.length
) currentFocus
= 0;
288 if (currentFocus
< 0) currentFocus
= x
.length
- 1;
289 // add class 'autocomplete-active':
290 x
[currentFocus
].classList
.add("autocomplete-active");
293 function closeAllLists(elmnt
) {
294 // close all autocomplete lists in the document,
295 // except the one passed as an argument:
296 const x
= document
.getElementsByClassName("autocomplete-items");
297 for (let i
= 0; i
< x
.length
; i
++) {
298 if (elmnt
!= x
[i
] && elmnt
!= input
) {
299 x
[i
].parentNode
.removeChild(x
[i
]);
304 // execute a function when someone clicks in the document:
305 document
.addEventListener("click", (e
) => {
306 closeAllLists(e
.target
);
310 // for attended sysupgrade
311 function updatePackageList(version
, target
) {
312 // set available packages
316 config
.versions
[version
] +
321 .then((response
) => response
.json())
322 .then((all_packages
) => {
323 setupAutocompleteList(
329 textarea
.value
= split(textarea
.value
)
330 // make list unique, ignore minus
331 .filter((value
, index
, self
) => {
332 const i
= self
.indexOf(value
.replace(/^-/, ""));
333 return i
=== index
|| i
< 0;
335 // limit to available packages, ignore minus
337 (value
) => all_packages
.indexOf(value
.replace(/^-/, "")) !== -1
345 function updateImages(version
, code
, date
, model
, url
, mobj
, is_custom
) {
346 // add download button for image
347 function addLink(type
, file
) {
348 const a
= document
.createElement("A");
349 a
.classList
.add("download-link");
351 url
.replace("{target}", mobj
.target
).replace("{version}", version
) +
354 const span
= document
.createElement("SPAN");
355 span
.appendChild(document
.createTextNode(""));
357 a
.appendChild(document
.createTextNode(type
.toUpperCase()));
359 if (config
.showHelp
) {
360 a
.onmouseover = function () {
361 // hide all help texts
362 Array
.from(document
.getElementsByClassName("download-help")).forEach(
363 (e
) => (e
.style
.display
= "none")
365 const lc
= type
.toLowerCase();
366 if (lc
.includes("sysupgrade")) {
367 show("#sysupgrade-help");
368 } else if (lc
.includes("factory") || lc
== "trx" || lc
== "chk") {
369 show("#factory-help");
371 lc
.includes("kernel") ||
372 lc
.includes("zimage") ||
373 lc
.includes("uimage")
375 show("#kernel-help");
376 } else if (lc
.includes("root")) {
377 show("#rootfs-help");
378 } else if (lc
.includes("sdcard")) {
379 show("#sdcard-help");
380 } else if (lc
.includes("tftp")) {
388 $("#download-links").appendChild(a
);
391 function switchClass(query
, from_class
, to_class
) {
392 $(query
).classList
.remove(from_class
);
393 $(query
).classList
.add(to_class
);
396 // remove all download links
397 Array
.from(document
.getElementsByClassName("download-link")).forEach((e
) =>
401 // hide all help texts
402 Array
.from(document
.getElementsByClassName("download-help")).forEach(
403 (e
) => (e
.style
.display
= "none")
406 if (model
&& url
&& mobj
) {
407 const target
= mobj
.target
;
408 const images
= mobj
.images
;
410 // change between "version" and "custom" title
412 switchClass("#build-title", "tr-version-build", "tr-custom-build");
415 "tr-version-downloads",
416 "tr-custom-downloads"
419 switchClass("#build-title", "tr-custom-build", "tr-version-build");
422 "tr-custom-downloads",
423 "tr-version-downloads"
427 // update title translation
430 // fill out build info
431 $("#image-model").innerText
= model
;
432 $("#image-target").innerText
= target
;
433 $("#image-version").innerText
= version
;
434 $("#image-code").innerText
= mobj
["code"] || code
;
435 $("#image-date").innerText
= date
;
437 images
.sort((a
, b
) => a
.name
.localeCompare(b
.name
));
439 for (const i
in images
) {
440 addLink(images
[i
].type
, images
[i
].name
);
443 if (config
.asu_url
) {
444 updatePackageList(version
, target
);
454 let build_date
= "unknown";
455 setupSelectList($("#versions"), Object
.keys(config
.versions
), (version
) => {
456 let url
= config
.versions
[version
];
457 if (config
.asu_url
) {
458 url
= config
.asu_url
+ "/" + url
+ "/profiles.json";
462 build_date
= obj
.headers
.get("last-modified");
466 // handle native openwrt json format
467 if ("profiles" in obj
) {
469 for (const [key
, value
] of Object
.entries(obj
["profiles"])) {
471 obj
["models"][get_model_titles(value
.titles
)] = value
;
477 setupAutocompleteList(
479 Object
.keys(obj
["models"]),
483 const model
= models
.value
;
484 if (model
in obj
["models"]) {
485 const url
= obj
.download_url
|| "unknown";
486 const code
= obj
.version_code
|| "unknown";
487 const mobj
= obj
["models"][model
];
488 updateImages(version
, code
, build_date
, model
, url
, mobj
, false);
489 current_model
= mobj
;
497 // trigger model update when selected version changes
498 $("#models").onfocus();
502 if (config
.asu_url
) {
509 // default to browser language
510 const user_lang
= (navigator
.language
|| navigator
.userLanguage
).split(
513 if (user_lang
in translations
) {
514 config
.language
= user_lang
;
515 $("#language-selection").value
= user_lang
;
520 $("#language-selection").onclick = function () {
521 config
.language
= this.children
[this.selectedIndex
].value
;