add dynamic image creation code & json refactor
[web/firmware-selector-openwrt-org.git] / index.js
1 function get_model_titles(titles) {
2 var title = []
3 for (var i in titles) {
4 if (titles[i].title) {
5 title.push(titles[i].title)
6 } else {
7 var title_fragments = []
8 if (titles[i].vendor) { title_fragments.push(titles[i].vendor) }
9 title_fragments.push(titles[i].model)
10 if (titles[i].variant) { title_fragments.push(titles[i].variant) }
11 title.push(title_fragments.join(" "))
12 }
13 }
14 return title.join("/")
15 }
16
17 function build_request(request_data) {
18 console.log("fire")
19 fetch(config.asu_url, {
20 method: "POST",
21 headers: { 'Content-Type': 'application/json' },
22 body: JSON.stringify(request_data)
23 })
24 .then(function(response) {
25 switch (response.status) {
26 case 200:
27 console.log("image found");
28 response.json()
29 .then(function(mobj) {
30 console.log(mobj)
31 updateImages(
32 mobj.version_number,
33 mobj.version_commit,
34 get_model_titles(mobj.titles),
35 mobj.url,
36 mobj)
37 });
38 break;
39 case 202:
40 // show some spinning animation
41 console.log("check again in 5 seconds");
42 setTimeout(function() { build_request(request_data) }, 5000);
43 break;
44 case 400:
45 console.log("bad request"); // see message
46 break;
47 case 422:
48 console.log("bad package"); // see message
49 break;
50 case 500:
51 console.log("build failed");
52 break;
53 }
54 })
55 }
56
57 function example_request() {
58 build_request({
59 "version": "SNAPSHOT",
60 "packages": ["bmon", "vim", "ip-full"],
61 "profile": "tplink_tl-wdr4300-v1"
62 })
63 }
64
65 function loadFile(url, callback) {
66 var xmlhttp = new XMLHttpRequest();
67 xmlhttp.onreadystatechange = function() {
68 if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
69 callback(JSON.parse(xmlhttp.responseText), url);
70 }
71 };
72 xmlhttp.open('GET', url, true);
73 xmlhttp.send();
74 }
75
76 function setupSelectList(select, items, onselection) {
77 for (var i = 0; i < items.length; i += 1) {
78 var option = document.createElement("OPTION");
79 option.innerHTML = items[i];
80 select.appendChild(option);
81 }
82
83 select.addEventListener("change", function(e) {
84 onselection(items[select.selectedIndex]);
85 });
86
87 if (select.selectedIndex >= 0) {
88 onselection(items[select.selectedIndex]);
89 }
90 }
91
92 // Change the translation of the entire document
93 function changeLanguage(language) {
94 var mapping = translations[language];
95 if (mapping) {
96 for (var tr in mapping) {
97 Array.from(document.getElementsByClassName(tr))
98 .forEach(function(e) { e.innerText = mapping[tr]; })
99 }
100 }
101 }
102
103 function setupAutocompleteList(input, items, onselection) {
104 // the setupAutocompleteList function takes two arguments,
105 // the text field element and an array of possible autocompleted values:
106 var currentFocus = -1;
107
108 // sort numbers and other characters separately
109 var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
110
111 items.sort(collator.compare);
112
113 // execute a function when someone writes in the text field:
114 input.oninput = function(e) {
115 // clear images
116 updateImages();
117
118 var value = this.value;
119 // close any already open lists of autocompleted values
120 closeAllLists();
121
122 if (!value) {
123 return false;
124 }
125
126 // create a DIV element that will contain the items (values):
127 var list = document.createElement("DIV");
128 list.setAttribute("id", this.id + "-autocomplete-list");
129 list.setAttribute("class", "autocomplete-items");
130 // append the DIV element as a child of the autocomplete container:
131 this.parentNode.appendChild(list);
132
133 // for each item in the array...
134 var c = 0;
135 for (var i = 0; i < items.length; i += 1) {
136 var item = items[i];
137
138 // match
139 var j = item.toUpperCase().indexOf(value.toUpperCase());
140 if (j < 0) {
141 continue;
142 }
143
144 c += 1;
145 if (c >= 15) {
146 var div = document.createElement("DIV");
147 div.innerHTML = "...";
148 list.appendChild(div);
149 break;
150 } else {
151 var div = document.createElement("DIV");
152 // make the matching letters bold:
153 div.innerHTML = item.substr(0, j)
154 + "<strong>" + item.substr(j, value.length) + "</strong>"
155 + item.substr(j + value.length)
156 + "<input type='hidden' value='" + item + "'>";
157
158 div.addEventListener("click", function(e) {
159 // set text field to selected value
160 input.value = this.getElementsByTagName("input")[0].value;
161 // close the list of autocompleted values,
162 // (or any other open lists of autocompleted values:
163 closeAllLists();
164 // callback
165 onselection(input.value);
166 });
167
168 list.appendChild(div);
169 }
170 }
171 };
172
173 input.onkeydown = function(e) {
174 var x = document.getElementById(this.id + "-autocomplete-list");
175 if (x) x = x.getElementsByTagName("div");
176 if (e.keyCode == 40) {
177 // key down
178 currentFocus += 1;
179 // and and make the current item more visible:
180 setActive(x);
181 } else if (e.keyCode == 38) {
182 // key up
183 currentFocus -= 1;
184 // and and make the current item more visible:
185 setActive(x);
186 } else if (e.keyCode == 13) {
187 // If the ENTER key is pressed, prevent the form from being submitted,
188 e.preventDefault();
189 if (currentFocus > -1) {
190 // and simulate a click on the "active" item:
191 if (x) x[currentFocus].click();
192 }
193 }
194 };
195
196 input.onfocus = function() {
197 onselection(input.value);
198 }
199
200 function setActive(x) {
201 // a function to classify an item as "active":
202 if (!x) return false;
203 // start by removing the "active" class on all items:
204 for (var i = 0; i < x.length; i++) {
205 x[i].classList.remove("autocomplete-active");
206 }
207 if (currentFocus >= x.length) currentFocus = 0;
208 if (currentFocus < 0) currentFocus = (x.length - 1);
209 // add class "autocomplete-active":
210 x[currentFocus].classList.add("autocomplete-active");
211 }
212
213 function closeAllLists(elmnt) {
214 // close all autocomplete lists in the document,
215 // except the one passed as an argument:
216 var x = document.getElementsByClassName("autocomplete-items");
217 for (var i = 0; i < x.length; i++) {
218 if (elmnt != x[i] && elmnt != input) {
219 x[i].parentNode.removeChild(x[i]);
220 }
221 }
222 }
223
224 // execute a function when someone clicks in the document:
225 document.addEventListener("click", function (e) {
226 closeAllLists(e.target);
227 });
228 }
229
230 function $(id) {
231 return document.getElementById(id);
232 }
233
234 function findCommonPrefix(images) {
235 var files = images.map(image => image.name)
236 var A = files.concat().sort();
237 var first = A[0];
238 var last = A[A.length - 1];
239 var L = first.length;
240 var i = 0;
241 while (i < L && first.charAt(i) === last.charAt(i)) {
242 i += 1;
243 }
244 return first.substring(0, i);
245 }
246
247 function updateImages(version, commit, model, url, mobj) {
248 // add download button for image
249 function addLink(label, tags, file, help_id) {
250 var a = document.createElement('A');
251 a.classList.add('download-link');
252 a.href = url
253 .replace('{target}', mobj.target)
254 .replace('{release}', version)
255 + '/' + file;
256 var span = document.createElement('SPAN');
257 span.appendChild(document.createTextNode(''));
258 a.appendChild(span);
259
260 // add sub label
261 if (tags.length > 0) {
262 a.appendChild(document.createTextNode(label + ' (' + tags.join(', ') + ')'));
263 } else {
264 a.appendChild(document.createTextNode(label));
265 }
266
267 if (config.showHelp) {
268 a.onmouseover = function() {
269 // hide all help texts
270 Array.from(document.getElementsByClassName('download-help'))
271 .forEach(function(e) { e.style.display = 'none'; });
272 $(help_id).style.display = 'block';
273 };
274 }
275
276 $('download-links').appendChild(a);
277 }
278
279 // remove all download links
280 Array.from(document.getElementsByClassName('download-link'))
281 .forEach(function(e) { e.remove(); });
282
283 // hide all help texts
284 Array.from(document.getElementsByClassName('download-help'))
285 .forEach(function(e) { e.style.display = 'none'; });
286
287 if (version && commit && model && url && mobj) {
288 var target = mobj.target;
289 var images = mobj.images;
290
291 // fill out build info
292 $('image-model').innerText = model;
293 $('image-target').innerText = target;
294 $('image-release').innerText = version;
295 $('image-commit').innerText = commit;
296
297 var prefix = findCommonPrefix(images);
298 var entries = {
299 'FACTORY': [],
300 'SYSUPGRADE': [],
301 'KERNEL': [],
302 'ROOTFS': [],
303 'SDCARD': [],
304 'TFTP': [],
305 'OTHER': []
306 };
307
308 images.sort();
309
310 for (var i in images) {
311 var image = images[i].name;
312 var lc = image.toLowerCase()
313 if (lc.includes('factory')) {
314 entries['FACTORY'].push(image);
315 } else if (lc.includes('sysupgrade')) {
316 entries['SYSUPGRADE'].push(image);
317 } else if (lc.includes('kernel') || lc.includes('zimage') || lc.includes('uimage')) {
318 entries['KERNEL'].push(image);
319 } else if (lc.includes('rootfs')) {
320 entries['ROOTFS'].push(image);
321 } else if (lc.includes('sdcard')) {
322 entries['SDCARD'].push(image);
323 } else if (lc.includes('tftp')) {
324 entries['TFTP'].push(image);
325 } else {
326 entries['OTHER'].push(image);
327 }
328 }
329
330 function extractTags(prefix, image) {
331 var all = image.substring(prefix.length).split('.')[0].split('-');
332 var ignore = ['', 'kernel', 'zImage', 'uImage', 'factory', 'sysupgrade', 'rootfs', 'sdcard'];
333 return all.filter(function (el) { return !ignore.includes(el); });
334 }
335
336 for (var category in entries) {
337 var images = entries[category];
338 for (var i in images) {
339 var image = images[i];
340 var tags = (images.length > 1) ? extractTags(prefix, image) : [];
341 addLink(category, tags, image, category.toLowerCase() + '-help');
342 }
343 }
344
345 $('images').style.display = 'block';
346 } else {
347 $('images').style.display = 'none';
348 }
349 }
350
351 // hide fields
352 updateImages();
353 changeLanguage(config.language);
354
355 setupSelectList($("releases"), Object.keys(config.versions), function(version) {
356 loadFile(config.versions[version], function(obj) {
357 setupAutocompleteList($("models"), Object.keys(obj['models']), function(model) {
358 if (model in obj['models']) {
359 var url = obj.url;
360 var commit = obj.version_commit;
361 var mobj = obj['models'][model];
362 updateImages(version, commit, model, url, mobj);
363 } else {
364 updateImages();
365 }
366 });
367
368 // trigger model update when selected version changes
369 $("models").onfocus();
370 });
371 });