+/* global translations, config */
+/* exported build_asu_request, init */
-function loadFile(url, callback) {
- var xmlhttp = new XMLHttpRequest();
- xmlhttp.onreadystatechange = function() {
- if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
- callback(xmlhttp.responseText, url);
+let current_model = {};
+let url_params = undefined;
+
+function $(query) {
+ if (typeof query === "string") {
+ return document.querySelector(query);
+ } else {
+ return query;
+ }
+}
+
+function show(query) {
+ $(query).style.display = "block";
+}
+
+function hide(query) {
+ $(query).style.display = "none";
+}
+
+function split(str) {
+ return str.match(/[^\s,]+/g) || [];
+}
+
+function get_model_titles(titles) {
+ return titles
+ .map((e) => {
+ if (e.title) {
+ return e.title;
+ } else {
+ return (
+ (e.vendor || "") +
+ " " +
+ (e.model || "") +
+ " " +
+ (e.variant || "")
+ ).trim();
+ }
+ })
+ .join(" / ");
+}
+
+function build_asu_request() {
+ if (!current_model || !current_model.id) {
+ alert("bad profile");
+ return;
+ }
+
+ function showStatus(message, url) {
+ show("#buildstatus");
+ const tr = message.startsWith("tr-") ? message : "";
+ if (url) {
+ $("#buildstatus").innerHTML =
+ '<a href="' + url + '" class="' + tr + '">' + message + "</a>";
+ } else {
+ $("#buildstatus").innerHTML = '<span class="' + tr + '"></span>';
}
+ translate();
+ }
+
+ // hide image view
+ updateImages();
+
+ show("#buildspinner");
+ showStatus("tr-request-image");
+
+ const request_data = {
+ target: current_model.target,
+ profile: current_model.id,
+ packages: split($("#packages").value),
+ version: $("#versions").value,
};
- xmlhttp.open('GET', url, true);
- xmlhttp.send();
+
+ fetch(config.asu_url + "/api/build", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(request_data),
+ })
+ .then((response) => {
+ switch (response.status) {
+ case 200:
+ hide("#buildspinner");
+ showStatus("tr-build-successful");
+
+ response.json().then((mobj) => {
+ const download_url = config.asu_url + "/store/" + mobj.bin_dir;
+ showStatus("tr-build-successful", download_url + "/buildlog.txt");
+ updateImages(
+ mobj.version_number,
+ mobj.version_code,
+ mobj.build_at,
+ get_model_titles(mobj.titles),
+ download_url,
+ mobj,
+ true
+ );
+ });
+ break;
+ case 202:
+ showStatus("tr-check-again");
+ setTimeout(() => {
+ build_asu_request();
+ }, 5000);
+ break;
+ case 400: // bad request
+ case 422: // bad package
+ case 500: // build failed
+ hide("#buildspinner");
+ response.json().then((mobj) => {
+ const message = mobj["message"] || "tr-build-failed";
+ const url = mobj.buildlog
+ ? config.asu_url + "/store/" + mobj.bin_dir + "/buildlog.txt"
+ : undefined;
+ showStatus(message, url);
+ });
+ break;
+ }
+ })
+ .catch((err) => {
+ hide("#buildspinner");
+ showStatus(err);
+ });
}
function setupSelectList(select, items, onselection) {
- for (var i = 0; i < items.length; i += 1) {
- var option = document.createElement("OPTION");
- option.innerHTML = items[i];
+ for (const item of items.sort().reverse()) {
+ const option = document.createElement("OPTION");
+ option.innerHTML = item;
select.appendChild(option);
}
- select.addEventListener("change", function(e) {
+ // pre-select version from URL or config.json
+ const preselect = url_params.get("version") || config.default_version;
+ if (preselect) {
+ $("#versions").value = preselect;
+ }
+
+ select.addEventListener("change", () => {
onselection(items[select.selectedIndex]);
});
}
// Change the translation of the entire document
-function changeLanguage(language) {
- var mapping = translations[language];
- if (mapping) {
- for (var id in mapping) {
- var elements = document.getElementsByClassName(id);
- for (var i in elements) {
- if (elements.hasOwnProperty(i)) {
- elements[i].innerHTML = mapping[id];
- }
- }
- }
+function translate() {
+ const mapping = translations[config.language];
+ for (const tr in mapping) {
+ Array.from(document.getElementsByClassName(tr)).forEach((e) => {
+ e.innerText = mapping[tr];
+ });
}
}
-function setupAutocompleteList(input, items, onselection) {
- // the setupAutocompleteList function takes two arguments,
- // the text field element and an array of possible autocompleted values:
- var currentFocus = -1;
+function setupAutocompleteList(input, items, as_list, onbegin, onend) {
+ let currentFocus = -1;
+
+ // sort numbers and other characters separately
+ const collator = new Intl.Collator(undefined, {
+ numeric: true,
+ sensitivity: "base",
+ });
+
+ items.sort(collator.compare);
- // execute a function when someone writes in the text field:
+ input.oninput = function () {
+ onbegin();
- input.oninput = function(e) {
- console.log("input");
- // clear images
- updateImages();
+ let offset = 0;
+ let value = this.value;
+ let value_list = [];
+
+ if (as_list) {
+ // automcomplete last text item
+ offset = this.value.lastIndexOf(" ") + 1;
+ value = this.value.substr(offset);
+ value_list = split(this.value.substr(0, offset));
+ }
- var value = this.value;
// close any already open lists of autocompleted values
closeAllLists();
- if (!value) { return false; }
+
+ if (!value) {
+ return false;
+ }
// create a DIV element that will contain the items (values):
- var list = document.createElement("DIV");
+ const list = document.createElement("DIV");
list.setAttribute("id", this.id + "-autocomplete-list");
list.setAttribute("class", "autocomplete-items");
// append the DIV element as a child of the autocomplete container:
this.parentNode.appendChild(list);
- // for each item in the array...
- var c = 0;
- for (var i = 0; i < items.length; i += 1) {
- var item = items[i];
+ function normalize(s) {
+ return s.toUpperCase().replace(/[-_.]/g, " ");
+ }
+ const match = normalize(value);
+ let c = 0;
+ for (const item of items) {
// match
- var j = item.toUpperCase().indexOf(value.toUpperCase());
+ let j = normalize(item).indexOf(match);
if (j < 0) {
continue;
}
+ // do not offer a duplicate item
+ if (as_list && value_list.indexOf(item) != -1) {
+ continue;
+ }
+
c += 1;
- if (c >= 10) {
- var div = document.createElement("DIV");
+ if (c >= 15) {
+ let div = document.createElement("DIV");
div.innerHTML = "...";
list.appendChild(div);
break;
} else {
- var div = document.createElement("DIV");
+ let div = document.createElement("DIV");
// make the matching letters bold:
- div.innerHTML = item.substr(0, j)
- + "<strong>" + item.substr(j, value.length) + "</strong>"
- + item.substr(j + value.length)
- + "<input type='hidden' value='" + item + "'>";
-
- div.addEventListener("click", function(e) {
- // set text field to selected value
- input.value = this.getElementsByTagName("input")[0].value;
+ div.innerHTML =
+ item.substr(0, j) +
+ "<strong>" +
+ item.substr(j, value.length) +
+ "</strong>" +
+ item.substr(j + value.length) +
+ '<input type="hidden" value="' +
+ item +
+ '">';
+
+ div.addEventListener("click", function () {
+ // include selected value
+ const selected = this.getElementsByTagName("input")[0].value;
+ if (as_list) {
+ input.value = value_list.join(" ") + " " + selected;
+ } else {
+ input.value = selected;
+ }
// close the list of autocompleted values,
- // (or any other open lists of autocompleted values:
closeAllLists();
- // callback
- onselection(input.value);
+ onend(input);
});
list.appendChild(div);
}
};
- input.onkeydown = function(e) {
- console.log("keydown " + e.keyCode);
- var x = document.getElementById(this.id + "-autocomplete-list");
- if (x) x = x.getElementsByTagName("div");
- if (e.keyCode == 40) {
- // key down
- currentFocus += 1;
- // and and make the current item more visible:
- setActive(x);
- } else if (e.keyCode == 38) {
- // key up
- currentFocus -= 1;
- // and and make the current item more visible:
- setActive(x);
- } else if (e.keyCode == 13) {
- // If the ENTER key is pressed, prevent the form from being submitted,
- e.preventDefault();
- if (currentFocus > -1) {
- // and simulate a click on the "active" item:
- if (x) x[currentFocus].click();
- }
+ input.onkeydown = function (e) {
+ let x = document.getElementById(this.id + "-autocomplete-list");
+ if (x) x = x.getElementsByTagName("div");
+ if (e.keyCode == 40) {
+ // key down
+ currentFocus += 1;
+ // and and make the current item more visible:
+ setActive(x);
+ } else if (e.keyCode == 38) {
+ // key up
+ currentFocus -= 1;
+ // and and make the current item more visible:
+ setActive(x);
+ } else if (e.keyCode == 13) {
+ // If the ENTER key is pressed, prevent the form from being submitted,
+ e.preventDefault();
+ if (currentFocus > -1) {
+ // and simulate a click on the 'active' item:
+ if (x) x[currentFocus].click();
}
+ }
};
- input.onfocus = function() {
- onselection(input.value);
- }
+ input.onfocus = function () {
+ onend(input);
+ };
+
+ // focus lost
+ input.onblur = function () {
+ onend(input);
+ };
- function setActive(x) {
- // a function to classify an item as "active":
- if (!x) return false;
- // start by removing the "active" class on all items:
- for (var i = 0; i < x.length; i++) {
- x[i].classList.remove("autocomplete-active");
+ function setActive(xs) {
+ // a function to classify an item as 'active':
+ if (!xs) return false;
+ // start by removing the 'active' class on all items:
+ for (const x of xs) {
+ x.classList.remove("autocomplete-active");
}
- if (currentFocus >= x.length) currentFocus = 0;
- if (currentFocus < 0) currentFocus = (x.length - 1);
- // add class "autocomplete-active":
- x[currentFocus].classList.add("autocomplete-active");
+ if (currentFocus >= xs.length) currentFocus = 0;
+ if (currentFocus < 0) currentFocus = xs.length - 1;
+ // add class 'autocomplete-active':
+ xs[currentFocus].classList.add("autocomplete-active");
}
function closeAllLists(elmnt) {
// close all autocomplete lists in the document,
// except the one passed as an argument:
- var x = document.getElementsByClassName("autocomplete-items");
- for (var i = 0; i < x.length; i++) {
- if (elmnt != x[i] && elmnt != input) {
- x[i].parentNode.removeChild(x[i]);
+ const xs = document.getElementsByClassName("autocomplete-items");
+ for (const x of xs) {
+ if (elmnt != x && elmnt != input) {
+ x.parentNode.removeChild(x);
}
}
}
// execute a function when someone clicks in the document:
- document.addEventListener("click", function (e) {
- closeAllLists(e.target);
+ document.addEventListener("click", (e) => {
+ closeAllLists(e.target);
});
}
-function $(id) {
- return document.getElementById(id);
+// for attended sysupgrade
+function updatePackageList(version, target) {
+ // set available packages
+ fetch(
+ config.asu_url +
+ "/" +
+ config.versions[version] +
+ "/" +
+ target +
+ "/index.json"
+ )
+ .then((response) => response.json())
+ .then((all_packages) => {
+ setupAutocompleteList(
+ $("#packages"),
+ all_packages,
+ true,
+ () => {},
+ (textarea) => {
+ textarea.value = split(textarea.value)
+ // make list unique, ignore minus
+ .filter((value, index, self) => {
+ const i = self.indexOf(value.replace(/^-/, ""));
+ return i === index || i < 0;
+ })
+ // limit to available packages, ignore minus
+ .filter(
+ (value) => all_packages.indexOf(value.replace(/^-/, "")) !== -1
+ )
+ .join(" ");
+ }
+ );
+ });
}
-function extractImageType(name) {
- var m = /-(sysupgrade|factory|rootfs|kernel|tftp)[-.]/.exec(name);
- return m ? m[1] : 'factory';
-}
+function updateImages(version, code, date, model, url, mobj, is_custom) {
+ // add download button for image
+ function addLink(type, file) {
+ const a = document.createElement("A");
+ a.classList.add("download-link");
+ a.href =
+ url.replace("{target}", mobj.target).replace("{version}", version) +
+ "/" +
+ file;
+ const span = document.createElement("SPAN");
+ span.appendChild(document.createTextNode(""));
+ a.appendChild(span);
+ a.appendChild(document.createTextNode(type.toUpperCase()));
+
+ if (config.showHelp) {
+ a.onmouseover = function () {
+ // hide all help texts
+ Array.from(document.getElementsByClassName("download-help")).forEach(
+ (e) => (e.style.display = "none")
+ );
+ const lc = type.toLowerCase();
+ if (lc.includes("sysupgrade")) {
+ show("#sysupgrade-help");
+ } else if (lc.includes("factory") || lc == "trx" || lc == "chk") {
+ show("#factory-help");
+ } else if (
+ lc.includes("kernel") ||
+ lc.includes("zimage") ||
+ lc.includes("uimage")
+ ) {
+ show("#kernel-help");
+ } else if (lc.includes("root")) {
+ show("#rootfs-help");
+ } else if (lc.includes("sdcard")) {
+ show("#sdcard-help");
+ } else if (lc.includes("tftp")) {
+ show("#tftp-help");
+ } else {
+ show("#other-help");
+ }
+ };
+ }
-function updateImages(model, target, release, commit, images) {
- if (model && target && release && commit && images) {
- $('image-model').innerText = model;
- $('image-target').innerText = target;
- $('image-release').innerText = release;
- $('image-commit').innerText = commit;
-
- for(var i in images) {
- var filename = images[i];
- var path = "https://" + target + "/" + filename;
- var type = extractImageType(filename);
-
- if (type == "sysupgrade") {
- $("sysupgrade-image").href = path;
- $("sysupgrade-image").style.display = "inline-flex";
- }
+ $("#download-links").appendChild(a);
+ }
- if (type == "factory") {
- $("factory-image").href = path;
- $("factory-image").style.display = "inline-flex";
- }
+ function switchClass(query, from_class, to_class) {
+ $(query).classList.remove(from_class);
+ $(query).classList.add(to_class);
+ }
- if (type == "tftp") {
- $("tftp-image").href = path;
- $("tftp-image").style.display = "inline-flex";
- }
+ // remove all download links
+ Array.from(document.getElementsByClassName("download-link")).forEach((e) =>
+ e.remove()
+ );
+
+ // hide all help texts
+ Array.from(document.getElementsByClassName("download-help")).forEach(
+ (e) => (e.style.display = "none")
+ );
+
+ if (model && url && mobj) {
+ const target = mobj.target;
+ const images = mobj.images;
+
+ // change between "version" and "custom" title
+ if (is_custom) {
+ switchClass("#build-title", "tr-version-build", "tr-custom-build");
+ switchClass(
+ "#downloads-title",
+ "tr-version-downloads",
+ "tr-custom-downloads"
+ );
+ } else {
+ switchClass("#build-title", "tr-custom-build", "tr-version-build");
+ switchClass(
+ "#downloads-title",
+ "tr-custom-downloads",
+ "tr-version-downloads"
+ );
+ }
- if (type == "kernel") {
- $("kernel-image").href = path;
- $("kernel-image").style.display = "inline-flex";
- }
+ // update title translation
+ translate();
- if (type == "rootfs") {
- $("rootfs-image").href = path;
- $("rootfs-image").style.display = "inline-flex";
- }
+ // fill out build info
+ $("#image-model").innerText = model;
+ $("#image-target").innerText = target;
+ $("#image-version").innerText = version;
+ $("#image-code").innerText = mobj["code"] || code;
+ $("#image-date").innerText = date;
+
+ images.sort((a, b) => a.name.localeCompare(b.name));
+
+ for (const i in images) {
+ addLink(images[i].type, images[i].name);
}
- $("images").style.display = 'block';
+
+ if (config.asu_url) {
+ updatePackageList(version, target);
+ }
+
+ // set current selection in URL
+ history.pushState(
+ null,
+ null,
+ document.location.href.split("?")[0] +
+ "?version=" +
+ encodeURIComponent(version) +
+ "&id=" +
+ encodeURIComponent(mobj["id"])
+ );
+
+ show("#images");
} else {
- $("images").style.display = 'none';
- $("sysupgrade-image").style.display = "none";
- $("factory-image").style.display = "none";
- $("tftp-image").style.display = "none";
- $("kernel-image").style.display = "none";
- $("rootfs-image").style.display = "none";
+ hide("#images");
}
}
-// hide fields
-updateImages();
-changeLanguage(config.language);
-
-function parseData(data) {
- var obj = JSON.parse(data);
- var out = {};
- for (var release in obj) {
- var entries = obj[release]['models'];
- var commit = obj[release]['commit']
- var models = {};
- for (var i = 0; i < entries.length; i += 1) {
- var entry = entries[i];
- var name = (entry[0] + " " + entry[1] + " " + entry[2]).trim();
- var target = entry[3];
- var images = entry[4];
- models[name] = {'name': name, 'target': target, 'commit': commit, 'images': images};
+// Update model title in search box.
+// Device id and model title might change between releases.
+function setModel(obj, id, model) {
+ if (id) {
+ for (const mobj of Object.values(obj["models"])) {
+ if (mobj["id"] == id) {
+ $("#models").value = mobj["model"];
+ return;
+ }
+ }
+ }
+
+ if (model) {
+ for (const mobj of Object.values(obj["models"])) {
+ if (mobj["model"].toLowerCase() == model.toLowerCase()) {
+ $("#models").value = mobj["model"];
+ return;
+ }
}
- out[release] = models;
}
- return out;
}
-loadFile(config.data, function(data) {
- var obj = parseData(data);
- setupSelectList($("releases"), Object.keys(obj), function(release) {
- console.log("release: " + release);
- setupAutocompleteList($("models"), Object.keys(obj[release]), function(model) {
- console.log("clicked " + model);
- if (model in obj[release]) {
- var target = obj[release][model].target;
- var commit = obj[release][model].commit;
- var images = obj[release][model].images;
- updateImages(model, target, release, commit, images);
- } else {
- updateImages();
+function init() {
+ url_params = new URLSearchParams(window.location.search);
+ let build_date = "unknown";
+
+ setupSelectList($("#versions"), Object.keys(config.versions), (version) => {
+ // A new version was selected
+ let url = config.versions[version];
+ if (config.asu_url) {
+ url = config.asu_url + "/" + url + "/profiles.json";
+ }
+
+ fetch(url)
+ .then((obj) => {
+ build_date = obj.headers.get("last-modified");
+ return obj.json();
+ })
+ .then((obj) => {
+ // handle native openwrt json format
+ if ("profiles" in obj) {
+ obj["models"] = {};
+ for (const [key, value] of Object.entries(obj["profiles"])) {
+ value["id"] = key;
+ obj["models"][get_model_titles(value.titles)] = value;
+ }
+ }
+
+ // add key (title) to each model object
+ for (const [title, mobj] of Object.entries(obj["models"])) {
+ mobj["model"] = title;
}
+
+ return obj;
+ })
+ .then((obj) => {
+ setupAutocompleteList(
+ $("#models"),
+ Object.keys(obj["models"]),
+ false,
+ updateImages,
+ (models) => {
+ const model = models.value;
+ if (model in obj["models"]) {
+ const url = obj.download_url || "unknown";
+ const code = obj.version_code || "unknown";
+ const mobj = obj["models"][model];
+ updateImages(version, code, build_date, model, url, mobj, false);
+ current_model = mobj;
+ } else {
+ updateImages();
+ current_model = {};
+ }
+ }
+ );
+
+ // set model when selected version changes
+ setModel(
+ obj,
+ current_model["id"] || url_params.get("id"),
+ current_model["model"] || url_params.get("model")
+ );
+
+ // trigger update of current selected model
+ $("#models").onfocus();
});
+ });
- // trigger model update when selected release changes
- $("models").onfocus();
- });
-})
+ if (config.asu_url) {
+ show("#custom");
+ }
+
+ // hide fields
+ updateImages();
+
+ // default to browser language
+ const user_lang = (navigator.language || navigator.userLanguage).split(
+ "-"
+ )[0];
+ if (user_lang in translations) {
+ config.language = user_lang;
+ $("#language-selection").value = user_lang;
+ }
+
+ translate();
+
+ $("#language-selection").onclick = function () {
+ config.language = this.children[this.selectedIndex].value;
+ translate();
+ };
+}