/* global translations, config */ /* exported build_asu_request, init */ 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 = '' + message + ""; } else { $("#buildstatus").innerHTML = ''; } 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, }; 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 (const item of items.sort().reverse()) { const option = document.createElement("OPTION"); option.innerHTML = item; select.appendChild(option); } // 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]); }); if (select.selectedIndex >= 0) { onselection(items[select.selectedIndex]); } } // Change the translation of the entire document 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, 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); input.oninput = function () { onbegin(); 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)); } // close any already open lists of autocompleted values closeAllLists(); if (!value) { return false; } // create a DIV element that will contain the items (values): 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); function normalize(s) { return s.toUpperCase().replace(/[-_.]/g, " "); } const match = normalize(value); let c = 0; for (const item of items) { // match 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 >= 15) { let div = document.createElement("DIV"); div.innerHTML = "..."; list.appendChild(div); break; } else { let div = document.createElement("DIV"); // make the matching letters bold: div.innerHTML = item.substr(0, j) + "" + item.substr(j, value.length) + "" + item.substr(j + value.length) + ''; 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, closeAllLists(); onend(input); }); list.appendChild(div); } } }; 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 () { onend(input); }; // focus lost input.onblur = function () { onend(input); }; 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 >= 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: 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", (e) => { closeAllLists(e.target); }); } // 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 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"); } }; } $("#download-links").appendChild(a); } function switchClass(query, from_class, to_class) { $(query).classList.remove(from_class); $(query).classList.add(to_class); } // 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" ); } // update title translation translate(); // 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); } 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 { hide("#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; } } } } 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(); }); }); 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(); }; }