fab5b878d14666fd2d64b08c295d831e89cbdfff
[web/firmware-selector-openwrt-org.git] / index.js
1
2 var current_model = {};
3
4 function $(id) {
5 return document.getElementById(id);
6 }
7
8 function show(id) {
9 $(id).style.display = 'block';
10 }
11
12 function hide(id) {
13 $(id).style.display = 'none';
14 }
15
16 function split(str) {
17 return str.match(/[^\s,]+/g) || [];
18 }
19
20 function get_model_titles(titles) {
21 return titles.map(e => {
22 if (e.title) {
23 return e.title;
24 } else {
25 return ((e.vendor || '') + ' ' + (e.model || '') + ' ' + (e.variant || '')).trim();
26 }
27 }).join(' / ');
28 }
29
30 function get_version_url(version) {
31 if (version == 'SNAPSHOT') {
32 return config.base_url + '/snapshots/targets/{target}'
33 } else {
34 return config.base_url + '/releases/{version}/targets/{target}'.replace('{version}', version)
35 }
36 }
37
38 function build_asa_request() {
39 if (!current_model || !current_model.id) {
40 alert('bad profile');
41 return;
42 }
43
44 function showStatus(text) {
45 show('buildstatus');
46 $('buildstatus').innerHTML = text;
47 translate();
48 }
49
50 function handleError(response) {
51 hide('buildspinner');
52
53 response.json()
54 .then(mobj => {
55 var message = mobj['message'] || '<span class="tr-build-failed"></span>';
56 if (mobj.buildlog == true) {
57 var url = config.asu_url + '/store/' + mobj.bin_dir + '/buildlog.txt';
58 showStatus('<a href="' + url + '">' + message + '</a>');
59 } else {
60 showStatus(message);
61 }
62 });
63 }
64
65 // hide image view
66 updateImages();
67
68 show('buildspinner');
69 showStatus('<span class="tr-request-image"></span>');
70
71 var request_data = {
72 'target': current_model.target,
73 'profile': current_model.id,
74 'packages': split($('packages').value),
75 'version': $('versions').value
76 }
77
78 fetch(config.asu_url + '/api/build', {
79 method: 'POST',
80 headers: { 'Content-Type': 'application/json' },
81 body: JSON.stringify(request_data)
82 })
83 .then(response => {
84 switch (response.status) {
85 case 200:
86 hide('buildspinner');
87 showStatus('<span class="tr-build-successful"></span>');
88
89 response.json()
90 .then(mobj => {
91 var download_url = config.asu_url + '/store/' + mobj.bin_dir;
92 updateImages(
93 mobj.version_number,
94 mobj.version_code,
95 mobj.build_at,
96 get_model_titles(mobj.titles),
97 download_url, mobj, true
98 );
99 });
100 break;
101 case 202:
102 showStatus('<span class="tr-check-again"></span>');
103 setTimeout(_ => { build_asa_request() }, 5000);
104 break;
105 case 400: // bad request
106 case 422: // bad package
107 case 500: // build failed
108 handleError(response);
109 break;
110 }
111 })
112 .catch(err => {
113 hide('buildspinner');
114 showStatus(err);
115 })
116 }
117
118 function setupSelectList(select, items, onselection) {
119 for (var i = 0; i < items.length; i += 1) {
120 var option = document.createElement('OPTION');
121 option.innerHTML = items[i];
122 select.appendChild(option);
123 }
124
125 select.addEventListener('change', e => {
126 onselection(items[select.selectedIndex]);
127 });
128
129 if (select.selectedIndex >= 0) {
130 onselection(items[select.selectedIndex]);
131 }
132 }
133
134 // Change the translation of the entire document
135 function translate() {
136 var mapping = translations[config.language];
137 for (var tr in mapping) {
138 Array.from(document.getElementsByClassName(tr))
139 .forEach(e => { e.innerText = mapping[tr]; })
140 }
141 }
142
143 function setupAutocompleteList(input, items, as_list, onbegin, onend) {
144 var currentFocus = -1;
145
146 // sort numbers and other characters separately
147 var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
148
149 items.sort(collator.compare);
150
151 input.oninput = function(e) {
152 onbegin();
153
154 var offset = 0;
155 var value = this.value;
156 var value_list = [];
157
158 if (as_list) {
159 // automcomplete last text item
160 offset = this.value.lastIndexOf(' ') + 1;
161 value = this.value.substr(offset);
162 value_list = split(this.value.substr(0, offset));
163 }
164
165 // close any already open lists of autocompleted values
166 closeAllLists();
167
168 if (!value) {
169 return false;
170 }
171
172 // create a DIV element that will contain the items (values):
173 var list = document.createElement('DIV');
174 list.setAttribute('id', this.id + '-autocomplete-list');
175 list.setAttribute('class', 'autocomplete-items');
176 // append the DIV element as a child of the autocomplete container:
177 this.parentNode.appendChild(list);
178
179 var c = 0;
180 for (var i = 0; i < items.length; i += 1) {
181 var item = items[i];
182
183 // match
184 var j = item.toUpperCase().indexOf(value.toUpperCase());
185 if (j < 0) {
186 continue;
187 }
188
189 // do not offer a duplicate item
190 if (as_list && value_list.indexOf(item) != -1) {
191 continue;
192 }
193
194 c += 1;
195 if (c >= 15) {
196 var div = document.createElement('DIV');
197 div.innerHTML = '...';
198 list.appendChild(div);
199 break;
200 } else {
201 var div = document.createElement('DIV');
202 // make the matching letters bold:
203 div.innerHTML = item.substr(0, j)
204 + '<strong>' + item.substr(j, value.length) + '</strong>'
205 + item.substr(j + value.length)
206 + '<input type="hidden" value="' + item + '">';
207
208 div.addEventListener('click', function(e) {
209 // include selected value
210 var selected = this.getElementsByTagName('input')[0].value;
211 if (as_list) {
212 input.value = value_list.join(' ') + ' ' + selected;
213 } else {
214 input.value = selected;
215 }
216 // close the list of autocompleted values,
217 closeAllLists();
218 onend(input);
219 });
220
221 list.appendChild(div);
222 }
223 }
224 };
225
226 input.onkeydown = function(e) {
227 var x = document.getElementById(this.id + '-autocomplete-list');
228 if (x) x = x.getElementsByTagName('div');
229 if (e.keyCode == 40) {
230 // key down
231 currentFocus += 1;
232 // and and make the current item more visible:
233 setActive(x);
234 } else if (e.keyCode == 38) {
235 // key up
236 currentFocus -= 1;
237 // and and make the current item more visible:
238 setActive(x);
239 } else if (e.keyCode == 13) {
240 // If the ENTER key is pressed, prevent the form from being submitted,
241 e.preventDefault();
242 if (currentFocus > -1) {
243 // and simulate a click on the 'active' item:
244 if (x) x[currentFocus].click();
245 }
246 }
247 };
248
249 input.onfocus = function() {
250 onend(input);
251 }
252
253 // focus lost
254 input.onblur = function() {
255 onend(input);
256 }
257
258 function setActive(x) {
259 // a function to classify an item as 'active':
260 if (!x) return false;
261 // start by removing the 'active' class on all items:
262 for (var i = 0; i < x.length; i++) {
263 x[i].classList.remove('autocomplete-active');
264 }
265 if (currentFocus >= x.length) currentFocus = 0;
266 if (currentFocus < 0) currentFocus = (x.length - 1);
267 // add class 'autocomplete-active':
268 x[currentFocus].classList.add('autocomplete-active');
269 }
270
271 function closeAllLists(elmnt) {
272 // close all autocomplete lists in the document,
273 // except the one passed as an argument:
274 var x = document.getElementsByClassName('autocomplete-items');
275 for (var i = 0; i < x.length; i++) {
276 if (elmnt != x[i] && elmnt != input) {
277 x[i].parentNode.removeChild(x[i]);
278 }
279 }
280 }
281
282 // execute a function when someone clicks in the document:
283 document.addEventListener('click', e => {
284 closeAllLists(e.target);
285 });
286 }
287
288 // for attended sysupgrade
289 function updatePackageList(target) {
290 // set available packages
291 fetch(config.asu_url + '/' + target + '/packages.json')
292 .then(response => response.json())
293 .then(all_packages => {
294 setupAutocompleteList($('packages'), all_packages, true, _ => {}, textarea => {
295 textarea.value = split(textarea.value)
296 .filter((value, index, self) => self.indexOf(value) === index) // make list unique
297 //.filter((value, index) => all_packages.indexOf(value) !== -1) // limit to available packages
298 .join(' ');
299 });
300 });
301 }
302
303 function updateImages(version, code, date, model, url, mobj, is_custom) {
304 // add download button for image
305 function addLink(type, file) {
306 var a = document.createElement('A');
307 a.classList.add('download-link');
308 a.href = url
309 .replace('{target}', mobj.target)
310 .replace('{version}', version)
311 + '/' + file;
312 var span = document.createElement('SPAN');
313 span.appendChild(document.createTextNode(''));
314 a.appendChild(span);
315 a.appendChild(document.createTextNode(type.toUpperCase()));
316
317 if (config.showHelp) {
318 a.onmouseover = function() {
319 // hide all help texts
320 Array.from(document.getElementsByClassName('download-help'))
321 .forEach(e => e.style.display = 'none');
322 var lc = type.toLowerCase();
323 if (lc.includes('sysupgrade')) {
324 show('sysupgrade-help');
325 } else if (lc.includes('factory') || lc == 'trx' || lc == 'chk') {
326 show('factory-help');
327 } else if (lc.includes('kernel') || lc.includes('zimage') || lc.includes('uimage')) {
328 show('kernel-help');
329 } else if (lc.includes('root')) {
330 show('rootfs-help');
331 } else if (lc.includes('sdcard')) {
332 show('sdcard-help');
333 } else if (lc.includes('tftp')) {
334 show('tftp-help');
335 } else {
336 show('other-help');
337 }
338 };
339 }
340
341 $('download-links').appendChild(a);
342 }
343
344 function switchClass(id, from_class, to_class) {
345 $(id).classList.remove(from_class);
346 $(id).classList.add(to_class);
347 }
348
349 // remove all download links
350 Array.from(document.getElementsByClassName('download-link'))
351 .forEach(e => e.remove());
352
353 // hide all help texts
354 Array.from(document.getElementsByClassName('download-help'))
355 .forEach(e => e.style.display = 'none');
356
357 if (version && code && date && model && url && mobj) {
358 var target = mobj.target;
359 var images = mobj.images;
360
361 // change between "version" and "custom" title
362 if (is_custom) {
363 switchClass('images-title', 'tr-version-build', 'tr-custom-build');
364 switchClass('downloads-title', 'tr-version-downloads', 'tr-custom-downloads');
365 } else {
366 switchClass('images-title', 'tr-custom-build', 'tr-version-build');
367 switchClass('downloads-title', 'tr-custom-downloads', 'tr-version-downloads');
368 }
369 // update title translation
370 translate();
371
372 // fill out build info
373 $('image-model').innerText = model;
374 $('image-target').innerText = target;
375 $('image-version').innerText = version;
376 $('image-code').innerText = code;
377 $('image-date').innerText = date;
378
379 images.sort((a, b) => a.name.localeCompare(b.name));
380
381 for (var i in images) {
382 addLink(images[i].type, images[i].name);
383 }
384
385 if (config.asu_url) {
386 updatePackageList(target);
387 }
388
389 show('images');
390 } else {
391 hide('images');
392 }
393 }
394
395 function init() {
396 setupSelectList($('versions'), Object.keys(config.versions), version => {
397 fetch(config.versions[version]).then(data => {
398 data.json().then(obj => {
399 // handle native openwrt json format
400 if ('profiles' in obj) {
401 obj['url'] = get_version_url(version)
402 obj['models'] = {}
403 for (const [key, value] of Object.entries(obj['profiles'])) {
404 obj['models'][get_model_titles(value.titles)] = value
405 obj['models'][get_model_titles(value.titles)]['id'] = key
406 }
407 }
408 return obj
409 }).then(obj => {
410 setupAutocompleteList($('models'), Object.keys(obj['models']), false, updateImages, models => {
411 var model = models.value;
412 if (model in obj['models']) {
413 var url = obj.url || 'unknown';
414 var code = obj.version_code || 'unknown';
415 var date = obj.build_date || 'unknown';
416 var mobj = obj['models'][model];
417 updateImages(version, code, date, model, url, mobj, false);
418 current_model = mobj;
419 } else {
420 updateImages();
421 current_model = {};
422 }
423 });
424
425 // trigger model update when selected version changes
426 $('models').onfocus();
427 });
428 });
429 });
430
431 if (config.asu_url) {
432 show('custom');
433 }
434
435 // hide fields
436 updateImages();
437
438 var user_lang = (navigator.language || navigator.userLanguage).split('-')[0];
439 if (user_lang in translations) {
440 config.language = user_lang;
441 $('language-selection').value = user_lang;
442 }
443
444 translate();
445
446 $('language-selection').onclick = function() {
447 config.language = this.children[this.selectedIndex].value;
448 translate();
449 }
450 }