luci-app-opkg: fix parsing empty package repository indexes
[project/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 if (pkg) {
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(L.toArray(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 if (L.ui.menu && L.ui.menu.flushCache)
779 L.ui.menu.flushCache();
780
781 L.hideModal();
782 updateLists();
783
784 if (res.code !== 0)
785 rejectFn(new Error(res.stderr || 'opkg error %d'.format(res.code)));
786 else
787 resolveFn(res);
788 }, this, res)
789 }, _('Dismiss'))));
790 });
791 });
792 }
793
794 function handleUpload(ev)
795 {
796 var path = '/tmp/upload.ipk';
797 return L.ui.uploadFile(path).then(L.bind(function(btn, res) {
798 L.showModal(_('Manually install package'), [
799 E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(res.name)),
800 E('ul', {}, [
801 res.size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res.size)) : '',
802 res.checksum ? E('li', {}, '%s: %s'.format(_('MD5'), res.checksum)) : '',
803 res.sha256sum ? E('li', {}, '%s: %s'.format(_('SHA256'), res.sha256sum)) : ''
804 ]),
805 E('div', { 'class': 'right' }, [
806 E('div', {
807 'click': function(ev) {
808 L.hideModal();
809 L.fs.remove(path);
810 },
811 'class': 'btn cbi-button-neutral'
812 }, _('Cancel')), ' ',
813 E('div', {
814 'class': 'btn cbi-button-action',
815 'data-command': 'install',
816 'data-package': path,
817 'click': function(ev) {
818 handleOpkg(ev).finally(function() {
819 L.fs.remove(path)
820 });
821 }
822 }, _('Install'))
823 ])
824 ]);
825 }, this, ev.target));
826 }
827
828 function updateLists()
829 {
830 cbi_update_table('#packages', [],
831 E('div', { 'class': 'spinning' }, _('Loading package information…')));
832
833 packages.available = { providers: {}, pkgs: {} };
834 packages.installed = { providers: {}, pkgs: {} };
835
836 L.get('admin/system/opkg/statvfs', null, function(xhr, stat) {
837 var pg = document.querySelector('.cbi-progressbar'),
838 total = stat.blocks || 0,
839 free = stat.bfree || 0;
840
841 pg.firstElementChild.style.width = Math.floor(total ? ((100 / total) * free) : 100) + '%';
842 pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, free * (stat.frsize || 0)));
843
844 L.get('admin/system/opkg/list/available', null, function(xhr) {
845 parseList(xhr.responseText, packages.available);
846 L.get('admin/system/opkg/list/installed', null, function(xhr) {
847 parseList(xhr.responseText, packages.installed);
848 display(document.querySelector('input[name="filter"]').value);
849 });
850 });
851 });
852 }
853
854 window.requestAnimationFrame(function() {
855 var filter = document.querySelector('input[name="filter"]'),
856 keyTimeout = null;
857
858 filter.value = filter.getAttribute('value');
859 filter.addEventListener('keyup',
860 function(ev) {
861 if (keyTimeout !== null)
862 window.clearTimeout(keyTimeout);
863
864 keyTimeout = window.setTimeout(function() {
865 display(ev.target.value);
866 }, 250);
867 });
868
869 document.querySelector('#pager > .prev').addEventListener('click', handlePage);
870 document.querySelector('#pager > .next').addEventListener('click', handlePage);
871 document.querySelector('.cbi-tabmenu.mode').addEventListener('click', handleMode);
872
873 updateLists();
874 });