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