879038e2972f3080b9f7dfb336cf5ef04bce929b
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 (const item
of items
.sort().reverse()) {
128 const option
= document
.createElement("OPTION");
129 option
.innerHTML
= item
;
130 select
.appendChild(option
);
133 // pre-select version from config.json
134 const preselect
= config
.default_version
;
136 $("#versions").value
= preselect
;
139 select
.addEventListener("change", () => {
140 onselection(items
[select
.selectedIndex
]);
143 if (select
.selectedIndex
>= 0) {
144 onselection(items
[select
.selectedIndex
]);
148 // Change the translation of the entire document
149 function translate() {
150 const mapping
= translations
[config
.language
];
151 for (const tr
in mapping
) {
152 Array
.from(document
.getElementsByClassName(tr
)).forEach((e
) => {
153 e
.innerText
= mapping
[tr
];
158 function setupAutocompleteList(input
, items
, as_list
, onbegin
, onend
) {
159 let currentFocus
= -1;
161 // sort numbers and other characters separately
162 const collator
= new Intl
.Collator(undefined, {
167 items
.sort(collator
.compare
);
169 input
.oninput = function () {
173 let value
= this.value
;
177 // automcomplete last text item
178 offset
= this.value
.lastIndexOf(" ") + 1;
179 value
= this.value
.substr(offset
);
180 value_list
= split(this.value
.substr(0, offset
));
183 // close any already open lists of autocompleted values
190 // create a DIV element that will contain the items (values):
191 const list
= document
.createElement("DIV");
192 list
.setAttribute("id", this.id
+ "-autocomplete-list");
193 list
.setAttribute("class", "autocomplete-items");
194 // append the DIV element as a child of the autocomplete container:
195 this.parentNode
.appendChild(list
);
197 function normalize(s
) {
198 return s
.toUpperCase().replace(/[-_.]/g, " ");
201 const match
= normalize(value
);
203 for (const item
of items
) {
205 let j
= normalize(item
).indexOf(match
);
210 // do not offer a duplicate item
211 if (as_list
&& value_list
.indexOf(item
) != -1) {
217 let div
= document
.createElement("DIV");
218 div
.innerHTML
= "...";
219 list
.appendChild(div
);
222 let div
= document
.createElement("DIV");
223 // make the matching letters bold:
227 item
.substr(j
, value
.length
) +
229 item
.substr(j
+ value
.length
) +
230 '<input type="hidden" value="' +
234 div
.addEventListener("click", function () {
235 // include selected value
236 const selected
= this.getElementsByTagName("input")[0].value
;
238 input
.value
= value_list
.join(" ") + " " + selected
;
240 input
.value
= selected
;
242 // close the list of autocompleted values,
247 list
.appendChild(div
);
252 input
.onkeydown = function (e
) {
253 let x
= document
.getElementById(this.id
+ "-autocomplete-list");
254 if (x
) x
= x
.getElementsByTagName("div");
255 if (e
.keyCode
== 40) {
258 // and and make the current item more visible:
260 } else if (e
.keyCode
== 38) {
263 // and and make the current item more visible:
265 } else if (e
.keyCode
== 13) {
266 // If the ENTER key is pressed, prevent the form from being submitted,
268 if (currentFocus
> -1) {
269 // and simulate a click on the 'active' item:
270 if (x
) x
[currentFocus
].click();
275 input
.onfocus = function () {
280 input
.onblur = function () {
284 function setActive(xs
) {
285 // a function to classify an item as 'active':
286 if (!xs
) return false;
287 // start by removing the 'active' class on all items:
288 for (const x
of xs
) {
289 x
.classList
.remove("autocomplete-active");
291 if (currentFocus
>= xs
.length
) currentFocus
= 0;
292 if (currentFocus
< 0) currentFocus
= xs
.length
- 1;
293 // add class 'autocomplete-active':
294 xs
[currentFocus
].classList
.add("autocomplete-active");
297 function closeAllLists(elmnt
) {
298 // close all autocomplete lists in the document,
299 // except the one passed as an argument:
300 const xs
= document
.getElementsByClassName("autocomplete-items");
301 for (const x
of xs
) {
302 if (elmnt
!= x
&& elmnt
!= input
) {
303 x
.parentNode
.removeChild(x
);
308 // execute a function when someone clicks in the document:
309 document
.addEventListener("click", (e
) => {
310 closeAllLists(e
.target
);
314 // for attended sysupgrade
315 function updatePackageList(version
, target
) {
316 // set available packages
320 config
.versions
[version
] +
325 .then((response
) => response
.json())
326 .then((all_packages
) => {
327 setupAutocompleteList(
333 textarea
.value
= split(textarea
.value
)
334 // make list unique, ignore minus
335 .filter((value
, index
, self
) => {
336 const i
= self
.indexOf(value
.replace(/^-/, ""));
337 return i
=== index
|| i
< 0;
339 // limit to available packages, ignore minus
341 (value
) => all_packages
.indexOf(value
.replace(/^-/, "")) !== -1
349 function updateImages(version
, code
, date
, model
, url
, mobj
, is_custom
) {
350 // add download button for image
351 function addLink(type
, file
) {
352 const a
= document
.createElement("A");
353 a
.classList
.add("download-link");
355 url
.replace("{target}", mobj
.target
).replace("{version}", version
) +
358 const span
= document
.createElement("SPAN");
359 span
.appendChild(document
.createTextNode(""));
361 a
.appendChild(document
.createTextNode(type
.toUpperCase()));
363 if (config
.showHelp
) {
364 a
.onmouseover = function () {
365 // hide all help texts
366 Array
.from(document
.getElementsByClassName("download-help")).forEach(
367 (e
) => (e
.style
.display
= "none")
369 const lc
= type
.toLowerCase();
370 if (lc
.includes("sysupgrade")) {
371 show("#sysupgrade-help");
372 } else if (lc
.includes("factory") || lc
== "trx" || lc
== "chk") {
373 show("#factory-help");
375 lc
.includes("kernel") ||
376 lc
.includes("zimage") ||
377 lc
.includes("uimage")
379 show("#kernel-help");
380 } else if (lc
.includes("root")) {
381 show("#rootfs-help");
382 } else if (lc
.includes("sdcard")) {
383 show("#sdcard-help");
384 } else if (lc
.includes("tftp")) {
392 $("#download-links").appendChild(a
);
395 function switchClass(query
, from_class
, to_class
) {
396 $(query
).classList
.remove(from_class
);
397 $(query
).classList
.add(to_class
);
400 // remove all download links
401 Array
.from(document
.getElementsByClassName("download-link")).forEach((e
) =>
405 // hide all help texts
406 Array
.from(document
.getElementsByClassName("download-help")).forEach(
407 (e
) => (e
.style
.display
= "none")
410 if (model
&& url
&& mobj
) {
411 const target
= mobj
.target
;
412 const images
= mobj
.images
;
414 // change between "version" and "custom" title
416 switchClass("#build-title", "tr-version-build", "tr-custom-build");
419 "tr-version-downloads",
420 "tr-custom-downloads"
423 switchClass("#build-title", "tr-custom-build", "tr-version-build");
426 "tr-custom-downloads",
427 "tr-version-downloads"
431 // update title translation
434 // fill out build info
435 $("#image-model").innerText
= model
;
436 $("#image-target").innerText
= target
;
437 $("#image-version").innerText
= version
;
438 $("#image-code").innerText
= mobj
["code"] || code
;
439 $("#image-date").innerText
= date
;
441 images
.sort((a
, b
) => a
.name
.localeCompare(b
.name
));
443 for (const i
in images
) {
444 addLink(images
[i
].type
, images
[i
].name
);
447 if (config
.asu_url
) {
448 updatePackageList(version
, target
);
457 // Update model title in search box.
458 // Device id and model title might change between releases.
459 function setModel(obj
, id
, model
) {
461 for (const mobj
of Object
.values(obj
["models"])) {
462 if (mobj
["id"] == id
) {
463 $("#models").value
= mobj
["model"];
470 for (const mobj
of Object
.values(obj
["models"])) {
471 if (mobj
["model"].toLowerCase() == model
.toLowerCase()) {
472 $("#models").value
= mobj
["model"];
480 let build_date
= "unknown";
482 setupSelectList($("#versions"), Object
.keys(config
.versions
), (version
) => {
483 // A new version was selected
484 let url
= config
.versions
[version
];
485 if (config
.asu_url
) {
486 url
= config
.asu_url
+ "/" + url
+ "/profiles.json";
491 build_date
= obj
.headers
.get("last-modified");
495 // handle native openwrt json format
496 if ("profiles" in obj
) {
498 for (const [key
, value
] of Object
.entries(obj
["profiles"])) {
500 obj
["models"][get_model_titles(value
.titles
)] = value
;
504 // add key (title) to each model object
505 for (const [title
, mobj
] of Object
.entries(obj
["models"])) {
506 mobj
["model"] = title
;
512 setupAutocompleteList(
514 Object
.keys(obj
["models"]),
518 const model
= models
.value
;
519 if (model
in obj
["models"]) {
520 const url
= obj
.download_url
|| "unknown";
521 const code
= obj
.version_code
|| "unknown";
522 const mobj
= obj
["models"][model
];
523 updateImages(version
, code
, build_date
, model
, url
, mobj
, false);
524 current_model
= mobj
;
532 // set model when selected version changes
533 setModel(obj
, current_model
["id"], current_model
["model"]);
535 // trigger update of current selected model
536 $("#models").onfocus();
540 if (config
.asu_url
) {
547 // default to browser language
548 const user_lang
= (navigator
.language
|| navigator
.userLanguage
).split(
551 if (user_lang
in translations
) {
552 config
.language
= user_lang
;
553 $("#language-selection").value
= user_lang
;
558 $("#language-selection").onclick = function () {
559 config
.language
= this.children
[this.selectedIndex
].value
;