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