Merge branch 'master' of github.com:mwarning/yet_another_firmware_selector
[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 build_asa_request() {
17 if (!current_model || !current_model.id) {
18 alert('bad profile');
19 return;
20 }
21
22 function split(str) {
23 return str.match(/[^\s,]+/g) || [];
24 }
25
26 function get_model_titles(titles) {
27 return titles.map(e => {
28 if (e.title) {
29 return e.title;
30 } else {
31 return ((e.vendor || '') + ' ' + (e.model || '') + ' ' + (e.variant || '')).trim();
32 }
33 }).join('/');
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, 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 function init() {
351 setupSelectList($('versions'), Object.keys(config.versions), version => {
352 fetch(config.versions[version]).then(data => {
353 data.json().then(obj => {
354 setupAutocompleteList($('models'), Object.keys(obj['models']), model => {
355 if (model in obj['models']) {
356 var url = obj.url;
357 var code = obj.version_code;
358 var date = obj.build_data || 'unknown';
359 var mobj = obj['models'][model];
360 updateImages(version, code, date, model, url, mobj, false);
361 current_model = mobj;
362 } else {
363 updateImages();
364 current_model = {};
365 }
366 });
367
368 // trigger model update when selected version changes
369 $('models').onfocus();
370 });
371 });
372 });
373
374 if (config.asu_url) {
375 show('custom');
376 }
377
378 // hide fields
379 updateImages();
380
381 var user_lang = (navigator.language || navigator.userLanguage).split('-')[0];
382 if (user_lang in translations) {
383 config.language = user_lang;
384 $('language-selection').value = user_lang;
385 }
386
387 translate();
388
389 $('language-selection').onclick = function() {
390 config.language = this.children[this.selectedIndex].value;
391 translate();
392 }
393 }