fix typo
[web/firmware-selector-openwrt-org.git] / index.js
index 509ea439a383bf513ded31d64aa4a4058b76299d..fab5b878d14666fd2d64b08c295d831e89cbdfff 100644 (file)
--- a/index.js
+++ b/index.js
 
-function loadFile(url, callback) {
-  var xmlhttp = new XMLHttpRequest();
-  xmlhttp.onreadystatechange = function() {
-    if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
-      callback(JSON.parse(xmlhttp.responseText), url);
+var current_model = {};
+
+function $(id) {
+  return document.getElementById(id);
+}
+
+function show(id) {
+  $(id).style.display = 'block';
+}
+
+function hide(id) {
+  $(id).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();
     }
-  };
-  xmlhttp.open('GET', url, true);
-  xmlhttp.send();
+  }).join(' / ');
+}
+
+function get_version_url(version) {
+  if (version == 'SNAPSHOT') {
+    return config.base_url + '/snapshots/targets/{target}'
+  } else {
+    return config.base_url + '/releases/{version}/targets/{target}'.replace('{version}', version)
+  }
+}
+
+function build_asa_request() {
+  if (!current_model || !current_model.id) {
+    alert('bad profile');
+    return;
+  }
+
+  function showStatus(text) {
+    show('buildstatus');
+    $('buildstatus').innerHTML = text;
+    translate();
+  }
+
+  function handleError(response) {
+    hide('buildspinner');
+
+    response.json()
+      .then(mobj => {
+        var message = mobj['message'] || '<span class="tr-build-failed"></span>';
+        if (mobj.buildlog == true) {
+          var url = config.asu_url + '/store/' + mobj.bin_dir + '/buildlog.txt';
+          showStatus('<a href="' + url + '">' + message + '</a>');
+        } else {
+          showStatus(message);
+        }
+      });
+  }
+
+  // hide image view
+  updateImages();
+
+  show('buildspinner');
+  showStatus('<span class="tr-request-image"></span>');
+
+  var 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('<span class="tr-build-successful"></span>');
+
+        response.json()
+        .then(mobj => {
+          var download_url = config.asu_url + '/store/' + mobj.bin_dir;
+          updateImages(
+            mobj.version_number,
+            mobj.version_code,
+            mobj.build_at,
+            get_model_titles(mobj.titles),
+            download_url, mobj, true
+          );
+        });
+        break;
+      case 202:
+        showStatus('<span class="tr-check-again"></span>');
+        setTimeout(_ => { build_asa_request() }, 5000);
+        break;
+      case 400: // bad request
+      case 422: // bad package
+      case 500: // build failed
+        handleError(response);
+        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");
+    var option = document.createElement('OPTION');
     option.innerHTML = items[i];
     select.appendChild(option);
   }
 
