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