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