-  select.addEventListener("change", function(e) {
+  select.addEventListener('change', e => {
     onselection(items[select.selectedIndex]);
   });
 
@@ -27,19 +132,15 @@ function setupSelectList(select, items, onselection) {
 }
 
 // Change the translation of the entire document
-function changeLanguage(language) {
-  var mapping = translations[language];
-  if (mapping) {
-    for (var tr in mapping) {
-      Array.from(document.getElementsByClassName(tr))
-      .forEach(function(e) { e.innerText = mapping[tr]; })
-    }
+function translate() {
+  var mapping = translations[config.language];
+  for (var 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:
+function setupAutocompleteList(input, items, as_list, onbegin, onend) {
   var currentFocus = -1;
 
   // sort numbers and other characters separately
@@ -47,12 +148,20 @@ function setupAutocompleteList(input, items, onselection) {
 
   items.sort(collator.compare);
 
-  // execute a function when someone writes in the text field:
   input.oninput = function(e) {
-    // clear images
-    updateImages();
+    onbegin();
 
+    var offset = 0;
     var value = this.value;
+    var 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();
 
@@ -61,13 +170,12 @@ function setupAutocompleteList(input, items, onselection) {
     }
 
     // create a DIV element that will contain the items (values):
-    var list = document.createElement("DIV");
-    list.setAttribute("id", this.id + "-autocomplete-list");
-    list.setAttribute("class", "autocomplete-items");
+    var 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];
@@ -78,28 +186,36 @@ function setupAutocompleteList(input, items, onselection) {
         continue;
       }
 
+      // do not offer a duplicate item
+      if (as_list && value_list.indexOf(item) != -1) {
+        continue;
+      }
+
       c += 1;
       if (c >= 15) {
-        var div = document.createElement("DIV");
-        div.innerHTML = "...";
+        var div = document.createElement('DIV');
+        div.innerHTML = '...';
         list.appendChild(div);
         break;
       } else {
-        var div = document.createElement("DIV");
+        var div = document.createElement('DIV');
         // make the matching letters bold:
         div.innerHTML = item.substr(0, j)
-          + "<strong>" + item.substr(j, value.length) + "</strong>"
+          + '<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;
+          + '<input type="hidden" value="' + item + '">';
+
+        div.addEventListener('click', function(e) {
+          // include selected value
+          var 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);
@@ -108,8 +224,8 @@ function setupAutocompleteList(input, items, onselection) {
   };
 
   input.onkeydown = function(e) {
-      var x = document.getElementById(this.id + "-autocomplete-list");
-      if (x) x = x.getElementsByTagName("div");
+      var x = document.getElementById(this.id + '-autocomplete-list');
+      if (x) x = x.getElementsByTagName('div');
       if (e.keyCode == 40) {
         // key down
         currentFocus += 1;
@@ -124,33 +240,38 @@ function setupAutocompleteList(input, items, onselection) {
         // 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:
+          // and simulate a click on the 'active' item:
           if (x) x[currentFocus].click();
         }
       }
   };
 
   input.onfocus = function() {
-    onselection(input.value);
+    onend(input);
+  }
+
+  // focus lost
+  input.onblur = function() {
+    onend(input);
   }
 
   function setActive(x) {
-    // a function to classify an item as "active":
+    // a function to classify an item as 'active':
     if (!x) return false;
-    // start by removing the "active" class on all items:
+    // start by removing the 'active' class on all items:
     for (var i = 0; i < x.length; i++) {
-      x[i].classList.remove("autocomplete-active");
+      x[i].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");
+    // add class 'autocomplete-active':
+    x[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");
+    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]);
@@ -159,149 +280,171 @@ function setupAutocompleteList(input, items, onselection) {
   }
 
   // execute a function when someone clicks in the document:
-  document.addEventListener("click", function (e) {
+  document.addEventListener('click', e => {
       closeAllLists(e.target);
   });
 }
 
-function $(id) {
-  return document.getElementById(id);
-}
-
-function findCommonPrefix(files) {
-    var A = files.concat().sort();
-    var first = A[0];
-    var last = A[A.length - 1];
-    var L = first.length;
-    var i = 0;
-    while (i < L && first.charAt(i) === last.charAt(i)) {
-      i += 1;
-    }
-    return first.substring(0, i);
+// for attended sysupgrade
+function updatePackageList(target) {
+  // set available packages
+  fetch(config.asu_url + '/' + target + '/packages.json')
+  .then(response => response.json())
+  .then(all_packages => {
+    setupAutocompleteList($('packages'), all_packages, true, _ => {}, textarea => {
+      textarea.value = split(textarea.value)
+        .filter((value, index, self) => self.indexOf(value) === index) // make list unique
+        //.filter((value, index) => all_packages.indexOf(value) !== -1) // limit to available packages
+        .join(' ');
+    });
+  });
 }
 
-function updateImages(version, commit, model, image_link, mobj) {
+function updateImages(version, code, date, model, url, mobj, is_custom) {
   // add download button for image
-  function addLink(label, tags, file, help_id) {
+  function addLink(type, file) {
     var a = document.createElement('A');
     a.classList.add('download-link');
-    a.href = image_link
-      .replace('%target', mobj.target)
-      .replace('%release', version)
-      .replace('%file', file);
+    a.href = url
+      .replace('{target}', mobj.target)
+      .replace('{version}', version)
+      + '/' + file;
     var span = document.createElement('SPAN');
     span.appendChild(document.createTextNode(''));
     a.appendChild(span);
-
-    // add sub label
-    if (tags.length > 0) {
-      a.appendChild(document.createTextNode(label + ' (' + tags.join(', ') + ')'));
-    } else {
-      a.appendChild(document.createTextNode(label));
-    }
+    a.appendChild(document.createTextNode(type.toUpperCase()));
 
     if (config.showHelp) {
       a.onmouseover = function() {
         // hide all help texts
         Array.from(document.getElementsByClassName('download-help'))
-          .forEach(function(e) { e.style.display = 'none'; });
-        $(help_id).style.display = 'block';
+          .forEach(e => e.style.display = 'none');
+        var 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(id, from_class, to_class) {
+    $(id).classList.remove(from_class);
+    $(id).classList.add(to_class);
+  }
+
   // remove all download links
   Array.from(document.getElementsByClassName('download-link'))
-    .forEach(function(e) { e.remove(); });
+    .forEach(e => e.remove());
 
   // hide all help texts
   Array.from(document.getElementsByClassName('download-help'))
-    .forEach(function(e) { e.style.display = 'none'; });
+    .forEach(e => e.style.display = 'none');
 
-  if (version && commit && model && image_link && mobj) {
+  if (version && code && date && model && url && mobj) {
     var target = mobj.target;
     var images = mobj.images;
 
+    // change between "version" and "custom" title
+    if (is_custom) {
+      switchClass('images-title', 'tr-version-build', 'tr-custom-build');
+      switchClass('downloads-title', 'tr-version-downloads', 'tr-custom-downloads');
+    } else {
+      switchClass('images-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-release').innerText = version;
-    $('image-commit').innerText = commit;
-
-    var prefix = findCommonPrefix(images);
-    var entries = {
-      'FACTORY': [],
-      'SYSUPGRADE': [],
-      'KERNEL': [],
-      'ROOTFS': [],
-      'SDCARD': [],
-      'TFTP': [],
-      'OTHER': []
-    };
-
-    images.sort();
+    $('image-version').innerText = version;
+    $('image-code').innerText = code;
+    $('image-date').innerText = date;
 
-    for (var i in images) {
-      var image = images[i];
-      var lc = image.toLowerCase()
-      if (lc.includes('factory')) {
-        entries['FACTORY'].push(image);
-      } else if (lc.includes('sysupgrade')) {
-        entries['SYSUPGRADE'].push(image);
-      } else if (lc.includes('kernel') || lc.includes('zimage') || lc.includes('uimage')) {
-        entries['KERNEL'].push(image);
-      } else if (lc.includes('rootfs')) {
-        entries['ROOTFS'].push(image);
-      } else if (lc.includes('sdcard')) {
-        entries['SDCARD'].push(image);
-      } else if (lc.includes('tftp')) {
-        entries['TFTP'].push(image);
-      } else {
-        entries['OTHER'].push(image);
-      }
-    }
+    images.sort((a, b) => a.name.localeCompare(b.name));
 
-    function extractTags(prefix, image) {
-      var all = image.substring(prefix.length).split('.')[0].split('-');
-      var ignore = ['', 'kernel', 'zImage', 'uImage', 'factory', 'sysupgrade', 'rootfs', 'sdcard'];
-      return all.filter(function (el) { return !ignore.includes(el); });
+    for (var i in images) {
+      addLink(images[i].type, images[i].name);
     }
 
-    for (var category in entries) {
-      var images = entries[category];
-      for (var i in images) {
-        var image = images[i];
-        var tags = (images.length > 1) ? extractTags(prefix, image) : [];
-        addLink(category, tags, image, category.toLowerCase() + '-help');
-      }
+    if (config.asu_url) {
+      updatePackageList(target);
     }
 
-    $('images').style.display = 'block';
+    show('images');
   } else {
-    $('images').style.display = 'none';
+    hide('images');
   }
 }
 
-// hide fields
-updateImages();
-changeLanguage(config.language);
-
-setupSelectList($("versions"), Object.keys(config.versions), function(version) {
-  loadFile(config.versions[version], function(obj) {
-    setupAutocompleteList($("models"), Object.keys(obj['models']), function(model) {
-      if (model in obj['models']) {
-        var link = obj.link;
-        var commit = obj.commit;
-        var mobj = obj['models'][model];
-        updateImages(version, commit, model, link, mobj);
-      } else {
-        updateImages();
-      }
-    });
+function init() {
+  setupSelectList($('versions'), Object.keys(config.versions), version => {
+    fetch(config.versions[version]).then(data => {
+      data.json().then(obj => {
+        // handle native openwrt json format
+        if ('profiles' in obj) {
+          obj['url'] = get_version_url(version)
+          obj['models'] = {}
+          for (const [key, value] of Object.entries(obj['profiles'])) {
+            obj['models'][get_model_titles(value.titles)] = value
+            obj['models'][get_model_titles(value.titles)]['id'] = key
+          }
+        }
+        return obj 
+      }).then(obj => {
+        setupAutocompleteList($('models'), Object.keys(obj['models']), false, updateImages, models => {
+          var model = models.value;
+          if (model in obj['models']) {
+            var url = obj.url || 'unknown';
+            var code = obj.version_code || 'unknown';
+            var date = obj.build_date || 'unknown';
+            var mobj = obj['models'][model];
+            updateImages(version, code, date, model, url, mobj, false);
+            current_model = mobj;
+          } else {
+            updateImages();
+            current_model = {};
+          }
+        });
 
-    // trigger model update when selected version changes
-    $("models").onfocus();
+        // trigger model update when selected version changes
+        $('models').onfocus();
+      });
+    });
   });
-});
\ No newline at end of file
+
+  if (config.asu_url) {
+    show('custom');
+  }
+
+  // hide fields
+  updateImages();
+
+  var 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();
+  }
+}