2 var current_model
= {};
5 if (typeof query
=== 'string') {
6 return document
.querySelector(query
);
12 function show(query
) {
13 $(query
).style
.display
= 'block';
16 function hide(query
) {
17 $(query
).style
.display
= 'none';
21 return str
.match(/[^\s,]+/g) || [];
24 function get_model_titles(titles
) {
25 return titles
.map(e
=> {
29 return ((e
.vendor
|| '') + ' ' + (e
.model
|| '') + ' ' + (e
.variant
|| '')).trim();
34 function build_asu_request() {
35 if (!current_model
|| !current_model
.id
) {
40 function showStatus(message
, url
) {
42 var tr
= message
.startsWith('tr-') ? message
: '';
44 $('#buildstatus').innerHTML
= '<a href="' + url
+ '" class="' + tr
+ '">' + message
+ '</a>';
46 $('#buildstatus').innerHTML
= '<span class="' + tr
+ '"></span>';
54 show('#buildspinner');
55 showStatus('tr-request-image');
58 'target': current_model
.target
,
59 'profile': current_model
.id
,
60 'packages': split($('#packages').value
),
61 'version': $('#versions').value
64 fetch(config
.asu_url
+ '/api/build', {
66 headers
: { 'Content-Type': 'application/json' },
67 body
: JSON
.stringify(request_data
)
70 switch (response
.status
) {
72 hide('#buildspinner');
73 showStatus('tr-build-successful');
77 var download_url
= config
.asu_url
+ '/store/' + mobj
.bin_dir
;
78 showStatus('tr-build-successful', download_url
+ '/buildlog.txt');
83 get_model_titles(mobj
.titles
),
84 download_url
, mobj
, true
89 showStatus('tr-check-again');
90 setTimeout(_
=> { build_asu_request() }, 5000);
92 case 400: // bad request
93 case 422: // bad package
94 case 500: // build failed
95 hide('#buildspinner');
98 var message
= mobj
['message'] || 'tr-build-failed';
99 var url
= mobj
.buildlog
? (config
.asu_url
+ '/store/' + mobj
.bin_dir
+ '/buildlog.txt') : undefined;
100 showStatus(message
, url
);
106 hide('#buildspinner');
111 function setupSelectList(select
, items
, onselection
) {
112 for (var i
= 0; i
< items
.length
; i
+= 1) {
113 var option
= document
.createElement('OPTION');
114 option
.innerHTML
= items
[i
];
115 select
.appendChild(option
);
118 select
.addEventListener('change', e
=> {
119 onselection(items
[select
.selectedIndex
]);
122 if (select
.selectedIndex
>= 0) {
123 onselection(items
[select
.selectedIndex
]);
127 // Change the translation of the entire document
128 function translate() {
129 var mapping
= translations
[config
.language
];
130 for (var tr
in mapping
) {
131 Array
.from(document
.getElementsByClassName(tr
))
132 .forEach(e
=> { e
.innerText
= mapping
[tr
]; })
136 function setupAutocompleteList(input
, items
, as_list
, onbegin
, onend
) {
137 var currentFocus
= -1;
139 // sort numbers and other characters separately
140 var collator
= new Intl
.Collator(undefined, {numeric
: true, sensitivity
: 'base'});
142 items
.sort(collator
.compare
);
144 input
.oninput = function(e
) {
148 var value
= this.value
;
152 // automcomplete last text item
153 offset
= this.value
.lastIndexOf(' ') + 1;
154 value
= this.value
.substr(offset
);
155 value_list
= split(this.value
.substr(0, offset
));
158 // close any already open lists of autocompleted values
165 // create a DIV element that will contain the items (values):
166 var list
= document
.createElement('DIV');
167 list
.setAttribute('id', this.id
+ '-autocomplete-list');
168 list
.setAttribute('class', 'autocomplete-items');
169 // append the DIV element as a child of the autocomplete container:
170 this.parentNode
.appendChild(list
);
172 function normalize(s
) {
173 return s
.toUpperCase().replace(/[-_.]/g, ' ');
176 var match
= normalize(value
);
178 for (var i
= 0; i
< items
.length
; i
+= 1) {
182 var j
= normalize(item
).indexOf(match
);
187 // do not offer a duplicate item
188 if (as_list
&& value_list
.indexOf(item
) != -1) {
194 var div
= document
.createElement('DIV');
195 div
.innerHTML
= '...';
196 list
.appendChild(div
);
199 var div
= document
.createElement('DIV');
200 // make the matching letters bold:
201 div
.innerHTML
= item
.substr(0, j
)
202 + '<strong>' + item
.substr(j
, value
.length
) + '</strong>'
203 + item
.substr(j
+ value
.length
)
204 + '<input type="hidden" value="' + item
+ '">';
206 div
.addEventListener('click', function(e
) {
207 // include selected value
208 var selected
= this.getElementsByTagName('input')[0].value
;
210 input
.value
= value_list
.join(' ') + ' ' + selected
;
212 input
.value
= selected
;
214 // close the list of autocompleted values,
219 list
.appendChild(div
);
224 input
.onkeydown = function(e
) {
225 var x
= document
.getElementById(this.id
+ '-autocomplete-list');
226 if (x
) x
= x
.getElementsByTagName('div');
227 if (e
.keyCode
== 40) {
230 // and and make the current item more visible:
232 } else if (e
.keyCode
== 38) {
235 // and and make the current item more visible:
237 } else if (e
.keyCode
== 13) {
238 // If the ENTER key is pressed, prevent the form from being submitted,
240 if (currentFocus
> -1) {
241 // and simulate a click on the 'active' item:
242 if (x
) x
[currentFocus
].click();
247 input
.onfocus = function() {
252 input
.onblur = function() {
256 function setActive(x
) {
257 // a function to classify an item as 'active':
258 if (!x
) return false;
259 // start by removing the 'active' class on all items:
260 for (var i
= 0; i
< x
.length
; i
++) {
261 x
[i
].classList
.remove('autocomplete-active');
263 if (currentFocus
>= x
.length
) currentFocus
= 0;
264 if (currentFocus
< 0) currentFocus
= (x
.length
- 1);
265 // add class 'autocomplete-active':
266 x
[currentFocus
].classList
.add('autocomplete-active');
269 function closeAllLists(elmnt
) {
270 // close all autocomplete lists in the document,
271 // except the one passed as an argument:
272 var x
= document
.getElementsByClassName('autocomplete-items');
273 for (var i
= 0; i
< x
.length
; i
++) {
274 if (elmnt
!= x
[i
] && elmnt
!= input
) {
275 x
[i
].parentNode
.removeChild(x
[i
]);
280 // execute a function when someone clicks in the document:
281 document
.addEventListener('click', e
=> {
282 closeAllLists(e
.target
);
286 // for attended sysupgrade
287 function updatePackageList(version
, target
) {
288 // set available packages
289 fetch(config
.asu_url
+ '/' + config
.versions
[version
] + '/' + target
+ '/index.json')
290 .then(response
=> response
.json())
291 .then(all_packages
=> {
292 setupAutocompleteList($('#packages'), all_packages
, true, _
=> {}, textarea
=> {
293 textarea
.value
= split(textarea
.value
)
294 // make list unique, ignore minus
295 .filter((value
, index
, self
) => {
296 var i
= self
.indexOf(value
.replace(/^\-/, ''));
297 return (i
=== index
) || (i
< 0);
299 // limit to available packages, ignore minus
300 .filter((value
, index
) => all_packages
.indexOf(value
.replace(/^\-/, '')) !== -1)
306 function updateImages(version
, code
, date
, model
, url
, mobj
, is_custom
) {
307 // add download button for image
308 function addLink(type
, file
) {
309 var a
= document
.createElement('A');
310 a
.classList
.add('download-link');
312 .replace('{target}', mobj
.target
)
313 .replace('{version}', version
)
315 var span
= document
.createElement('SPAN');
316 span
.appendChild(document
.createTextNode(''));
318 a
.appendChild(document
.createTextNode(type
.toUpperCase()));
320 if (config
.showHelp
) {
321 a
.onmouseover = function() {
322 // hide all help texts
323 Array
.from(document
.getElementsByClassName('download-help'))
324 .forEach(e
=> e
.style
.display
= 'none');
325 var lc
= type
.toLowerCase();
326 if (lc
.includes('sysupgrade')) {
327 show('#sysupgrade-help');
328 } else if (lc
.includes('factory') || lc
== 'trx' || lc
== 'chk') {
329 show('#factory-help');
330 } else if (lc
.includes('kernel') || lc
.includes('zimage') || lc
.includes('uimage')) {
331 show('#kernel-help');
332 } else if (lc
.includes('root')) {
333 show('#rootfs-help');
334 } else if (lc
.includes('sdcard')) {
335 show('#sdcard-help');
336 } else if (lc
.includes('tftp')) {
344 $('#download-links').appendChild(a
);
347 function switchClass(query
, from_class
, to_class
) {
348 $(query
).classList
.remove(from_class
);
349 $(query
).classList
.add(to_class
);
352 // remove all download links
353 Array
.from(document
.getElementsByClassName('download-link'))
354 .forEach(e
=> e
.remove());
356 // hide all help texts
357 Array
.from(document
.getElementsByClassName('download-help'))
358 .forEach(e
=> e
.style
.display
= 'none');
360 if (model
&& url
&& mobj
) {
361 var target
= mobj
.target
;
362 var images
= mobj
.images
;
364 // change between "version" and "custom" title
366 switchClass('#build-title', 'tr-version-build', 'tr-custom-build');
367 switchClass('#downloads-title', 'tr-version-downloads', 'tr-custom-downloads');
369 switchClass('#build-title', 'tr-custom-build', 'tr-version-build');
370 switchClass('#downloads-title', 'tr-custom-downloads', 'tr-version-downloads');
373 // update title translation
376 // fill out build info
377 $('#image-model').innerText
= model
;
378 $('#image-target').innerText
= target
;
379 $('#image-version').innerText
= version
;
380 $('#image-code').innerText
= mobj
['code'] || code
;
381 $('#image-date').innerText
= date
;
383 images
.sort((a
, b
) => a
.name
.localeCompare(b
.name
));
385 for (var i
in images
) {
386 addLink(images
[i
].type
, images
[i
].name
);
389 if (config
.asu_url
) {
390 updatePackageList(version
, target
);
400 var build_date
= "unknown"
401 setupSelectList($('#versions'), Object
.keys(config
.versions
), version
=> {
402 var url
= config
.versions
[version
];
403 if (config
.asu_url
) {
404 url
= config
.asu_url
+ '/' + url
+ '/profiles.json';
408 build_date
= obj
.headers
.get('last-modified');
412 // handle native openwrt json format
413 if ('profiles' in obj
) {
415 for (const [key
, value
] of Object
.entries(obj
['profiles'])) {
417 obj
['models'][get_model_titles(value
.titles
)] = value
423 setupAutocompleteList($('#models'), Object
.keys(obj
['models']), false, updateImages
, models
=> {
424 var model
= models
.value
;
425 if (model
in obj
['models']) {
426 var url
= obj
.download_url
|| 'unknown';
427 var code
= obj
.version_code
|| 'unknown';
428 var mobj
= obj
['models'][model
];
429 updateImages(version
, code
, build_date
, model
, url
, mobj
, false);
430 current_model
= mobj
;
437 // trigger model update when selected version changes
438 $('#models').onfocus();
442 if (config
.asu_url
) {
449 // default to browser language
450 var user_lang
= (navigator
.language
|| navigator
.userLanguage
).split('-')[0];
451 if (user_lang
in translations
) {
452 config
.language
= user_lang
;
453 $('#language-selection').value
= user_lang
;
458 $('#language-selection').onclick = function() {
459 config
.language
= this.children
[this.selectedIndex
].value
;