5442e794b070abba43dfcab732cf0f70e2507cba
[oweals/luci.git] / applications / luci-app-opkg / htdocs / luci-static / resources / view / opkg.js
1 var packages = {
2         available: { providers: {}, pkgs: {} },
3         installed: { providers: {}, pkgs: {} }
4 };
5
6 var currentDisplayMode = 'available', currentDisplayRows = [];
7
8 function parseList(s, dest)
9 {
10         var re = /([^\n]*)\n/g,
11             pkg = null, key = null, val = null, m;
12
13         while ((m = re.exec(s)) !== null) {
14                 if (m[1].match(/^\s(.*)$/)) {
15                         if (pkg !== null && key !== null && val !== null)
16                                 val += '\n' + RegExp.$1.trim();
17
18                         continue;
19                 }
20
21                 if (key !== null && val !== null) {
22                         switch (key) {
23                         case 'package':
24                                 pkg = { name: val };
25                                 break;
26
27                         case 'depends':
28                         case 'provides':
29                                 var list = val.split(/\s*,\s*/);
30                                 if (list.length !== 1 || list[0].length > 0)
31                                         pkg[key] = list;
32                                 break;
33
34                         case 'installed-time':
35                                 pkg.installtime = new Date(+val * 1000);
36                                 break;
37
38                         case 'installed-size':
39                                 pkg.installsize = +val;
40                                 break;
41
42                         case 'status':
43                                 var stat = val.split(/\s+/),
44                                     mode = stat[1],
45                                     installed = stat[2];
46
47                                 switch (mode) {
48                                 case 'user':
49                                 case 'hold':
50                                         pkg[mode] = true;
51                                         break;
52                                 }
53
54                                 switch (installed) {
55                                 case 'installed':
56                                         pkg.installed = true;
57                                         break;
58                                 }
59                                 break;
60
61                         case 'essential':
62                                 if (val === 'yes')
63                                         pkg.essential = true;
64                                 break;
65
66                         case 'size':
67                                 pkg.size = +val;
68                                 break;
69
70                         case 'architecture':
71                         case 'auto-installed':
72                         case 'filename':
73                         case 'sha256sum':
74                         case 'section':
75                                 break;
76
77                         default:
78                                 pkg[key] = val;
79                                 break;
80                         }
81
82                         key = val = null;
83                 }
84
85                 if (m[1].trim().match(/^([\w-]+)\s*:(.+)$/)) {
86                         key = RegExp.$1.toLowerCase();
87                         val = RegExp.$2.trim();
88                 }
89                 else {
90                         dest.pkgs[pkg.name] = pkg;
91
92                         var provides = dest.providers[pkg.name] ? [] : [ pkg.name ];
93
94                         if (pkg.provides)
95                                 provides.push.apply(provides, pkg.provides);
96
97                         provides.forEach(function(p) {
98                                 dest.providers[p] = dest.providers[p] || [];
99                                 dest.providers[p].push(pkg);
100                         });
101                 }
102         }
103 }
104
105 function display(pattern)
106 {
107         var src = packages[currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode],
108             table = document.querySelector('#packages'),
109             pager = document.querySelector('#pager');
110
111         currentDisplayRows.length = 0;
112
113         if (typeof(pattern) === 'string' && pattern.length > 0)
114                 pattern = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig');
115
116         for (var name in src.pkgs) {
117                 var pkg = src.pkgs[name],
118                     desc = pkg.description || '',
119                     altsize = null;
120
121                 if (!pkg.size && packages.available.pkgs[name])
122                         altsize = packages.available.pkgs[name].size;
123
124                 if (!desc && packages.available.pkgs[name])
125                         desc = packages.available.pkgs[name].description || '';
126
127                 desc = desc.split(/\n/);
128                 desc = desc[0].trim() + (desc.length > 1 ? '…' : '');
129
130                 if ((pattern instanceof RegExp) &&
131                     !name.match(pattern) && !desc.match(pattern))
132                         continue;
133
134                 var btn, ver;
135
136                 if (currentDisplayMode === 'updates') {
137                         var avail = packages.available.pkgs[name],
138                             inst  = packages.installed.pkgs[name];
139
140                         if (!inst || !inst.installed)
141                                 continue;
142
143                         if (!avail || compareVersion(avail.version, pkg.version) <= 0)
144                                 continue;
145
146                         ver = '%s » %s'.format(
147                                 truncateVersion(pkg.version || '-'),
148                                 truncateVersion(avail.version || '-'));
149
150                         btn = E('div', {
151                                 'class': 'btn cbi-button-positive',
152                                 'data-package': name,
153                                 'click': handleInstall
154                         }, _('Upgrade…'));
155                 }
156                 else if (currentDisplayMode === 'installed') {
157                         if (!pkg.installed)
158                                 continue;
159
160                         ver = truncateVersion(pkg.version || '-');
161                         btn = E('div', {
162                                 'class': 'btn cbi-button-negative',
163                                 'data-package': name,
164                                 'click': handleRemove
165                         }, _('Remove…'));
166                 }
167                 else {
168                         var inst = packages.installed.pkgs[name];
169
170                         ver = truncateVersion(pkg.version || '-');
171
172                         if (!inst || !inst.installed)
173                                 btn = E('div', {
174                                         'class': 'btn cbi-button-action',
175                                         'data-package': name,
176                                         'click': handleInstall
177                                 }, _('Install…'));
178                         else if (inst.installed && inst.version != pkg.version)
179                                 btn = E('div', {
180                                         'class': 'btn cbi-button-positive',
181                                         'data-package': name,
182                                         'click': handleInstall
183                                 }, _('Upgrade…'));
184                         else
185                                 btn = E('div', {
186                                         'class': 'btn cbi-button-neutral',
187                                         'disabled': 'disabled'
188                                 }, _('Installed'));
189                 }
190
191                 name = '%h'.format(name);
192                 desc = '%h'.format(desc || '-');
193
194                 if (pattern) {
195                         name = name.replace(pattern, '<ins>$&</ins>');
196                         desc = desc.replace(pattern, '<ins>$&</ins>');
197                 }
198
199                 currentDisplayRows.push([
200                         name,
201                         ver,
202                         pkg.size ? '%.1024mB'.format(pkg.size)
203                                  : (altsize ? '~%.1024mB'.format(altsize) : '-'),
204                         desc,
205                         btn
206                 ]);
207         }
208
209         currentDisplayRows.sort(function(a, b) {
210                 if (a[0] < b[0])
211                         return -1;
212                 else if (a[0] > b[0])
213                         return 1;
214                 else
215                         return 0;
216         });
217
218         pager.parentNode.style.display = '';
219         pager.setAttribute('data-offset', 100);
220         handlePage({ target: pager.querySelector('.prev') });
221 }
222
223 function handlePage(ev)
224 {
225         var filter = document.querySelector('input[name="filter"]'),
226             pager = ev.target.parentNode,
227             offset = +pager.getAttribute('data-offset'),
228             next = ev.target.classList.contains('next');
229
230         if ((next && (offset + 100) >= currentDisplayRows.length) ||
231             (!next && (offset < 100)))
232             return;
233
234         offset += next ? 100 : -100;
235         pager.setAttribute('data-offset', offset);
236         pager.querySelector('.text').firstChild.data = currentDisplayRows.length
237                 ? _('Displaying %d-%d of %d').format(1 + offset, Math.min(offset + 100, currentDisplayRows.length), currentDisplayRows.length)
238                 : _('No packages');
239
240         if (offset < 100)
241                 pager.querySelector('.prev').setAttribute('disabled', 'disabled');
242         else
243                 pager.querySelector('.prev').removeAttribute('disabled');
244
245         if ((offset + 100) >= currentDisplayRows.length)
246                 pager.querySelector('.next').setAttribute('disabled', 'disabled');
247         else
248                 pager.querySelector('.next').removeAttribute('disabled');
249
250         var placeholder = _('No information available');
251
252         if (filter.value)
253                 placeholder = [
254                         E('span', {}, _('No packages matching "<strong>%h</strong>".').format(filter.value)), ' (',
255                         E('a', { href: '#', onclick: 'handleReset(event)' }, _('Reset')), ')'
256                 ];
257
258         cbi_update_table('#packages', currentDisplayRows.slice(offset, offset + 100),
259                 placeholder);
260 }
261
262 function handleMode(ev)
263 {
264         var tab = findParent(ev.target, 'li');
265         if (tab.getAttribute('data-mode') === currentDisplayMode)
266                 return;
267
268         tab.parentNode.querySelectorAll('li').forEach(function(li) {
269                 li.classList.remove('cbi-tab');
270                 li.classList.add('cbi-tab-disabled');
271         });
272
273         tab.classList.remove('cbi-tab-disabled');
274         tab.classList.add('cbi-tab');
275
276         currentDisplayMode = tab.getAttribute('data-mode');
277
278         display(document.querySelector('input[name="filter"]').value);
279
280         ev.target.blur();
281         ev.preventDefault();
282 }
283
284 function orderOf(c)
285 {
286         if (c === '~')
287                 return -1;
288         else if (c === '' || c >= '0' && c <= '9')
289                 return 0;
290         else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
291                 return c.charCodeAt(0);
292         else
293                 return c.charCodeAt(0) + 256;
294 }
295
296 function compareVersion(val, ref)
297 {
298         var vi = 0, ri = 0,
299             isdigit = { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 };
300
301         val = val || '';
302         ref = ref || '';
303
304         if (val === ref)
305                 return 0;
306
307         while (vi < val.length || ri < ref.length) {
308                 var first_diff = 0;
309
310                 while ((vi < val.length && !isdigit[val.charAt(vi)]) ||
311                        (ri < ref.length && !isdigit[ref.charAt(ri)])) {
312                         var vc = orderOf(val.charAt(vi)), rc = orderOf(ref.charAt(ri));
313                         if (vc !== rc)
314                                 return vc - rc;
315
316                         vi++; ri++;
317                 }
318
319                 while (val.charAt(vi) === '0')
320                         vi++;
321
322                 while (ref.charAt(ri) === '0')
323                         ri++;
324
325                 while (isdigit[val.charAt(vi)] && isdigit[ref.charAt(ri)]) {
326                         first_diff = first_diff || (val.charCodeAt(vi) - ref.charCodeAt(ri));
327                         vi++; ri++;
328                 }
329
330                 if (isdigit[val.charAt(vi)])
331                         return 1;
332                 else if (isdigit[ref.charAt(ri)])
333                         return -1;
334                 else if (first_diff)
335                         return first_diff;
336         }
337
338         return 0;
339 }
340
341 function versionSatisfied(ver, ref, vop)
342 {
343         var r = compareVersion(ver, ref);
344
345         switch (vop) {
346         case '<':
347         case '<=':
348                 return r <= 0;
349
350         case '>':
351         case '>=':
352                 return r >= 0;
353
354         case '<<':
355                 return r < 0;
356
357         case '>>':
358                 return r > 0;
359
360         case '=':
361                 return r == 0;
362         }
363
364         return false;
365 }
366
367 function pkgStatus(pkg, vop, ver, info)
368 {
369         info.errors = info.errors || [];
370         info.install = info.install || [];
371
372         if (pkg.installed) {
373                 if (vop && !versionSatisfied(pkg.version, ver, vop)) {
374                         var repl = null;
375
376                         (packages.available.providers[pkg.name] || []).forEach(function(p) {
377                                 if (!repl && versionSatisfied(p.version, ver, vop))
378                                         repl = p;
379                         });
380
381                         if (repl) {
382                                 info.install.push(repl);
383                                 return E('span', {
384                                         'class': 'label',
385                                         'data-tooltip': _('Requires update to %h %h')
386                                                 .format(repl.name, repl.version)
387                                 }, _('Needs upgrade'));
388                         }
389
390                         info.errors.push(_('The installed version of package <em>%h</em> is not compatible, require %s while %s is installed.').format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version)));
391
392                         return E('span', {
393                                 'class': 'label warning',
394                                 'data-tooltip': _('Require version %h %h,\ninstalled %h')
395                                         .format(vop, ver, pkg.version)
396                         }, _('Version incompatible'));
397                 }
398
399                 return E('span', { 'class': 'label notice' }, _('Installed'));
400         }
401         else if (!pkg.missing) {
402                 if (!vop || versionSatisfied(pkg.version, ver, vop)) {
403                         info.install.push(pkg);
404                         return E('span', { 'class': 'label' }, _('Not installed'));
405                 }
406
407                 info.errors.push(_('The repository version of package <em>%h</em> is not compatible, require %s but only %s is available.')
408                                 .format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version)));
409
410                 return E('span', {
411                         'class': 'label warning',
412                         'data-tooltip': _('Require version %h %h,\ninstalled %h')
413                                 .format(vop, ver, pkg.version)
414                 }, _('Version incompatible'));
415         }
416         else {
417                 info.errors.push(_('Required dependency package <em>%h</em> is not available in any repository.').format(pkg.name));
418
419                 return E('span', { 'class': 'label warning' }, _('Not available'));
420         }
421 }
422
423 function renderDependencyItem(dep, info)
424 {
425         var li = E('li'),
426             vop = dep.version ? dep.version[0] : null,
427             ver = dep.version ? dep.version[1] : null,
428             depends = [];
429
430         for (var i = 0; dep.pkgs && i < dep.pkgs.length; i++) {
431                 var pkg = packages.installed.pkgs[dep.pkgs[i]] ||
432                           packages.available.pkgs[dep.pkgs[i]] ||
433                           { name: dep.name };
434
435                 if (i > 0)
436                         li.appendChild(document.createTextNode(' | '));
437
438                 var text = pkg.name;
439
440                 if (pkg.installsize)
441                         text += ' (%.1024mB)'.format(pkg.installsize);
442                 else if (pkg.size)
443                         text += ' (~%.1024mB)'.format(pkg.size);
444
445                 li.appendChild(E('span', { 'data-tooltip': pkg.description },
446                         [ text, ' ', pkgStatus(pkg, vop, ver, info) ]));
447
448                 (pkg.depends || []).forEach(function(d) {
449                         if (depends.indexOf(d) === -1)
450                                 depends.push(d);
451                 });
452         }
453
454         if (!li.firstChild)
455                 li.appendChild(E('span', {},
456                         [ dep.name, ' ',
457                           pkgStatus({ name: dep.name, missing: true }, vop, ver, info) ]));
458
459         var subdeps = renderDependencies(depends, info);
460         if (subdeps)
461                 li.appendChild(subdeps);
462
463         return li;
464 }
465
466 function renderDependencies(depends, info)
467 {
468         var deps = depends || [],
469             items = [];
470
471         info.seen = info.seen || [];
472
473         for (var i = 0; i < deps.length; i++) {
474                 if (deps[i] === 'libc')
475                         continue;
476
477                 if (deps[i].match(/^(.+)\s+\((<=|<|>|>=|=|<<|>>)(.+)\)$/)) {
478                         dep = RegExp.$1.trim();
479                         vop = RegExp.$2.trim();
480                         ver = RegExp.$3.trim();
481                 }
482                 else {
483                         dep = deps[i].trim();
484                         vop = ver = null;
485                 }
486
487                 if (info.seen[dep])
488                         continue;
489
490                 var pkgs = [];
491
492                 (packages.installed.providers[dep] || []).forEach(function(p) {
493                         if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
494                 });
495
496                 (packages.available.providers[dep] || []).forEach(function(p) {
497                         if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
498                 });
499
500                 info.seen[dep] = {
501                         name:    dep,
502                         pkgs:    pkgs,
503                         version: [vop, ver]
504                 };
505
506                 items.push(renderDependencyItem(info.seen[dep], info));
507         }
508
509         if (items.length)
510                 return E('ul', { 'class': 'deps' }, items);
511
512         return null;
513 }
514
515 function truncateVersion(v, op)
516 {
517         v = v.replace(/\b(([a-f0-9]{8})[a-f0-9]{24,32})\b/,
518                 '<span data-tooltip="$1">$2…</span>');
519
520         if (!op || op === '=')
521                 return v;
522
523         return '%h %h'.format(op, v);
524 }
525
526 function handleReset(ev)
527 {
528         var filter = document.querySelector('input[name="filter"]');
529
530         filter.value = '';
531         display();
532 }
533
534 function handleInstall(ev)
535 {
536         var name = ev.target.getAttribute('data-package'),
537             pkg = packages.available.pkgs[name],
538             depcache = {},
539             size;
540
541         if (pkg.installsize)
542                 size = _('~%.1024mB installed').format(pkg.installsize);
543         else if (pkg.size)
544                 size = _('~%.1024mB compressed').format(pkg.size);
545         else
546                 size = _('unknown');
547
548         var deps = renderDependencies(pkg.depends, depcache),
549             tree = null, errs = null, inst = null, desc = null;
550
551         if (depcache.errors && depcache.errors.length) {
552                 errs = E('ul', { 'class': 'errors' });
553                 depcache.errors.forEach(function(err) {
554                         errs.appendChild(E('li', {}, err));
555                 });
556         }
557
558         var totalsize = pkg.installsize || pkg.size || 0,
559             totalpkgs = 1;
560
561         if (depcache.install && depcache.install.length)
562                 depcache.install.forEach(function(ipkg) {
563                         totalsize += ipkg.installsize || ipkg.size || 0;
564                         totalpkgs++;
565                 });
566
567         inst = E('p', {},
568                 _('Require approx. %.1024mB size for %d package(s) to install.')
569                         .format(totalsize, totalpkgs));
570
571         if (deps) {
572                 tree = E('li', '<strong>%s:</strong>'.format(_('Dependencies')));
573                 tree.appendChild(deps);
574         }
575
576         if (pkg.description) {
577                 desc = E('div', {}, [
578                         E('h5', {}, _('Description')),
579                         E('p', {}, pkg.description)
580                 ]);
581         }
582
583         L.showModal(_('Details for package <em>%h</em>').format(pkg.name), [
584                 E('ul', {}, [
585                         E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
586                         E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)),
587                         tree || '',
588                 ]),
589                 desc || '',
590                 errs || inst || '',
591                 E('div', { 'class': 'right' }, [
592                         E('label', { 'class': 'cbi-checkbox', 'style': 'float:left; padding-top:.5em' }, [
593                                 E('input', { 'type': 'checkbox', 'name': 'overwrite' }), ' ',
594                                 _('Overwrite files from other package(s)')
595                         ]),
596                         E('div', {
597                                 'class': 'btn',
598                                 'click': L.hideModal
599                         }, _('Cancel')),
600                         ' ',
601                         E('div', {
602                                 'data-command': 'install',
603                                 'data-package': name,
604                                 'class': 'btn cbi-button-action',
605                                 'click': handleOpkg
606                         }, _('Install'))
607                 ])
608         ]);
609 }
610
611 function handleManualInstall(ev)
612 {
613         var name_or_url = document.querySelector('input[name="install"]').value,
614             install = E('div', {
615                         'class': 'btn cbi-button-action',
616                         'data-command': 'install',
617                         'data-package': name_or_url,
618                         'click': function(ev) {
619                                 document.querySelector('input[name="install"]').value = '';
620                                 handleOpkg(ev);
621                         }
622                 }, _('Install')), warning;
623
624         if (!name_or_url.length) {
625                 return;
626         }
627         else if (name_or_url.indexOf('/') !== -1) {
628                 warning = E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(name_or_url));
629         }
630         else if (!packages.available.providers[name_or_url]) {
631                 warning = E('p', {}, _('The package <em>%h</em> is not available in any configured repository.').format(name_or_url));
632                 install = '';
633         }
634         else {
635                 warning = E('p', {}, _('Really attempt to install <em>%h</em>?').format(name_or_url));
636         }
637
638         L.showModal(_('Manually install package'), [
639                 warning,
640                 E('div', { 'class': 'right' }, [
641                         E('div', {
642                                 'click': L.hideModal,
643                                 'class': 'btn cbi-button-neutral'
644                         }, _('Cancel')),
645                         ' ', install
646                 ])
647         ]);
648 }
649
650 function handleConfig(ev)
651 {
652         L.showModal(_('OPKG Configuration'), [
653                 E('p', { 'class': 'spinning' }, _('Loading configuration data…'))
654         ]);
655
656         L.get('admin/system/opkg/config', null, function(xhr, conf) {
657                 var body = [
658                         E('p', {}, _('Below is a listing of the various configuration files used by <em>opkg</em>. Use <em>opkg.conf</em> for global settings and <em>customfeeds.conf</em> for custom repository entries. The configuration in the other files may be changed but is usually not preserved by <em>sysupgrade</em>.'))
659                 ];
660
661                 Object.keys(conf).sort().forEach(function(file) {
662                         body.push(E('h5', {}, '%h'.format(file)));
663                         body.push(E('textarea', {
664                                 'name': file,
665                                 'rows': Math.max(Math.min(conf[file].match(/\n/g).length, 10), 3)
666                         }, '%h'.format(conf[file])));
667                 });
668
669                 body.push(E('div', { 'class': 'right' }, [
670                         E('div', {
671                                 'class': 'btn cbi-button-neutral',
672                                 'click': L.hideModal
673                         }, _('Cancel')),
674                         ' ',
675                         E('div', {
676                                 'class': 'btn cbi-button-positive',
677                                 'click': function(ev) {
678                                         var data = {};
679                                         findParent(ev.target, '.modal').querySelectorAll('textarea[name]')
680                                                 .forEach(function(textarea) {
681                                                         data[textarea.getAttribute('name')] = textarea.value
682                                                 });
683
684                                         L.showModal(_('OPKG Configuration'), [
685                                                 E('p', { 'class': 'spinning' }, _('Saving configuration data…'))
686                                         ]);
687
688                                         L.post('admin/system/opkg/config', { data: JSON.stringify(data) }, L.hideModal);
689                                 }
690                         }, _('Save')),
691                 ]));
692
693                 L.showModal(_('OPKG Configuration'), body);
694         });
695 }
696
697 function handleRemove(ev)
698 {
699         var name = ev.target.getAttribute('data-package'),
700             pkg = packages.installed.pkgs[name],
701             avail = packages.available.pkgs[name] || {},
702             size, desc;
703
704         if (avail.installsize)
705                 size = _('~%.1024mB installed').format(avail.installsize);
706         else if (avail.size)
707                 size = _('~%.1024mB compressed').format(avail.size);
708         else
709                 size = _('unknown');
710
711         if (avail.description) {
712                 desc = E('div', {}, [
713                         E('h5', {}, _('Description')),
714                         E('p', {}, avail.description)
715                 ]);
716         }
717
718         L.showModal(_('Remove package <em>%h</em>').format(pkg.name), [
719                 E('ul', {}, [
720                         E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
721                         E('li', '<strong>%s:</strong> %h'.format(_('Size'), size))
722                 ]),
723                 desc || '',
724                 E('div', { 'style': 'display:flex; justify-content:space-between; flex-wrap:wrap' }, [
725                         E('label', {}, [
726                                 E('input', { type: 'checkbox', checked: 'checked', name: 'autoremove' }),
727                                 _('Automatically remove unused dependencies')
728                         ]),
729                         E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [
730                                 E('div', {
731                                         'class': 'btn',
732                                         'click': L.hideModal
733                                 }, _('Cancel')),
734                                 ' ',
735                                 E('div', {
736                                         'data-command': 'remove',
737                                         'data-package': name,
738                                         'class': 'btn cbi-button-negative',
739                                         'click': handleOpkg
740                                 }, _('Remove'))
741                         ])
742                 ])
743         ]);
744 }
745
746 function handleOpkg(ev)
747 {
748         return new Promise(function(resolveFn, rejectFn) {
749                 var cmd = ev.target.getAttribute('data-command'),
750                     pkg = ev.target.getAttribute('data-package'),
751                     rem = document.querySelector('input[name="autoremove"]'),
752                     owr = document.querySelector('input[name="overwrite"]'),
753                     url = 'admin/system/opkg/exec/' + encodeURIComponent(cmd);
754
755                 var dlg = L.showModal(_('Executing package manager'), [
756                         E('p', { 'class': 'spinning' },
757                                 _('Waiting for the <em>opkg %h</em> command to complete…').format(cmd))
758                 ]);
759
760                 L.post(url, { package: pkg, autoremove: rem ? rem.checked : false, overwrite: owr ? owr.checked : false }, function(xhr, res) {
761                         dlg.removeChild(dlg.lastChild);
762
763                         if (res.stdout)
764                                 dlg.appendChild(E('pre', [ res.stdout ]));
765
766                         if (res.stderr) {
767                                 dlg.appendChild(E('h5', _('Errors')));
768                                 dlg.appendChild(E('pre', { 'class': 'errors' }, [ res.stderr ]));
769                         }
770
771                         if (res.code !== 0)
772                                 dlg.appendChild(E('p', _('The <em>opkg %h</em> command failed with code <code>%d</code>.').format(cmd, (res.code & 0xff) || -1)));
773
774                         dlg.appendChild(E('div', { 'class': 'right' },
775                                 E('div', {
776                                         'class': 'btn',
777                                         'click': L.bind(function(res) {
778                                                 L.hideModal();
779                                                 updateLists();
780
781                                                 if (res.code !== 0)
782                                                         rejectFn(new Error(res.stderr || 'opkg error %d'.format(res.code)));
783                                                 else
784                                                         resolveFn(res);
785                                         }, this, res)
786                                 }, _('Dismiss'))));
787                 });
788         });
789 }
790
791 function handleUpload(ev)
792 {
793         var path = '/tmp/upload.ipk';
794         return L.ui.uploadFile(path).then(L.bind(function(btn, res) {
795                 L.showModal(_('Manually install package'), [
796                         E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(res.name)),
797                         E('ul', {}, [
798                                 res.size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res.size)) : '',
799                                 res.checksum ? E('li', {}, '%s: %s'.format(_('MD5'), res.checksum)) : '',
800                                 res.sha256sum ? E('li', {}, '%s: %s'.format(_('SHA256'), res.sha256sum)) : ''
801                         ]),
802                         E('div', { 'class': 'right' }, [
803                                 E('div', {
804                                         'click': function(ev) {
805                                                 L.hideModal();
806                                                 L.fs.remove(path);
807                                         },
808                                         'class': 'btn cbi-button-neutral'
809                                 }, _('Cancel')), ' ',
810                                 E('div', {
811                                         'class': 'btn cbi-button-action',
812                                         'data-command': 'install',
813                                         'data-package': path,
814                                         'click': function(ev) {
815                                                 handleOpkg(ev).finally(function() {
816                                                         L.fs.remove(path)
817                                                 });
818                                         }
819                                 }, _('Install'))
820                         ])
821                 ]);
822         }, this, ev.target));
823 }
824
825 function updateLists()
826 {
827         cbi_update_table('#packages', [],
828                 E('div', { 'class': 'spinning' }, _('Loading package information…')));
829
830         packages.available = { providers: {}, pkgs: {} };
831         packages.installed = { providers: {}, pkgs: {} };
832
833         L.get('admin/system/opkg/statvfs', null, function(xhr, stat) {
834                 var pg = document.querySelector('.cbi-progressbar'),
835                     total = stat.blocks || 0,
836                     free = stat.bfree || 0;
837
838                 pg.firstElementChild.style.width = Math.floor(total ? ((100 / total) * free) : 100) + '%';
839                 pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, free * (stat.frsize || 0)));
840
841                 L.get('admin/system/opkg/list/available', null, function(xhr) {
842                         parseList(xhr.responseText, packages.available);
843                         L.get('admin/system/opkg/list/installed', null, function(xhr) {
844                                 parseList(xhr.responseText, packages.installed);
845                                 display(document.querySelector('input[name="filter"]').value);
846                         });
847                 });
848         });
849 }
850
851 window.requestAnimationFrame(function() {
852         var filter = document.querySelector('input[name="filter"]'),
853             keyTimeout = null;
854
855         filter.value = filter.getAttribute('value');
856         filter.addEventListener('keyup',
857                 function(ev) {
858                         if (keyTimeout !== null)
859                                 window.clearTimeout(keyTimeout);
860
861                         keyTimeout = window.setTimeout(function() {
862                                 display(ev.target.value);
863                         }, 250);
864                 });
865
866         document.querySelector('#pager > .prev').addEventListener('click', handlePage);
867         document.querySelector('#pager > .next').addEventListener('click', handlePage);
868         document.querySelector('.cbi-tabmenu.mode').addEventListener('click', handleMode);
869
870         updateLists();
871 });