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