use modern fetch api
[web/firmware-selector-openwrt-org.git] / index.js
1
2 var current_model = {};
3 var current_language = config.language;
4
5 function $(id) {
6 return document.getElementById(id);
7 }
8
9 function show(id) {
10 $(id).style.display = 'block';
11 }
12
13 function hide(id) {
14 $(id).style.display = 'none';
15 }
16
17 function build_asa_request() {
18 if (!current_model || !current_model.id) {
19 alert('bad profile');
20 return;
21 }
22
23 function split(str) {
24 return str.match(/[^\s,]+/g) || [];
25 }
26
27 function get_model_titles(titles) {
28 return titles.map(e => {
29 if (e.title) {
30 return e.title;
31 } else {
32 return ((e.vendor || '') + (e.model || '') + (e.variant || '')).trim();
33 }
34 }).join('/');
35 }
36
37 function showStatus(text) {
38 show('buildstatus');
39 $('buildstatus').innerHTML = text;
40 translate();
41 }
42
43 function handleError(response) {
44 hide('buildspinner');
45
46 response.json()
47 .then(mobj => {
48 var message = mobj['message'] || '<span class="tr-build-failed"></span>';
49 if (mobj.buildlog == true) {
50 var url = config.asu_url + '/store/' + mobj.bin_dir + '/buildlog.txt';
51 showStatus('<a href="' + url + '">' + message + '</a>');
52 } else {
53 showStatus(message);
54 }
55 });
56 }
57
58 // hide image view
59 updateImages();
60
61 show('buildspinner');
62 showStatus('<span class="tr-request-image"></span>');
63
64 var request_data = {
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_commit,
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[current_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, onselection) {
136 // the setupAutocompleteList function takes two arguments,
137 // the text field element and an array of possible autocompleted values:
138 var currentFocus = -1;
139
140 // sort numbers and other characters separately
141 var collator = new Intl.Collator(undefined, {numeric: true, sensitivity: 'base'});
142
143 items.sort(collator.compare);
144
145 // execute a function when someone writes in the text field:
146 input.oninput = function(e) {
147 // clear images
148 updateImages();
149
150 var value = this.value;
151 // close any already open lists of autocompleted values
152 closeAllLists();
153
154 if (!value) {
155 return false;
156 }
157
158 // create a DIV element that will contain the items (values):
159 var list = document.createElement('DIV');
160 list.setAttribute('id', this.id + '-autocomplete-list');
161 list.setAttribute('class', 'autocomplete-items');
162 // append the DIV element as a child of the autocomplete container:
163 this.parentNode.appendChild(list);
164
165 // for each item in the array...
166 var c = 0;
167 for (var i = 0; i < items.length; i += 1) {
168 var item = items[i];
169
170 // match
171 var j = item.toUpperCase().indexOf(value.toUpperCase());
172 if (j < 0) {
173 continue;
174 }
175
176 c += 1;
177 if (c >= 15) {
178 var div = document.createElement('DIV');
179 div.innerHTML = '...';
180 list.appendChild(div);
181 break;
182 } else {
183 var div = document.createElement('DIV');
184 // make the matching letters bold:
185 div.innerHTML = item.substr(0, j)
186 + '<strong>' + item.substr(j, value.length) + '</strong>'
187 + item.substr(j + value.length)
188 + '<input type="hidden" value="' + item + '">';
189
190 div.addEventListener('click', function(e) {
191 // set text field to selected value
192 input.value = this.getElementsByTagName('input')[0].value;
193 // close the list of autocompleted values,
194 // (or any other open lists of autocompleted values:
195 closeAllLists();
196 // callback
197 onselection(input.value);
198 });
199
200 list.appendChild(div);
201 }
202 }
203 };
204
205 input.onkeydown = function(e) {
206 var x = document.getElementById(this.id + '-autocomplete-list');
207 if (x) x = x.getElementsByTagName('div');
208 if (e.keyCode == 40) {
209 // key down
210 currentFocus += 1;
211 // and and make the current item more visible:
212 setActive(x);
213 } else if (e.keyCode == 38) {
214 // key up
215 currentFocus -= 1;
216 // and and make the current item more visible:
217 setActive(x);
218 } else if (e.keyCode == 13) {
219 // If the ENTER key is pressed, prevent the form from being submitted,
220 e.preventDefault();
221 if (currentFocus > -1) {
222 // and simulate a click on the 'active' item:
223 if (x) x[currentFocus].click();
224 }
225 }
226 };
227
228 input.onfocus = function() {
229 onselection(input.value);
230 }
231
232 function setActive(x) {
233 // a function to classify an item as 'active':
234 if (!x) return false;
235 // start by removing the 'active' class on all items:
236 for (var i = 0; i < x.length; i++) {
237 x[i].classList.remove('autocomplete-active');
238 }
239 if (currentFocus >= x.length) currentFocus = 0;
240 if (currentFocus < 0) currentFocus = (x.length - 1);
241 // add class 'autocomplete-active':
242 x[currentFocus].classList.add('autocomplete-active');
243 }
244
245 function closeAllLists(elmnt) {
246 // close all autocomplete lists in the document,
247 // except the one passed as an argument:
248 var x = document.getElementsByClassName('autocomplete-items');
249 for (var i = 0; i < x.length; i++) {
250 if (elmnt != x[i] && elmnt != input) {
251 x[i].parentNode.removeChild(x[i]);
252 }
253 }
254 }
255
256 // execute a function when someone clicks in the document:
257 document.addEventListener('click', e => {
258 closeAllLists(e.target);
259 });
260 }
261
262 function updateImages(version, code, date, model, url, mobj, is_custom) {
263 // add download button for image
264 function addLink(type, file) {
265 var a = document.createElement('A');
266 a.classList.add('download-link');
267 a.href = url
268 .replace('{target}', mobj.target)
269 .replace('{version}', version)
270 + '/' + file;
271 var span = document.createElement('SPAN');
272 span.appendChild(document.createTextNode(''));
273 a.appendChild(span);
274 a.appendChild(document.createTextNode(type.toUpperCase()));
275
276 if (config.showHelp) {
277 a.onmouseover = function() {
278 // hide all help texts
279 Array.from(document.getElementsByClassName('download-help'))
280 .forEach(e => e.style.display = 'none');
281 var lc = type.toLowerCase();
282 if (lc.includes('sysupgrade')) {
283 show('sysupgrade-help');
284 } else if (lc.includes('factory') || lc == 'trx' || lc == 'chk') {
285 show('factory-help');
286 } else if (lc.includes('kernel') || lc.includes('zimage') || lc.includes('uimage')) {
287 show('kernel-help');
288 } else if (lc.includes('root')) {
289 show('rootfs-help');
290 } else if (lc.includes('sdcard')) {
291 show('sdcard-help');
292 } else if (lc.includes('tftp')) {
293 show('tftp-help');
294 } else {
295 show('other-help');
296 }
297 };
298 }
299
300 $('download-links').appendChild(a);
301 }
302
303 function switchClass(id, from_class, to_class) {
304 $(id).classList.remove(from_class);
305 $(id).classList.add(to_class);
306 }
307
308 // remove all download links
309 Array.from(document.getElementsByClassName('download-link'))
310 .forEach(e => e.remove());
311
312 // hide all help texts
313 Array.from(document.getElementsByClassName('download-help'))
314 .forEach(e => e.style.display = 'none');
315
316 if (version && code && date && model && url && mobj) {
317 var target = mobj.target;
318 var images = mobj.images;
319
320 // change between "version" and "custom" title
321 if (is_custom) {
322 switchClass('images-title', 'tr-version-build', 'tr-custom-build');
323 switchClass('downloads-title', 'tr-version-downloads', 'tr-custom-downloads');
324 } else {
325 switchClass('images-title', 'tr-custom-build', 'tr-version-build');
326 switchClass('downloads-title', 'tr-custom-downloads', 'tr-version-downloads');
327 }
328 // update title translation
329 translate();
330
331 // fill out build info
332 $('image-model').innerText = model;
333 $('image-target').innerText = target;
334 $('image-version').innerText = version;
335 $('image-code').innerText = code;
336 $('image-date').innerText = date;
337
338 images.sort((a, b) => a.name.localeCompare(b.name));
339
340 for (var i in images) {
341 addLink(images[i].type, images[i].name);
342 }
343
344 show('images');
345 } else {
346 hide('images');
347 }
348 }
349
350 setupSelectList($('versions'), Object.keys(config.versions), version => {
351 fetch(config.versions[version]).then(data => {
352 data.json().then(obj => {
353 setupAutocompleteList($('models'), Object.keys(obj['models']), model => {
354 if (model in obj['models']) {
355 var url = obj.url;
356 var code = obj.version_code;
357 var date = obj.build_data || 'unknown';
358 var mobj = obj['models'][model];
359 updateImages(version, code, date, model, url, mobj, false);
360 current_model = mobj;
361 } else {
362 updateImages();
363 current_model = {};
364 }
365 });
366
367 // trigger model update when selected version changes
368 $('models').onfocus();
369 });
370 });
371 });
372
373 if (config.asu_url) {
374 show('custom');
375 }
376
377 // hide fields
378 updateImages();
379 translate();