Fix broken repository link in target/makeccs
[librecmc/librecmc.git] / package / luci / 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                         if (!avail || compareVersion(avail.version, pkg.version) <= 0)
139                                 continue;
140
141                         ver = '%s » %s'.format(
142                                 truncateVersion(pkg.version || '-'),
143                                 truncateVersion(avail.version || '-'));
144
145                         btn = E('div', {
146                                 'class': 'btn cbi-button-positive',
147                                 'data-package': name,
148                                 'click': handleInstall
149                         }, _('Upgrade…'));
150                 }
151                 else if (currentDisplayMode === 'installed') {
152                         ver = truncateVersion(pkg.version || '-');
153                         btn = E('div', {
154                                 'class': 'btn cbi-button-negative',
155                                 'data-package': name,
156                                 'click': handleRemove
157                         }, _('Remove'));
158                 }
159                 else {
160                         ver = truncateVersion(pkg.version || '-');
161
162                         if (!packages.installed.pkgs[name])
163                                 btn = E('div', {
164                                         'class': 'btn cbi-button-action',
165                                         'data-package': name,
166                                         'click': handleInstall
167                                 }, _('Install…'));
168                         else if (packages.installed.pkgs[name].version != pkg.version)
169                                 btn = E('div', {
170                                         'class': 'btn cbi-button-positive',
171                                         'data-package': name,
172                                         'click': handleInstall
173                                 }, _('Upgrade…'));
174                         else
175                                 btn = E('div', {
176                                         'class': 'btn cbi-button-neutral',
177                                         'disabled': 'disabled'
178                                 }, _('Installed'));
179                 }
180
181                 name = '%h'.format(name);
182                 desc = '%h'.format(desc || '-');
183
184                 if (pattern) {
185                         name = name.replace(pattern, '<ins>$&</ins>');
186                         desc = desc.replace(pattern, '<ins>$&</ins>');
187                 }
188
189                 currentDisplayRows.push([
190                         name,
191                         ver,
192                         pkg.size ? '%.1024mB'.format(pkg.size)
193                                  : (altsize ? '~%.1024mB'.format(altsize) : '-'),
194                         desc,
195                         btn
196                 ]);
197         }
198
199         currentDisplayRows.sort(function(a, b) {
200                 if (a[0] < b[0])
201                         return -1;
202                 else if (a[0] > b[0])
203                         return 1;
204                 else
205                         return 0;
206         });
207
208         pager.parentNode.style.display = '';
209         pager.setAttribute('data-offset', 100);
210         handlePage({ target: pager.querySelector('.prev') });
211 }
212
213 function handlePage(ev)
214 {
215         var filter = document.querySelector('input[name="filter"]'),
216             pager = ev.target.parentNode,
217             offset = +pager.getAttribute('data-offset'),
218             next = ev.target.classList.contains('next');
219
220         if ((next && (offset + 100) >= currentDisplayRows.length) ||
221             (!next && (offset < 100)))
222             return;
223
224         offset += next ? 100 : -100;
225         pager.setAttribute('data-offset', offset);
226         pager.querySelector('.text').firstChild.data = currentDisplayRows.length
227                 ? _('Displaying %d-%d of %d').format(1 + offset, Math.min(offset + 100, currentDisplayRows.length), currentDisplayRows.length)
228                 : _('No packages');
229
230         if (offset < 100)
231                 pager.querySelector('.prev').setAttribute('disabled', 'disabled');
232         else
233                 pager.querySelector('.prev').removeAttribute('disabled');
234
235         if ((offset + 100) >= currentDisplayRows.length)
236                 pager.querySelector('.next').setAttribute('disabled', 'disabled');
237         else
238                 pager.querySelector('.next').removeAttribute('disabled');
239
240         var placeholder = _('No information available');
241
242         if (filter.value)
243                 placeholder = [
244                         E('span', {}, _('No packages matching "<strong>%h</strong>".').format(filter.value)), ' (',
245                         E('a', { href: '#', onclick: 'handleReset(event)' }, _('Reset')), ')'
246                 ];
247
248         cbi_update_table('#packages', currentDisplayRows.slice(offset, offset + 100),
249                 placeholder);
250 }
251
252 function handleMode(ev)
253 {
254         var tab = findParent(ev.target, 'li');
255         if (tab.getAttribute('data-mode') === currentDisplayMode)
256                 return;
257
258         tab.parentNode.querySelectorAll('li').forEach(function(li) {
259                 li.classList.remove('cbi-tab');
260                 li.classList.add('cbi-tab-disabled');
261         });
262
263         tab.classList.remove('cbi-tab-disabled');
264         tab.classList.add('cbi-tab');
265
266         currentDisplayMode = tab.getAttribute('data-mode');
267
268         display(document.querySelector('input[name="filter"]').value);
269
270         ev.target.blur();
271         ev.preventDefault();
272 }
273
274 function orderOf(c)
275 {
276         if (c === '~')
277                 return -1;
278         else if (c === '' || c >= '0' && c <= '9')
279                 return 0;
280         else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
281                 return c.charCodeAt(0);
282         else
283                 return c.charCodeAt(0) + 256;
284 }
285
286 function compareVersion(val, ref)
287 {
288         var vi = 0, ri = 0,
289             isdigit = { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 };
290
291         val = val || '';
292         ref = ref || '';
293
294         if (val === ref)
295                 return 0;
296
297         while (vi < val.length || ri < ref.length) {
298                 var first_diff = 0;
299
300                 while ((vi < val.length && !isdigit[val.charAt(vi)]) ||
301                        (ri < ref.length && !isdigit[ref.charAt(ri)])) {
302                         var vc = orderOf(val.charAt(vi)), rc = orderOf(ref.charAt(ri));
303                         if (vc !== rc)
304                                 return vc - rc;
305
306                         vi++; ri++;
307                 }
308
309                 while (val.charAt(vi) === '0')
310                         vi++;
311
312                 while (ref.charAt(ri) === '0')
313                         ri++;
314
315                 while (isdigit[val.charAt(vi)] && isdigit[ref.charAt(ri)]) {
316                         first_diff = first_diff || (val.charCodeAt(vi) - ref.charCodeAt(ri));
317                         vi++; ri++;
318                 }
319
320                 if (isdigit[val.charAt(vi)])
321                         return 1;
322                 else if (isdigit[ref.charAt(ri)])
323                         return -1;
324                 else if (first_diff)
325                         return first_diff;
326         }
327
328         return 0;
329 }
330
331 function versionSatisfied(ver, ref, vop)
332 {
333         var r = compareVersion(ver, ref);
334
335         switch (vop) {
336         case '<':
337         case '<=':
338                 return r <= 0;
339
340         case '>':
341         case '>=':
342                 return r >= 0;
343
344         case '<<':
345                 return r < 0;
346
347         case '>>':
348                 return r > 0;
349
350         case '=':
351                 return r == 0;
352         }
353
354         return false;
355 }
356
357 function pkgStatus(pkg, vop, ver, info)
358 {
359         info.errors = info.errors || [];
360         info.install = info.install || [];
361
362         if (pkg.installed) {
363                 if (vop && !versionSatisfied(pkg.version, ver, vop)) {
364                         var repl = null;
365
366                         (packages.available.providers[pkg.name] || []).forEach(function(p) {
367                                 if (!repl && versionSatisfied(p.version, ver, vop))
368                                         repl = p;
369                         });
370
371                         if (repl) {
372                                 info.install.push(repl);
373                                 return E('span', {
374                                         'class': 'label',
375                                         'data-tooltip': _('Requires update to %h %h')
376                                                 .format(repl.name, repl.version)
377                                 }, _('Needs upgrade'));
378                         }
379
380                         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)));
381
382                         return E('span', {
383                                 'class': 'label warning',
384                                 'data-tooltip': _('Require version %h %h,\ninstalled %h')
385                                         .format(vop, ver, pkg.version)
386                         }, _('Version incompatible'));
387                 }
388
389                 return E('span', { 'class': 'label notice' }, _('Installed'));
390         }
391         else if (!pkg.missing) {
392                 if (!vop || versionSatisfied(pkg.version, ver, vop)) {
393                         info.install.push(pkg);
394                         return E('span', { 'class': 'label' }, _('Not installed'));
395                 }
396
397                 info.errors.push(_('The repository version of package <em>%h</em> is not compatible, require %s but only %s is available.')
398                                 .format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version)));
399
400                 return E('span', {
401                         'class': 'label warning',
402                         'data-tooltip': _('Require version %h %h,\ninstalled %h')
403                                 .format(vop, ver, pkg.version)
404                 }, _('Version incompatible'));
405         }
406         else {
407                 info.errors.push(_('Required dependency package <em>%h</em> is not available in any repository.').format(pkg.name));
408
409                 return E('span', { 'class': 'label warning' }, _('Not available'));
410         }
411 }
412
413 function renderDependencyItem(dep, info)
414 {
415         var li = E('li'),
416             vop = dep.version ? dep.version[0] : null,
417             ver = dep.version ? dep.version[1] : null,
418             depends = [];
419
420         for (var i = 0; dep.pkgs && i < dep.pkgs.length; i++) {
421                 var pkg = packages.installed.pkgs[dep.pkgs[i]] ||
422                           packages.available.pkgs[dep.pkgs[i]] ||
423                           { name: dep.name };
424
425                 if (i > 0)
426                         li.appendChild(document.createTextNode(' | '));
427
428                 var text = pkg.name;
429
430                 if (pkg.installsize)
431                         text += ' (%.1024mB)'.format(pkg.installsize);
432                 else if (pkg.size)
433                         text += ' (~%.1024mB)'.format(pkg.size);
434
435                 li.appendChild(E('span', { 'data-tooltip': pkg.description },
436                         [ text, ' ', pkgStatus(pkg, vop, ver, info) ]));
437
438                 (pkg.depends || []).forEach(function(d) {
439                         if (depends.indexOf(d) === -1)
440                                 depends.push(d);
441                 });
442         }
443
444         if (!li.firstChild)
445                 li.appendChild(E('span', {},
446                         [ dep.name, ' ',
447                           pkgStatus({ name: dep.name, missing: true }, vop, ver, info) ]));
448
449         var subdeps = renderDependencies(depends, info);
450         if (subdeps)
451                 li.appendChild(subdeps);
452
453         return li;
454 }
455
456 function renderDependencies(depends, info)
457 {
458         var deps = depends || [],
459             items = [];
460
461         info.seen = info.seen || [];
462
463         for (var i = 0; i < deps.length; i++) {
464                 if (deps[i] === 'libc')
465                         continue;
466
467                 if (deps[i].match(/^(.+)\s+\((<=|<|>|>=|=|<<|>>)(.+)\)$/)) {
468                         dep = RegExp.$1.trim();
469                         vop = RegExp.$2.trim();
470                         ver = RegExp.$3.trim();
471                 }
472                 else {
473                         dep = deps[i].trim();
474                         vop = ver = null;
475                 }
476
477                 if (info.seen[dep])
478                         continue;
479
480                 var pkgs = [];
481
482                 (packages.installed.providers[dep] || []).forEach(function(p) {
483                         if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
484                 });
485
486                 (packages.available.providers[dep] || []).forEach(function(p) {
487                         if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
488                 });
489
490                 info.seen[dep] = {
491                         name:    dep,
492                         pkgs:    pkgs,
493                         version: [vop, ver]
494                 };
495
496                 items.push(renderDependencyItem(info.seen[dep], info));
497         }
498
499         if (items.length)
500                 return E('ul', { 'class': 'deps' }, items);
501
502         return null;
503 }
504
505 function truncateVersion(v, op)
506 {
507         v = v.replace(/\b(([a-f0-9]{8})[a-f0-9]{24,32})\b/,
508                 '<span data-tooltip="$1">$2…</span>');
509
510         if (!op || op === '=')
511                 return v;
512
513         return '%h %h'.format(op, v);
514 }
515
516 function handleReset(ev)
517 {
518         var filter = document.querySelector('input[name="filter"]');
519
520         filter.value = '';
521         display();
522 }
523
524 function handleInstall(ev)
525 {
526         var name = ev.target.getAttribute('data-package'),
527             pkg = packages.available.pkgs[name],
528             depcache = {},
529             size;
530
531         if (pkg.installsize)
532                 size = _('~%.1024mB installed').format(pkg.installsize);
533         else if (pkg.size)
534                 size = _('~%.1024mB compressed').format(pkg.size);
535         else
536                 size = _('unknown');
537
538         var deps = renderDependencies(pkg.depends, depcache),
539             tree = null, errs = null, inst = null, desc = null;
540
541         if (depcache.errors && depcache.errors.length) {
542                 errs = E('ul', { 'class': 'errors' });
543                 depcache.errors.forEach(function(err) {
544                         errs.appendChild(E('li', {}, err));
545                 });
546         }
547
548         var totalsize = pkg.installsize || pkg.size || 0,
549             totalpkgs = 1;
550
551         if (depcache.install && depcache.install.length)
552                 depcache.install.forEach(function(ipkg) {
553                         totalsize += ipkg.installsize || ipkg.size || 0;
554                         totalpkgs++;
555                 });
556
557         inst = E('p', {},
558                 _('Require approx. %.1024mB size for %d package(s) to install.')
559                         .format(totalsize, totalpkgs));
560
561         if (deps) {
562                 tree = E('li', '<strong>%s:</strong>'.format(_('Dependencies')));
563                 tree.appendChild(deps);
564         }
565
566         if (pkg.description) {
567                 desc = E('div', {}, [
568                         E('h5', {}, _('Description')),
569                         E('p', {}, pkg.description)
570                 ]);
571         }
572
573         L.showModal(_('Details for package <em>%h</em>').format(pkg.name), [
574                 E('ul', {}, [
575                         E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
576                         E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)),
577                         tree || '',
578                 ]),
579                 desc || '',
580                 errs || inst || '',
581                 E('div', { 'class': 'right' }, [
582                         E('div', {
583                                 'class': 'btn',
584                                 'click': L.hideModal
585                         }, _('Cancel')),
586                         ' ',
587                         E('div', {
588                                 'data-command': 'install',
589                                 'data-package': name,
590                                 'class': 'btn cbi-button-action',
591                                 'click': handleOpkg
592                         }, _('Install'))
593                 ])
594         ]);
595 }
596
597 function handleManualInstall(ev)
598 {
599         var name_or_url = document.querySelector('input[name="install"]').value,
600             install = E('div', {
601                         'class': 'btn cbi-button-action',
602                         'data-command': 'install',
603                         'data-package': name_or_url,
604                         'click': function(ev) {
605                                 document.querySelector('input[name="install"]').value = '';
606                                 handleOpkg(ev);
607                         }
608                 }, _('Install')), warning;
609
610         if (!name_or_url.length) {
611                 return;
612         }
613         else if (name_or_url.indexOf('/') !== -1) {
614                 warning = E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(name_or_url));
615         }
616         else if (!packages.available.providers[name_or_url]) {
617                 warning = E('p', {}, _('The package <em>%h</em> is not available in any configured repository.').format(name_or_url));
618                 install = '';
619         }
620         else {
621                 warning = E('p', {}, _('Really attempt to install <em>%h</em>?').format(name_or_url));
622         }
623
624         L.showModal(_('Manually install package'), [
625                 warning,
626                 E('div', { 'class': 'right' }, [
627                         E('div', {
628                                 'click': L.hideModal,
629                                 'class': 'btn cbi-button-neutral'
630                         }, _('Cancel')),
631                         ' ', install
632                 ])
633         ]);
634 }
635
636 function handleConfig(ev)
637 {
638         L.showModal(_('OPKG Configuration'), [
639                 E('p', { 'class': 'spinning' }, _('Loading configuration data…'))
640         ]);
641
642         L.get('admin/system/opkg/config', null, function(xhr, conf) {
643                 var body = [
644                         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>.'))
645                 ];
646
647                 Object.keys(conf).sort().forEach(function(file) {
648                         body.push(E('h5', {}, '%h'.format(file)));
649                         body.push(E('textarea', {
650                                 'name': file,
651                                 'rows': Math.max(Math.min(conf[file].match(/\n/g).length, 10), 3)
652                         }, '%h'.format(conf[file])));
653                 });
654
655                 body.push(E('div', { 'class': 'right' }, [
656                         E('div', {
657                                 'class': 'btn cbi-button-neutral',
658                                 'click': L.hideModal
659                         }, _('Cancel')),
660                         ' ',
661                         E('div', {
662                                 'class': 'btn cbi-button-positive',
663                                 'click': function(ev) {
664                                         var data = {};
665                                         findParent(ev.target, '.modal').querySelectorAll('textarea[name]')
666                                                 .forEach(function(textarea) {
667                                                         data[textarea.getAttribute('name')] = textarea.value
668                                                 });
669
670                                         L.showModal(_('OPKG Configuration'), [
671                                                 E('p', { 'class': 'spinning' }, _('Saving configuration data…'))
672                                         ]);
673
674                                         L.post('admin/system/opkg/config', { data: JSON.stringify(data) }, L.hideModal);
675                                 }
676                         }, _('Save')),
677                 ]));
678
679                 L.showModal(_('OPKG Configuration'), body);
680         });
681 }
682
683 function handleRemove(ev)
684 {
685         var name = ev.target.getAttribute('data-package'),
686             pkg = packages.installed.pkgs[name],
687             avail = packages.available.pkgs[name] || {},
688             size, desc;
689
690         if (avail.installsize)
691                 size = _('~%.1024mB installed').format(avail.installsize);
692         else if (avail.size)
693                 size = _('~%.1024mB compressed').format(avail.size);
694         else
695                 size = _('unknown');
696
697         if (avail.description) {
698                 desc = E('div', {}, [
699                         E('h5', {}, _('Description')),
700                         E('p', {}, avail.description)
701                 ]);
702         }
703
704         L.showModal(_('Remove package <em>%h</em>').format(pkg.name), [
705                 E('ul', {}, [
706                         E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
707                         E('li', '<strong>%s:</strong> %h'.format(_('Size'), size))
708                 ]),
709                 desc || '',
710                 E('div', { 'style': 'display:flex; justify-content:space-between; flex-wrap:wrap' }, [
711                         E('label', {}, [
712                                 E('input', { type: 'checkbox', checked: 'checked', name: 'autoremove' }),
713                                 _('Automatically remove unused dependencies')
714                         ]),
715                         E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [
716                                 E('div', {
717                                         'class': 'btn',
718                                         'click': L.hideModal
719                                 }, _('Cancel')),
720                                 ' ',
721                                 E('div', {
722                                         'data-command': 'remove',
723                                         'data-package': name,
724                                         'class': 'btn cbi-button-negative',
725                                         'click': handleOpkg
726                                 }, _('Remove'))
727                         ])
728                 ])
729         ]);
730 }
731
732 function handleOpkg(ev)
733 {
734         var cmd = ev.target.getAttribute('data-command'),
735             pkg = ev.target.getAttribute('data-package'),
736             rem = document.querySelector('input[name="autoremove"]'),
737             url = 'admin/system/opkg/exec/' + encodeURIComponent(cmd);
738
739         var dlg = L.showModal(_('Executing package manager'), [
740                 E('p', { 'class': 'spinning' },
741                         _('Waiting for the <em>opkg %h</em> command to complete…').format(cmd))
742         ]);
743
744         L.post(url, { package: pkg, autoremove: rem ? rem.checked : false }, function(xhr, res) {
745                 dlg.removeChild(dlg.lastChild);
746
747                 if (res.stdout)
748                         dlg.appendChild(E('pre', [ res.stdout ]));
749
750                 if (res.stderr) {
751                         dlg.appendChild(E('h5', _('Errors')));
752                         dlg.appendChild(E('pre', { 'class': 'errors' }, [ res.stderr ]));
753                 }
754
755                 if (res.code !== 0)
756                         dlg.appendChild(E('p', _('The <em>opkg %h</em> command failed with code <code>%d</code>.').format(cmd, (res.code & 0xff) || -1)));
757
758                 dlg.appendChild(E('div', { 'class': 'right' },
759                         E('div', {
760                                 'class': 'btn',
761                                 'click': function() {
762                                         L.hideModal();
763                                         updateLists();
764                                 }
765                         }, _('Dismiss'))));
766         });
767 }
768
769 function updateLists()
770 {
771         cbi_update_table('#packages', [],
772                 E('div', { 'class': 'spinning' }, _('Loading package information…')));
773
774         packages.available = { providers: {}, pkgs: {} };
775         packages.installed = { providers: {}, pkgs: {} };
776
777         L.get('admin/system/opkg/statvfs', null, function(xhr, stat) {
778                 var pg = document.querySelector('.cbi-progressbar'),
779                     total = stat.blocks || 0,
780                     free = stat.bfree || 0;
781
782                 pg.firstElementChild.style.width = Math.floor(total ? ((100 / total) * free) : 100) + '%';
783                 pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, free * (stat.frsize || 0)));
784
785                 L.get('admin/system/opkg/list/available', null, function(xhr) {
786                         parseList(xhr.responseText, packages.available);
787                         L.get('admin/system/opkg/list/installed', null, function(xhr) {
788                                 parseList(xhr.responseText, packages.installed);
789                                 display(document.querySelector('input[name="filter"]').value);
790                         });
791                 });
792         });
793 }
794
795 window.requestAnimationFrame(function() {
796         var filter = document.querySelector('input[name="filter"]'),
797             keyTimeout = null;
798
799         filter.value = filter.getAttribute('value');
800         filter.addEventListener('keyup',
801                 function(ev) {
802                         if (keyTimeout !== null)
803                                 window.clearTimeout(keyTimeout);
804
805                         keyTimeout = window.setTimeout(function() {
806                                 display(ev.target.value);
807                         }, 250);
808                 });
809
810         document.querySelector('#pager > .prev').addEventListener('click', handlePage);
811         document.querySelector('#pager > .next').addEventListener('click', handlePage);
812         document.querySelector('.cbi-tabmenu.mode').addEventListener('click', handleMode);
813
814         updateLists();
815 });