b7a52d1d1294225ed4cf1857da8500f1514b232e
[project/luci.git] / applications / luci-app-opkg / htdocs / luci-static / resources / view / opkg.js
1 'use strict';
2 'require fs';
3 'require ui';
4 'require rpc';
5
6 var css = ' \
7 .controls { \
8 display: flex; \
9 margin: .5em 0 1em 0; \
10 flex-wrap: wrap; \
11 justify-content: space-around; \
12 } \
13 \
14 .controls > * { \
15 padding: .25em; \
16 white-space: nowrap; \
17 flex: 1 1 33%; \
18 box-sizing: border-box; \
19 display: flex; \
20 flex-wrap: wrap; \
21 } \
22 \
23 .controls > *:first-child, \
24 .controls > * > label { \
25 flex-basis: 100%; \
26 min-width: 250px; \
27 } \
28 \
29 .controls > *:nth-child(2), \
30 .controls > *:nth-child(3) { \
31 flex-basis: 20%; \
32 } \
33 \
34 .controls > * > .btn { \
35 flex-basis: 20px; \
36 text-align: center; \
37 } \
38 \
39 .controls > * > * { \
40 flex-grow: 1; \
41 align-self: center; \
42 } \
43 \
44 .controls > div > input { \
45 width: auto; \
46 } \
47 \
48 .td.version, \
49 .td.size { \
50 white-space: nowrap; \
51 } \
52 \
53 ul.deps, ul.deps ul, ul.errors { \
54 margin-left: 1em; \
55 } \
56 \
57 ul.deps li, ul.errors li { \
58 list-style: none; \
59 } \
60 \
61 ul.deps li:before { \
62 content: "↳"; \
63 display: inline-block; \
64 width: 1em; \
65 margin-left: -1em; \
66 } \
67 \
68 ul.deps li > span { \
69 white-space: nowrap; \
70 } \
71 \
72 ul.errors li { \
73 color: #c44; \
74 font-size: 90%; \
75 font-weight: bold; \
76 padding-left: 1.5em; \
77 } \
78 \
79 ul.errors li:before { \
80 content: "⚠"; \
81 display: inline-block; \
82 width: 1.5em; \
83 margin-left: -1.5em; \
84 } \
85 ';
86
87 var callMountPoints = rpc.declare({
88 object: 'luci',
89 method: 'getMountPoints',
90 expect: { result: [] }
91 });
92
93 var packages = {
94 available: { providers: {}, pkgs: {} },
95 installed: { providers: {}, pkgs: {} }
96 };
97
98 var currentDisplayMode = 'available', currentDisplayRows = [];
99
100 function parseList(s, dest)
101 {
102 var re = /([^\n]*)\n/g,
103 pkg = null, key = null, val = null, m;
104
105 while ((m = re.exec(s)) !== null) {
106 if (m[1].match(/^\s(.*)$/)) {
107 if (pkg !== null && key !== null && val !== null)
108 val += '\n' + RegExp.$1.trim();
109
110 continue;
111 }
112
113 if (key !== null && val !== null) {
114 switch (key) {
115 case 'package':
116 pkg = { name: val };
117 break;
118
119 case 'depends':
120 case 'provides':
121 var list = val.split(/\s*,\s*/);
122 if (list.length !== 1 || list[0].length > 0)
123 pkg[key] = list;
124 break;
125
126 case 'installed-time':
127 pkg.installtime = new Date(+val * 1000);
128 break;
129
130 case 'installed-size':
131 pkg.installsize = +val;
132 break;
133
134 case 'status':
135 var stat = val.split(/\s+/),
136 mode = stat[1],
137 installed = stat[2];
138
139 switch (mode) {
140 case 'user':
141 case 'hold':
142 pkg[mode] = true;
143 break;
144 }
145
146 switch (installed) {
147 case 'installed':
148 pkg.installed = true;
149 break;
150 }
151 break;
152
153 case 'essential':
154 if (val === 'yes')
155 pkg.essential = true;
156 break;
157
158 case 'size':
159 pkg.size = +val;
160 break;
161
162 case 'architecture':
163 case 'auto-installed':
164 case 'filename':
165 case 'sha256sum':
166 case 'section':
167 break;
168
169 default:
170 pkg[key] = val;
171 break;
172 }
173
174 key = val = null;
175 }
176
177 if (m[1].trim().match(/^([\w-]+)\s*:(.+)$/)) {
178 key = RegExp.$1.toLowerCase();
179 val = RegExp.$2.trim();
180 }
181 else {
182 dest.pkgs[pkg.name] = pkg;
183
184 var provides = dest.providers[pkg.name] ? [] : [ pkg.name ];
185
186 if (pkg.provides)
187 provides.push.apply(provides, pkg.provides);
188
189 provides.forEach(function(p) {
190 dest.providers[p] = dest.providers[p] || [];
191 dest.providers[p].push(pkg);
192 });
193 }
194 }
195 }
196
197 function display(pattern)
198 {
199 var src = packages[currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode],
200 table = document.querySelector('#packages'),
201 pager = document.querySelector('#pager');
202
203 currentDisplayRows.length = 0;
204
205 if (typeof(pattern) === 'string' && pattern.length > 0)
206 pattern = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig');
207
208 for (var name in src.pkgs) {
209 var pkg = src.pkgs[name],
210 desc = pkg.description || '',
211 altsize = null;
212
213 if (!pkg.size && packages.available.pkgs[name])
214 altsize = packages.available.pkgs[name].size;
215
216 if (!desc && packages.available.pkgs[name])
217 desc = packages.available.pkgs[name].description || '';
218
219 desc = desc.split(/\n/);
220 desc = desc[0].trim() + (desc.length > 1 ? '…' : '');
221
222 if ((pattern instanceof RegExp) &&
223 !name.match(pattern) && !desc.match(pattern))
224 continue;
225
226 var btn, ver;
227
228 if (currentDisplayMode === 'updates') {
229 var avail = packages.available.pkgs[name],
230 inst = packages.installed.pkgs[name];
231
232 if (!inst || !inst.installed)
233 continue;
234
235 if (!avail || compareVersion(avail.version, pkg.version) <= 0)
236 continue;
237
238 ver = '%s » %s'.format(
239 truncateVersion(pkg.version || '-'),
240 truncateVersion(avail.version || '-'));
241
242 btn = E('div', {
243 'class': 'btn cbi-button-positive',
244 'data-package': name,
245 'click': handleInstall
246 }, _('Upgrade…'));
247 }
248 else if (currentDisplayMode === 'installed') {
249 if (!pkg.installed)
250 continue;
251
252 ver = truncateVersion(pkg.version || '-');
253 btn = E('div', {
254 'class': 'btn cbi-button-negative',
255 'data-package': name,
256 'click': handleRemove
257 }, _('Remove…'));
258 }
259 else {
260 var inst = packages.installed.pkgs[name];
261
262 ver = truncateVersion(pkg.version || '-');
263
264 if (!inst || !inst.installed)
265 btn = E('div', {
266 'class': 'btn cbi-button-action',
267 'data-package': name,
268 'click': handleInstall
269 }, _('Install…'));
270 else if (inst.installed && inst.version != pkg.version)
271 btn = E('div', {
272 'class': 'btn cbi-button-positive',
273 'data-package': name,
274 'click': handleInstall
275 }, _('Upgrade…'));
276 else
277 btn = E('div', {
278 'class': 'btn cbi-button-neutral',
279 'disabled': 'disabled'
280 }, _('Installed'));
281 }
282
283 name = '%h'.format(name);
284 desc = '%h'.format(desc || '-');
285
286 if (pattern) {
287 name = name.replace(pattern, '<ins>$&</ins>');
288 desc = desc.replace(pattern, '<ins>$&</ins>');
289 }
290
291 currentDisplayRows.push([
292 name,
293 ver,
294 pkg.size ? '%.1024mB'.format(pkg.size)
295 : (altsize ? '~%.1024mB'.format(altsize) : '-'),
296 desc,
297 btn
298 ]);
299 }
300
301 currentDisplayRows.sort(function(a, b) {
302 if (a[0] < b[0])
303 return -1;
304 else if (a[0] > b[0])
305 return 1;
306 else
307 return 0;
308 });
309
310 pager.parentNode.style.display = '';
311 pager.setAttribute('data-offset', 100);
312 handlePage({ target: pager.querySelector('.prev') });
313 }
314
315 function handlePage(ev)
316 {
317 var filter = document.querySelector('input[name="filter"]'),
318 pager = ev.target.parentNode,
319 offset = +pager.getAttribute('data-offset'),
320 next = ev.target.classList.contains('next');
321
322 if ((next && (offset + 100) >= currentDisplayRows.length) ||
323 (!next && (offset < 100)))
324 return;
325
326 offset += next ? 100 : -100;
327 pager.setAttribute('data-offset', offset);
328 pager.querySelector('.text').firstChild.data = currentDisplayRows.length
329 ? _('Displaying %d-%d of %d').format(1 + offset, Math.min(offset + 100, currentDisplayRows.length), currentDisplayRows.length)
330 : _('No packages');
331
332 if (offset < 100)
333 pager.querySelector('.prev').setAttribute('disabled', 'disabled');
334 else
335 pager.querySelector('.prev').removeAttribute('disabled');
336
337 if ((offset + 100) >= currentDisplayRows.length)
338 pager.querySelector('.next').setAttribute('disabled', 'disabled');
339 else
340 pager.querySelector('.next').removeAttribute('disabled');
341
342 var placeholder = _('No information available');
343
344 if (filter.value)
345 placeholder = [
346 E('span', {}, _('No packages matching "<strong>%h</strong>".').format(filter.value)), ' (',
347 E('a', { href: '#', onclick: 'handleReset(event)' }, _('Reset')), ')'
348 ];
349
350 cbi_update_table('#packages', currentDisplayRows.slice(offset, offset + 100),
351 placeholder);
352 }
353
354 function handleMode(ev)
355 {
356 var tab = findParent(ev.target, 'li');
357 if (tab.getAttribute('data-mode') === currentDisplayMode)
358 return;
359
360 tab.parentNode.querySelectorAll('li').forEach(function(li) {
361 li.classList.remove('cbi-tab');
362 li.classList.add('cbi-tab-disabled');
363 });
364
365 tab.classList.remove('cbi-tab-disabled');
366 tab.classList.add('cbi-tab');
367
368 currentDisplayMode = tab.getAttribute('data-mode');
369
370 display(document.querySelector('input[name="filter"]').value);
371
372 ev.target.blur();
373 ev.preventDefault();
374 }
375
376 function orderOf(c)
377 {
378 if (c === '~')
379 return -1;
380 else if (c === '' || c >= '0' && c <= '9')
381 return 0;
382 else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
383 return c.charCodeAt(0);
384 else
385 return c.charCodeAt(0) + 256;
386 }
387
388 function compareVersion(val, ref)
389 {
390 var vi = 0, ri = 0,
391 isdigit = { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 };
392
393 val = val || '';
394 ref = ref || '';
395
396 if (val === ref)
397 return 0;
398
399 while (vi < val.length || ri < ref.length) {
400 var first_diff = 0;
401
402 while ((vi < val.length && !isdigit[val.charAt(vi)]) ||
403 (ri < ref.length && !isdigit[ref.charAt(ri)])) {
404 var vc = orderOf(val.charAt(vi)), rc = orderOf(ref.charAt(ri));
405 if (vc !== rc)
406 return vc - rc;
407
408 vi++; ri++;
409 }
410
411 while (val.charAt(vi) === '0')
412 vi++;
413
414 while (ref.charAt(ri) === '0')
415 ri++;
416
417 while (isdigit[val.charAt(vi)] && isdigit[ref.charAt(ri)]) {
418 first_diff = first_diff || (val.charCodeAt(vi) - ref.charCodeAt(ri));
419 vi++; ri++;
420 }
421
422 if (isdigit[val.charAt(vi)])
423 return 1;
424 else if (isdigit[ref.charAt(ri)])
425 return -1;
426 else if (first_diff)
427 return first_diff;
428 }
429
430 return 0;
431 }
432
433 function versionSatisfied(ver, ref, vop)
434 {
435 var r = compareVersion(ver, ref);
436
437 switch (vop) {
438 case '<':
439 case '<=':
440 return r <= 0;
441
442 case '>':
443 case '>=':
444 return r >= 0;
445
446 case '<<':
447 return r < 0;
448
449 case '>>':
450 return r > 0;
451
452 case '=':
453 return r == 0;
454 }
455
456 return false;
457 }
458
459 function pkgStatus(pkg, vop, ver, info)
460 {
461 info.errors = info.errors || [];
462 info.install = info.install || [];
463
464 if (pkg.installed) {
465 if (vop && !versionSatisfied(pkg.version, ver, vop)) {
466 var repl = null;
467
468 (packages.available.providers[pkg.name] || []).forEach(function(p) {
469 if (!repl && versionSatisfied(p.version, ver, vop))
470 repl = p;
471 });
472
473 if (repl) {
474 info.install.push(repl);
475 return E('span', {
476 'class': 'label',
477 'data-tooltip': _('Requires update to %h %h')
478 .format(repl.name, repl.version)
479 }, _('Needs upgrade'));
480 }
481
482 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)));
483
484 return E('span', {
485 'class': 'label warning',
486 'data-tooltip': _('Require version %h %h,\ninstalled %h')
487 .format(vop, ver, pkg.version)
488 }, _('Version incompatible'));
489 }
490
491 return E('span', { 'class': 'label notice' }, _('Installed'));
492 }
493 else if (!pkg.missing) {
494 if (!vop || versionSatisfied(pkg.version, ver, vop)) {
495 info.install.push(pkg);
496 return E('span', { 'class': 'label' }, _('Not installed'));
497 }
498
499 info.errors.push(_('The repository version of package <em>%h</em> is not compatible, require %s but only %s is available.')
500 .format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version)));
501
502 return E('span', {
503 'class': 'label warning',
504 'data-tooltip': _('Require version %h %h,\ninstalled %h')
505 .format(vop, ver, pkg.version)
506 }, _('Version incompatible'));
507 }
508 else {
509 info.errors.push(_('Required dependency package <em>%h</em> is not available in any repository.').format(pkg.name));
510
511 return E('span', { 'class': 'label warning' }, _('Not available'));
512 }
513 }
514
515 function renderDependencyItem(dep, info)
516 {
517 var li = E('li'),
518 vop = dep.version ? dep.version[0] : null,
519 ver = dep.version ? dep.version[1] : null,
520 depends = [];
521
522 for (var i = 0; dep.pkgs && i < dep.pkgs.length; i++) {
523 var pkg = packages.installed.pkgs[dep.pkgs[i]] ||
524 packages.available.pkgs[dep.pkgs[i]] ||
525 { name: dep.name };
526
527 if (i > 0)
528 li.appendChild(document.createTextNode(' | '));
529
530 var text = pkg.name;
531
532 if (pkg.installsize)
533 text += ' (%.1024mB)'.format(pkg.installsize);
534 else if (pkg.size)
535 text += ' (~%.1024mB)'.format(pkg.size);
536
537 li.appendChild(E('span', { 'data-tooltip': pkg.description },
538 [ text, ' ', pkgStatus(pkg, vop, ver, info) ]));
539
540 (pkg.depends || []).forEach(function(d) {
541 if (depends.indexOf(d) === -1)
542 depends.push(d);
543 });
544 }
545
546 if (!li.firstChild)
547 li.appendChild(E('span', {},
548 [ dep.name, ' ',
549 pkgStatus({ name: dep.name, missing: true }, vop, ver, info) ]));
550
551 var subdeps = renderDependencies(depends, info);
552 if (subdeps)
553 li.appendChild(subdeps);
554
555 return li;
556 }
557
558 function renderDependencies(depends, info)
559 {
560 var deps = depends || [],
561 items = [];
562
563 info.seen = info.seen || [];
564
565 for (var i = 0; i < deps.length; i++) {
566 var dep, vop, ver;
567
568 if (deps[i] === 'libc')
569 continue;
570
571 if (deps[i].match(/^(.+)\s+\((<=|<|>|>=|=|<<|>>)(.+)\)$/)) {
572 dep = RegExp.$1.trim();
573 vop = RegExp.$2.trim();
574 ver = RegExp.$3.trim();
575 }
576 else {
577 dep = deps[i].trim();
578 vop = ver = null;
579 }
580
581 if (info.seen[dep])
582 continue;
583
584 var pkgs = [];
585
586 (packages.installed.providers[dep] || []).forEach(function(p) {
587 if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
588 });
589
590 (packages.available.providers[dep] || []).forEach(function(p) {
591 if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
592 });
593
594 info.seen[dep] = {
595 name: dep,
596 pkgs: pkgs,
597 version: [vop, ver]
598 };
599
600 items.push(renderDependencyItem(info.seen[dep], info));
601 }
602
603 if (items.length)
604 return E('ul', { 'class': 'deps' }, items);
605
606 return null;
607 }
608
609 function truncateVersion(v, op)
610 {
611 v = v.replace(/\b(([a-f0-9]{8})[a-f0-9]{24,32})\b/,
612 '<span data-tooltip="$1">$2…</span>');
613
614 if (!op || op === '=')
615 return v;
616
617 return '%h %h'.format(op, v);
618 }
619
620 function handleReset(ev)
621 {
622 var filter = document.querySelector('input[name="filter"]');
623
624 filter.value = '';
625 display();
626 }
627
628 function handleInstall(ev)
629 {
630 var name = ev.target.getAttribute('data-package'),
631 pkg = packages.available.pkgs[name],
632 depcache = {},
633 size;
634
635 if (pkg.installsize)
636 size = _('~%.1024mB installed').format(pkg.installsize);
637 else if (pkg.size)
638 size = _('~%.1024mB compressed').format(pkg.size);
639 else
640 size = _('unknown');
641
642 var deps = renderDependencies(pkg.depends, depcache),
643 tree = null, errs = null, inst = null, desc = null;
644
645 if (depcache.errors && depcache.errors.length) {
646 errs = E('ul', { 'class': 'errors' });
647 depcache.errors.forEach(function(err) {
648 errs.appendChild(E('li', {}, err));
649 });
650 }
651
652 var totalsize = pkg.installsize || pkg.size || 0,
653 totalpkgs = 1;
654
655 if (depcache.install && depcache.install.length)
656 depcache.install.forEach(function(ipkg) {
657 totalsize += ipkg.installsize || ipkg.size || 0;
658 totalpkgs++;
659 });
660
661 inst = E('p', {},
662 _('Require approx. %.1024mB size for %d package(s) to install.')
663 .format(totalsize, totalpkgs));
664
665 if (deps) {
666 tree = E('li', '<strong>%s:</strong>'.format(_('Dependencies')));
667 tree.appendChild(deps);
668 }
669
670 if (pkg.description) {
671 desc = E('div', {}, [
672 E('h5', {}, _('Description')),
673 E('p', {}, pkg.description)
674 ]);
675 }
676
677 ui.showModal(_('Details for package <em>%h</em>').format(pkg.name), [
678 E('ul', {}, [
679 E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
680 E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)),
681 tree || '',
682 ]),
683 desc || '',
684 errs || inst || '',
685 E('div', { 'class': 'right' }, [
686 E('label', { 'class': 'cbi-checkbox', 'style': 'float:left' }, [
687 E('input', { 'id': 'overwrite-cb', 'type': 'checkbox', 'name': 'overwrite' }), ' ',
688 E('label', { 'for': 'overwrite-cb' }), ' ',
689 _('Overwrite files from other package(s)')
690 ]),
691 E('div', {
692 'class': 'btn',
693 'click': ui.hideModal
694 }, _('Cancel')),
695 ' ',
696 E('div', {
697 'data-command': 'install',
698 'data-package': name,
699 'class': 'btn cbi-button-action',
700 'click': handleOpkg
701 }, _('Install'))
702 ])
703 ]);
704 }
705
706 function handleManualInstall(ev)
707 {
708 var name_or_url = document.querySelector('input[name="install"]').value,
709 install = E('div', {
710 'class': 'btn cbi-button-action',
711 'data-command': 'install',
712 'data-package': name_or_url,
713 'click': function(ev) {
714 document.querySelector('input[name="install"]').value = '';
715 handleOpkg(ev);
716 }
717 }, _('Install')), warning;
718
719 if (!name_or_url.length) {
720 return;
721 }
722 else if (name_or_url.indexOf('/') !== -1) {
723 warning = E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(name_or_url));
724 }
725 else if (!packages.available.providers[name_or_url]) {
726 warning = E('p', {}, _('The package <em>%h</em> is not available in any configured repository.').format(name_or_url));
727 install = '';
728 }
729 else {
730 warning = E('p', {}, _('Really attempt to install <em>%h</em>?').format(name_or_url));
731 }
732
733 ui.showModal(_('Manually install package'), [
734 warning,
735 E('div', { 'class': 'right' }, [
736 E('div', {
737 'click': ui.hideModal,
738 'class': 'btn cbi-button-neutral'
739 }, _('Cancel')),
740 ' ', install
741 ])
742 ]);
743 }
744
745 function handleConfig(ev)
746 {
747 var conf = {};
748
749 ui.showModal(_('OPKG Configuration'), [
750 E('p', { 'class': 'spinning' }, _('Loading configuration data…'))
751 ]);
752
753 fs.list('/etc/opkg').then(function(partials) {
754 var files = [ '/etc/opkg.conf' ];
755
756 for (var i = 0; i < partials.length; i++)
757 if (partials[i].type == 'file' && partials[i].name.match(/\.conf$/))
758 files.push('/etc/opkg/' + partials[i].name);
759
760 return Promise.all(files.map(function(file) {
761 return fs.read(file)
762 .then(L.bind(function(conf, file, res) { conf[file] = res }, this, conf, file))
763 .catch(function(err) {
764 ui.addNotification(null, E('p', {}, [ _('Unable to read %s: %s').format(file, err) ]));
765 ui.hideModal();
766 throw err;
767 });
768 }));
769 }).then(function() {
770 var body = [
771 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>.'))
772 ];
773
774 Object.keys(conf).sort().forEach(function(file) {
775 body.push(E('h5', {}, '%h'.format(file)));
776 body.push(E('textarea', {
777 'name': file,
778 'rows': Math.max(Math.min(L.toArray(conf[file].match(/\n/g)).length, 10), 3)
779 }, '%h'.format(conf[file])));
780 });
781
782 body.push(E('div', { 'class': 'right' }, [
783 E('div', {
784 'class': 'btn cbi-button-neutral',
785 'click': ui.hideModal
786 }, _('Cancel')),
787 ' ',
788 E('div', {
789 'class': 'btn cbi-button-positive',
790 'click': function(ev) {
791 var data = {};
792 findParent(ev.target, '.modal').querySelectorAll('textarea[name]')
793 .forEach(function(textarea) {
794 data[textarea.getAttribute('name')] = textarea.value
795 });
796
797 ui.showModal(_('OPKG Configuration'), [
798 E('p', { 'class': 'spinning' }, _('Saving configuration data…'))
799 ]);
800
801 Promise.all(Object.keys(data).map(function(file) {
802 return fs.write(file, data[file]).catch(function(err) {
803 ui.addNotification(null, E('p', {}, [ _('Unable to save %s: %s').format(file, err) ]));
804 });
805 })).then(ui.hideModal);
806 }
807 }, _('Save')),
808 ]));
809
810 ui.showModal(_('OPKG Configuration'), body);
811 });
812 }
813
814 function handleRemove(ev)
815 {
816 var name = ev.target.getAttribute('data-package'),
817 pkg = packages.installed.pkgs[name],
818 avail = packages.available.pkgs[name] || {},
819 size, desc;
820
821 if (avail.installsize)
822 size = _('~%.1024mB installed').format(avail.installsize);
823 else if (avail.size)
824 size = _('~%.1024mB compressed').format(avail.size);
825 else
826 size = _('unknown');
827
828 if (avail.description) {
829 desc = E('div', {}, [
830 E('h5', {}, _('Description')),
831 E('p', {}, avail.description)
832 ]);
833 }
834
835 ui.showModal(_('Remove package <em>%h</em>').format(pkg.name), [
836 E('ul', {}, [
837 E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
838 E('li', '<strong>%s:</strong> %h'.format(_('Size'), size))
839 ]),
840 desc || '',
841 E('div', { 'style': 'display:flex; justify-content:space-between; flex-wrap:wrap' }, [
842 E('label', {}, [
843 E('input', { type: 'checkbox', checked: 'checked', name: 'autoremove' }),
844 _('Automatically remove unused dependencies')
845 ]),
846 E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [
847 E('div', {
848 'class': 'btn',
849 'click': ui.hideModal
850 }, _('Cancel')),
851 ' ',
852 E('div', {
853 'data-command': 'remove',
854 'data-package': name,
855 'class': 'btn cbi-button-negative',
856 'click': handleOpkg
857 }, _('Remove'))
858 ])
859 ])
860 ]);
861 }
862
863 function handleOpkg(ev)
864 {
865 return new Promise(function(resolveFn, rejectFn) {
866 var cmd = ev.target.getAttribute('data-command'),
867 pkg = ev.target.getAttribute('data-package'),
868 rem = document.querySelector('input[name="autoremove"]'),
869 owr = document.querySelector('input[name="overwrite"]');
870
871 var dlg = ui.showModal(_('Executing package manager'), [
872 E('p', { 'class': 'spinning' },
873 _('Waiting for the <em>opkg %h</em> command to complete…').format(cmd))
874 ]);
875
876 var argv = [ cmd, '--force-removal-of-dependent-packages' ];
877
878 if (rem && rem.checked)
879 argv.push('--autoremove');
880
881 if (owr && owr.checked)
882 argv.push('--force-overwrite');
883
884 if (pkg != null)
885 argv.push(pkg);
886
887 fs.exec_direct('/usr/libexec/opkg-call', argv, 'json').then(function(res) {
888 dlg.removeChild(dlg.lastChild);
889
890 if (res.stdout)
891 dlg.appendChild(E('pre', [ res.stdout ]));
892
893 if (res.stderr) {
894 dlg.appendChild(E('h5', _('Errors')));
895 dlg.appendChild(E('pre', { 'class': 'errors' }, [ res.stderr ]));
896 }
897
898 if (res.code !== 0)
899 dlg.appendChild(E('p', _('The <em>opkg %h</em> command failed with code <code>%d</code>.').format(cmd, (res.code & 0xff) || -1)));
900
901 dlg.appendChild(E('div', { 'class': 'right' },
902 E('div', {
903 'class': 'btn',
904 'click': L.bind(function(res) {
905 ui.hideModal();
906 updateLists();
907
908 if (res.code !== 0)
909 rejectFn(new Error(res.stderr || 'opkg error %d'.format(res.code)));
910 else
911 resolveFn(res);
912 }, this, res)
913 }, _('Dismiss'))));
914 }).catch(function(err) {
915 ui.addNotification(null, E('p', _('Unable to execute <em>opkg %s</em> command: %s').format(cmd, err)));
916 ui.hideModal();
917 });
918 });
919 }
920
921 function handleUpload(ev)
922 {
923 var path = '/tmp/upload.ipk';
924 return ui.uploadFile(path).then(L.bind(function(btn, res) {
925 ui.showModal(_('Manually install package'), [
926 E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(res.name)),
927 E('ul', {}, [
928 res.size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res.size)) : '',
929 res.checksum ? E('li', {}, '%s: %s'.format(_('MD5'), res.checksum)) : '',
930 res.sha256sum ? E('li', {}, '%s: %s'.format(_('SHA256'), res.sha256sum)) : ''
931 ]),
932 E('div', { 'class': 'right' }, [
933 E('div', {
934 'click': function(ev) {
935 ui.hideModal();
936 fs.remove(path);
937 },
938 'class': 'btn cbi-button-neutral'
939 }, _('Cancel')), ' ',
940 E('div', {
941 'class': 'btn cbi-button-action',
942 'data-command': 'install',
943 'data-package': path,
944 'click': function(ev) {
945 handleOpkg(ev).finally(function() {
946 fs.remove(path)
947 });
948 }
949 }, _('Install'))
950 ])
951 ]);
952 }, this, ev.target));
953 }
954
955 function downloadLists()
956 {
957 return Promise.all([
958 callMountPoints(),
959 fs.exec_direct('/usr/libexec/opkg-call', [ 'list-available' ]),
960 fs.exec_direct('/usr/libexec/opkg-call', [ 'list-installed' ])
961 ]);
962 }
963
964 function updateLists(data)
965 {
966 cbi_update_table('#packages', [],
967 E('div', { 'class': 'spinning' }, _('Loading package information…')));
968
969 packages.available = { providers: {}, pkgs: {} };
970 packages.installed = { providers: {}, pkgs: {} };
971
972 return (data ? Promise.resolve(data) : downloadLists()).then(function(data) {
973 var pg = document.querySelector('.cbi-progressbar'),
974 mount = L.toArray(data[0].filter(function(m) { return m.mount == '/' || m.mount == '/overlay' }))
975 .sort(function(a, b) { return a.mount > b.mount })[0] || { size: 0, free: 0 };
976
977 pg.firstElementChild.style.width = Math.floor(mount.size ? ((100 / mount.size) * mount.free) : 100) + '%';
978 pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, mount.free));
979
980 parseList(data[1], packages.available);
981 parseList(data[2], packages.installed);
982
983 display(document.querySelector('input[name="filter"]').value);
984 });
985 }
986
987 var keyTimeout = null;
988
989 function handleKeyUp(ev) {
990 if (keyTimeout !== null)
991 window.clearTimeout(keyTimeout);
992
993 keyTimeout = window.setTimeout(function() {
994 display(ev.target.value);
995 }, 250);
996 }
997
998 return L.view.extend({
999 load: function() {
1000 return downloadLists();
1001 },
1002
1003 render: function(listData) {
1004 var query = decodeURIComponent(L.toArray(location.search.match(/\bquery=([^=]+)\b/))[1] || '');
1005
1006 var view = E([], [
1007 E('style', { 'type': 'text/css' }, [ css ]),
1008
1009 E('h2', {}, _('Software')),
1010
1011 E('div', { 'class': 'controls' }, [
1012 E('div', {}, [
1013 E('label', {}, _('Free space') + ':'),
1014 E('div', { 'class': 'cbi-progressbar', 'title': _('unknown') }, E('div', {}, [ '\u00a0' ]))
1015 ]),
1016
1017 E('div', {}, [
1018 E('label', {}, _('Filter') + ':'),
1019 E('span', { 'class': 'control-group' }, [
1020 E('input', { 'type': 'text', 'name': 'filter', 'placeholder': _('Type to filter…'), 'value': query, 'keyup': handleKeyUp }),
1021 E('button', { 'class': 'btn cbi-button', 'click': handleReset }, [ _('Clear') ])
1022 ])
1023 ]),
1024
1025 E('div', {}, [
1026 E('label', {}, _('Download and install package') + ':'),
1027 E('span', { 'class': 'control-group' }, [
1028 E('input', { 'type': 'text', 'name': 'install', 'placeholder': _('Package name or URL…'), 'keydown': function(ev) { if (ev.keyCode === 13) handleManualInstall(ev) } }),
1029 E('button', { 'class': 'btn cbi-button cbi-button-action', 'click': handleManualInstall }, [ _('OK') ])
1030 ])
1031 ]),
1032
1033 E('div', {}, [
1034 E('label', {}, _('Actions') + ':'), ' ',
1035 E('span', { 'class': 'control-group' }, [
1036 E('button', { 'class': 'btn cbi-button-positive', 'data-command': 'update', 'click': handleOpkg }, [ _('Update lists…') ]), ' ',
1037 E('button', { 'class': 'btn cbi-button-action', 'click': handleUpload }, [ _('Upload Package…') ]), ' ',
1038 E('button', { 'class': 'btn cbi-button-neutral', 'click': handleConfig }, [ _('Configure opkg…') ])
1039 ])
1040 ])
1041 ]),
1042
1043 E('ul', { 'class': 'cbi-tabmenu mode' }, [
1044 E('li', { 'data-mode': 'available', 'class': 'available cbi-tab', 'click': handleMode }, E('a', { 'href': '#' }, [ _('Available') ])),
1045 E('li', { 'data-mode': 'installed', 'class': 'installed cbi-tab-disabled', 'click': handleMode }, E('a', { 'href': '#' }, [ _('Installed') ])),
1046 E('li', { 'data-mode': 'updates', 'class': 'installed cbi-tab-disabled', 'click': handleMode }, E('a', { 'href': '#' }, [ _('Updates') ]))
1047 ]),
1048
1049 E('div', { 'class': 'controls', 'style': 'display:none' }, [
1050 E('div', { 'id': 'pager', 'class': 'center' }, [
1051 E('button', { 'class': 'btn cbi-button-neutral prev', 'aria-label': _('Previous page'), 'click': handlePage }, [ '«' ]),
1052 E('div', { 'class': 'text' }, [ 'dummy' ]),
1053 E('button', { 'class': 'btn cbi-button-neutral next', 'aria-label': _('Next page'), 'click': handlePage }, [ '»' ])
1054 ])
1055 ]),
1056
1057 E('div', { 'id': 'packages', 'class': 'table' }, [
1058 E('div', { 'class': 'tr cbi-section-table-titles' }, [
1059 E('div', { 'class': 'th col-2 left' }, [ _('Package name') ]),
1060 E('div', { 'class': 'th col-2 left version' }, [ _('Version') ]),
1061 E('div', { 'class': 'th col-1 center size'}, [ _('Size (.ipk)') ]),
1062 E('div', { 'class': 'th col-10 left' }, [ _('Description') ]),
1063 E('div', { 'class': 'th right cbi-section-actions' }, [ '\u00a0' ])
1064 ])
1065 ])
1066 ]);
1067
1068 requestAnimationFrame(function() {
1069 updateLists(listData)
1070 });
1071
1072 return view;
1073 },
1074
1075 handleSave: null,
1076 handleSaveApply: null,
1077 handleReset: null
1078 });