forgot to join titles
[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(function(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(function(response) {
56 switch (response.status) {
57 case 200:
58 hide('loading');
59
60 console.log('image found');
61 response.json()
62 .then(function(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(function() { 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('bad request (' + response.status + ')'); // see message
81 response.json()
82 .then(function(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', function(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(function(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', function (e) {
255 closeAllLists(e.target);
256 });
257 }
258
259 function findCommonPrefix(images) {
260 var files = images.map(image => image.name)
261 var A = files.concat().sort();
262 var first = A[0];
263 var last = A[A.length - 1];
264 var L = first.length;
265 var i = 0;
266 while (i < L && first.charAt(i) === last.charAt(i)) {
267 i += 1;
268 }
269 return first.substring(0, i);
270 }
271
272 function updateImages(version, commit, model, url, mobj, is_custom) {
273 // add download button for image
274 function addLink(label, tags, file, help_id) {
275 var a = document.createElement('A');
276 a.classList.add('download-link');
277 a.href = url
278 .replace('{target}', mobj.target)
279 .replace('{release}', version)
280 + '/' + file;
281 var span = document.createElement('SPAN');
282 span.appendChild(document.createTextNode(''));
283 a.appendChild(span);
284
285 // add sub label
286 if (tags.length > 0) {
287 a.appendChild(document.createTextNode(label + ' (' + tags.join(', ') + ')'));
288 } else {
289 a.appendChild(document.createTextNode(label));
290 }
291
292 if (config.showHelp) {
293 a.onmouseover = function() {
294 // hide all help texts
295 Array.from(document.getElementsByClassName('download-help'))
296 .forEach(function(e) { e.style.display = 'none'; });
297 show(help_id);
298 };
299 }
300
301 $('download-links').appendChild(a);
302 }
303
304 function switchClass(id, from_class, to_class) {
305 $(id).classList.remove(from_class);
306 $(id).classList.add(to_class);
307 }
308
309 // remove all download links
310 Array.from(document.getElementsByClassName('download-link'))
311 .forEach(function(e) { e.remove(); });
312
313 // hide all help texts
314 Array.from(document.getElementsByClassName('download-help'))
315 .forEach(function(e) { e.style.display = 'none'; });
316
317 if (version && commit && model && url && mobj) {
318 var target = mobj.target;
319 var images = mobj.images;
320
321 // change between "release" and "custom" title
322 if (is_custom) {
323 switchClass('images-title', 'tr-release-build', 'tr-custom-build');
324 switchClass('downloads-title', 'tr-release-downloads', 'tr-custom-downloads');
325 } else {
326 switchClass('images-title', 'tr-custom-build', 'tr-release-build');
327 switchClass('downloads-title', 'tr-custom-downloads', 'tr-release-downloads');
328 }
329 // update title translation
330 applyLanguage();
331
332 // fill out build info
333 $('image-model').innerText = model;
334 $('image-target').innerText = target;
335 $('image-release').innerText = version;
336 $('image-commit').innerText = commit;
337
338 var prefix = findCommonPrefix(images);
339 var entries = {
340 'FACTORY': [],
341 'SYSUPGRADE': [],
342 'KERNEL': [],
343 'ROOTFS': [],
344 'SDCARD': [],
345 'TFTP': [],
346 'OTHER': []
347 };
348
349 images.sort();
350
351 for (var i in images) {
352 var image = images[i].name;
353 var lc = image.toLowerCase()
354 if (lc.includes('factory')) {
355 entries['FACTORY'].push(image);
356 } else if (lc.includes('sysupgrade')) {
357 entries['SYSUPGRADE'].push(image);
358 } else if (lc.includes('kernel') || lc.includes('zimage') || lc.includes('uimage')) {
359 entries['KERNEL'].push(image);
360 } else if (lc.includes('rootfs')) {
361 entries['ROOTFS'].push(image);
362 } else if (lc.includes('sdcard')) {
363 entries['SDCARD'].push(image);
364 } else if (lc.includes('tftp')) {
365 entries['TFTP'].push(image);
366 } else {
367 entries['OTHER'].push(image);
368 }
369 }
370
371 function extractTags(prefix, image) {
372 var all = image.substring(prefix.length).split('.')[0].split('-');
373 var ignore = ['', 'kernel', 'zImage', 'uImage', 'factory', 'sysupgrade', 'rootfs', 'sdcard'];
374 return all.filter(function (el) { return !ignore.includes(el); });
375 }
376
377 for (var category in entries) {
378 var images = entries[category];
379 for (var i in images) {
380 var image = images[i];
381 var tags = (images.length > 1) ? extractTags(prefix, image) : [];
382 var label = category;
383 addLink(label, tags, image, category.toLowerCase() + '-help');
384 }
385 }
386
387 show('images');
388 } else {
389 hide('images');
390 }
391 }
392
393 setupSelectList($('releases'), Object.keys(config.versions), function(version) {
394 loadFile(config.versions[version], function(obj) {
395 data = obj
396 setupAutocompleteList($('models'), Object.keys(obj['models']), function(model) {
397 if (model in obj['models']) {
398 var url = obj.url;
399 var commit = obj.version_commit;
400 var mobj = obj['models'][model];
401 updateImages(version, commit, model, url, mobj, false);
402 current_model = mobj;
403 } else {
404 updateImages();
405 current_model = {};
406 }
407 });
408
409 // trigger model update when selected version changes
410 $('models').onfocus();
411 });
412 });
413
414 if (config.asu_url) {
415 show('custom');
416 }
417
418 // hide fields
419 updateImages();
420 applyLanguage(config.language);