2 available
: { providers
: {}, pkgs
: {} },
3 installed
: { providers
: {}, pkgs
: {} }
6 var currentDisplayMode
= 'available', currentDisplayRows
= [];
8 function parseList(s
, dest
)
10 var re
= /([^\n]*)\n/g,
11 pkg
= null, key
= null, val
= null, m
;
13 while ((m
= re
.exec(s
)) !== null) {
14 if (m
[1].match(/^\s(.*)$/)) {
15 if (pkg
!== null && key
!== null && val
!== null)
16 val
+= '\n' + RegExp
.$1.trim();
21 if (key
!== null && val
!== null) {
29 var list
= val
.split(/\s*,\s*/);
30 if (list
.length
!== 1 || list
[0].length
> 0)
34 case 'installed-time':
35 pkg
.installtime
= new Date(+val
* 1000);
38 case 'installed-size':
39 pkg
.installsize
= +val
;
43 var stat
= val
.split(/\s+/),
71 case 'auto-installed':
85 if (m
[1].trim().match(/^([\w-]+)\s*:(.+)$/)) {
86 key
= RegExp
.$1.toLowerCase();
87 val
= RegExp
.$2.trim();
90 dest
.pkgs
[pkg
.name
] = pkg
;
92 var provides
= dest
.providers
[pkg
.name
] ? [] : [ pkg
.name
];
95 provides
.push
.apply(provides
, pkg
.provides
);
97 provides
.forEach(function(p
) {
98 dest
.providers
[p
] = dest
.providers
[p
] || [];
99 dest
.providers
[p
].push(pkg
);
105 function display(pattern
)
107 var src
= packages
[currentDisplayMode
=== 'updates' ? 'installed' : currentDisplayMode
],
108 table
= document
.querySelector('#packages'),
109 pager
= document
.querySelector('#pager');
111 currentDisplayRows
.length
= 0;
113 if (typeof(pattern
) === 'string' && pattern
.length
> 0)
114 pattern
= new RegExp(pattern
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig');
116 for (var name
in src
.pkgs
) {
117 var pkg
= src
.pkgs
[name
],
118 desc
= pkg
.description
|| '',
121 if (!pkg
.size
&& packages
.available
.pkgs
[name
])
122 altsize
= packages
.available
.pkgs
[name
].size
;
124 if (!desc
&& packages
.available
.pkgs
[name
])
125 desc
= packages
.available
.pkgs
[name
].description
|| '';
127 desc
= desc
.split(/\n/);
128 desc
= desc
[0].trim() + (desc
.length
> 1 ? '…' : '');
130 if ((pattern
instanceof RegExp
) &&
131 !name
.match(pattern
) && !desc
.match(pattern
))
136 if (currentDisplayMode
=== 'updates') {
137 var avail
= packages
.available
.pkgs
[name
],
138 inst
= packages
.installed
.pkgs
[name
];
140 if (!inst
|| !inst
.installed
)
143 if (!avail
|| compareVersion(avail
.version
, pkg
.version
) <= 0)
146 ver
= '%s » %s'.format(
147 truncateVersion(pkg
.version
|| '-'),
148 truncateVersion(avail
.version
|| '-'));
151 'class': 'btn cbi-button-positive',
152 'data-package': name
,
153 'click': handleInstall
156 else if (currentDisplayMode
=== 'installed') {
160 ver
= truncateVersion(pkg
.version
|| '-');
162 'class': 'btn cbi-button-negative',
163 'data-package': name
,
164 'click': handleRemove
168 var inst
= packages
.installed
.pkgs
[name
];
170 ver
= truncateVersion(pkg
.version
|| '-');
172 if (!inst
|| !inst
.installed
)
174 'class': 'btn cbi-button-action',
175 'data-package': name
,
176 'click': handleInstall
178 else if (inst
.installed
&& inst
.version
!= pkg
.version
)
180 'class': 'btn cbi-button-positive',
181 'data-package': name
,
182 'click': handleInstall
186 'class': 'btn cbi-button-neutral',
187 'disabled': 'disabled'
191 name
= '%h'.format(name
);
192 desc
= '%h'.format(desc
|| '-');
195 name
= name
.replace(pattern
, '<ins>$&</ins>');
196 desc
= desc
.replace(pattern
, '<ins>$&</ins>');
199 currentDisplayRows
.push([
202 pkg
.size
? '%.1024mB'.format(pkg
.size
)
203 : (altsize
? '~%.1024mB'.format(altsize
) : '-'),
209 currentDisplayRows
.sort(function(a
, b
) {
212 else if (a
[0] > b
[0])
218 pager
.parentNode
.style
.display
= '';
219 pager
.setAttribute('data-offset', 100);
220 handlePage({ target
: pager
.querySelector('.prev') });
223 function handlePage(ev
)
225 var filter
= document
.querySelector('input[name="filter"]'),
226 pager
= ev
.target
.parentNode
,
227 offset
= +pager
.getAttribute('data-offset'),
228 next
= ev
.target
.classList
.contains('next');
230 if ((next
&& (offset
+ 100) >= currentDisplayRows
.length
) ||
231 (!next
&& (offset
< 100)))
234 offset
+= next
? 100 : -100;
235 pager
.setAttribute('data-offset', offset
);
236 pager
.querySelector('.text').firstChild
.data
= currentDisplayRows
.length
237 ? _('Displaying %d-%d of %d').format(1 + offset
, Math
.min(offset
+ 100, currentDisplayRows
.length
), currentDisplayRows
.length
)
241 pager
.querySelector('.prev').setAttribute('disabled', 'disabled');
243 pager
.querySelector('.prev').removeAttribute('disabled');
245 if ((offset
+ 100) >= currentDisplayRows
.length
)
246 pager
.querySelector('.next').setAttribute('disabled', 'disabled');
248 pager
.querySelector('.next').removeAttribute('disabled');
250 var placeholder
= _('No information available');
254 E('span', {}, _('No packages matching "<strong>%h</strong>".').format(filter
.value
)), ' (',
255 E('a', { href
: '#', onclick
: 'handleReset(event)' }, _('Reset')), ')'
258 cbi_update_table('#packages', currentDisplayRows
.slice(offset
, offset
+ 100),
262 function handleMode(ev
)
264 var tab
= findParent(ev
.target
, 'li');
265 if (tab
.getAttribute('data-mode') === currentDisplayMode
)
268 tab
.parentNode
.querySelectorAll('li').forEach(function(li
) {
269 li
.classList
.remove('cbi-tab');
270 li
.classList
.add('cbi-tab-disabled');
273 tab
.classList
.remove('cbi-tab-disabled');
274 tab
.classList
.add('cbi-tab');
276 currentDisplayMode
= tab
.getAttribute('data-mode');
278 display(document
.querySelector('input[name="filter"]').value
);
288 else if (c
=== '' || c
>= '0' && c
<= '9')
290 else if ((c
>= 'a' && c
<= 'z') || (c
>= 'A' && c
<= 'Z'))
291 return c
.charCodeAt(0);
293 return c
.charCodeAt(0) + 256;
296 function compareVersion(val
, ref
)
299 isdigit
= { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 };
307 while (vi
< val
.length
|| ri
< ref
.length
) {
310 while ((vi
< val
.length
&& !isdigit
[val
.charAt(vi
)]) ||
311 (ri
< ref
.length
&& !isdigit
[ref
.charAt(ri
)])) {
312 var vc
= orderOf(val
.charAt(vi
)), rc
= orderOf(ref
.charAt(ri
));
319 while (val
.charAt(vi
) === '0')
322 while (ref
.charAt(ri
) === '0')
325 while (isdigit
[val
.charAt(vi
)] && isdigit
[ref
.charAt(ri
)]) {
326 first_diff
= first_diff
|| (val
.charCodeAt(vi
) - ref
.charCodeAt(ri
));
330 if (isdigit
[val
.charAt(vi
)])
332 else if (isdigit
[ref
.charAt(ri
)])
341 function versionSatisfied(ver
, ref
, vop
)
343 var r
= compareVersion(ver
, ref
);
367 function pkgStatus(pkg
, vop
, ver
, info
)
369 info
.errors
= info
.errors
|| [];
370 info
.install
= info
.install
|| [];
373 if (vop
&& !versionSatisfied(pkg
.version
, ver
, vop
)) {
376 (packages
.available
.providers
[pkg
.name
] || []).forEach(function(p
) {
377 if (!repl
&& versionSatisfied(p
.version
, ver
, vop
))
382 info
.install
.push(repl
);
385 'data-tooltip': _('Requires update to %h %h')
386 .format(repl
.name
, repl
.version
)
387 }, _('Needs upgrade'));
390 info
.errors
.push(_('The installed version of package <em>%h</em> is not compatible, require %s while %s is installed.').format(pkg
.name
, truncateVersion(ver
, vop
), truncateVersion(pkg
.version
)));
393 'class': 'label warning',
394 'data-tooltip': _('Require version %h %h,\ninstalled %h')
395 .format(vop
, ver
, pkg
.version
)
396 }, _('Version incompatible'));
399 return E('span', { 'class': 'label notice' }, _('Installed'));
401 else if (!pkg
.missing
) {
402 if (!vop
|| versionSatisfied(pkg
.version
, ver
, vop
)) {
403 info
.install
.push(pkg
);
404 return E('span', { 'class': 'label' }, _('Not installed'));
407 info
.errors
.push(_('The repository version of package <em>%h</em> is not compatible, require %s but only %s is available.')
408 .format(pkg
.name
, truncateVersion(ver
, vop
), truncateVersion(pkg
.version
)));
411 'class': 'label warning',
412 'data-tooltip': _('Require version %h %h,\ninstalled %h')
413 .format(vop
, ver
, pkg
.version
)
414 }, _('Version incompatible'));
417 info
.errors
.push(_('Required dependency package <em>%h</em> is not available in any repository.').format(pkg
.name
));
419 return E('span', { 'class': 'label warning' }, _('Not available'));
423 function renderDependencyItem(dep
, info
)
426 vop
= dep
.version
? dep
.version
[0] : null,
427 ver
= dep
.version
? dep
.version
[1] : null,
430 for (var i
= 0; dep
.pkgs
&& i
< dep
.pkgs
.length
; i
++) {
431 var pkg
= packages
.installed
.pkgs
[dep
.pkgs
[i
]] ||
432 packages
.available
.pkgs
[dep
.pkgs
[i
]] ||
436 li
.appendChild(document
.createTextNode(' | '));
441 text
+= ' (%.1024mB)'.format(pkg
.installsize
);
443 text
+= ' (~%.1024mB)'.format(pkg
.size
);
445 li
.appendChild(E('span', { 'data-tooltip': pkg
.description
},
446 [ text
, ' ', pkgStatus(pkg
, vop
, ver
, info
) ]));
448 (pkg
.depends
|| []).forEach(function(d
) {
449 if (depends
.indexOf(d
) === -1)
455 li
.appendChild(E('span', {},
457 pkgStatus({ name
: dep
.name
, missing
: true }, vop
, ver
, info
) ]));
459 var subdeps
= renderDependencies(depends
, info
);
461 li
.appendChild(subdeps
);
466 function renderDependencies(depends
, info
)
468 var deps
= depends
|| [],
471 info
.seen
= info
.seen
|| [];
473 for (var i
= 0; i
< deps
.length
; i
++) {
474 if (deps
[i
] === 'libc')
477 if (deps
[i
].match(/^(.+)\s+\((<=|<|>|>=|=|<<|>>)(.+)\)$/)) {
478 dep
= RegExp
.$1.trim();
479 vop
= RegExp
.$2.trim();
480 ver
= RegExp
.$3.trim();
483 dep
= deps
[i
].trim();
492 (packages
.installed
.providers
[dep
] || []).forEach(function(p
) {
493 if (pkgs
.indexOf(p
.name
) === -1) pkgs
.push(p
.name
);
496 (packages
.available
.providers
[dep
] || []).forEach(function(p
) {
497 if (pkgs
.indexOf(p
.name
) === -1) pkgs
.push(p
.name
);
506 items
.push(renderDependencyItem(info
.seen
[dep
], info
));
510 return E('ul', { 'class': 'deps' }, items
);
515 function truncateVersion(v
, op
)
517 v
= v
.replace(/\b(([a-f0-9]{8})[a-f0-9]{24,32})\b/,
518 '<span data-tooltip="$1">$2…</span>');
520 if (!op
|| op
=== '=')
523 return '%h %h'.format(op
, v
);
526 function handleReset(ev
)
528 var filter
= document
.querySelector('input[name="filter"]');
534 function handleInstall(ev
)
536 var name
= ev
.target
.getAttribute('data-package'),
537 pkg
= packages
.available
.pkgs
[name
],
542 size
= _('~%.1024mB installed').format(pkg
.installsize
);
544 size
= _('~%.1024mB compressed').format(pkg
.size
);
548 var deps
= renderDependencies(pkg
.depends
, depcache
),
549 tree
= null, errs
= null, inst
= null, desc
= null;
551 if (depcache
.errors
&& depcache
.errors
.length
) {
552 errs
= E('ul', { 'class': 'errors' });
553 depcache
.errors
.forEach(function(err
) {
554 errs
.appendChild(E('li', {}, err
));
558 var totalsize
= pkg
.installsize
|| pkg
.size
|| 0,
561 if (depcache
.install
&& depcache
.install
.length
)
562 depcache
.install
.forEach(function(ipkg
) {
563 totalsize
+= ipkg
.installsize
|| ipkg
.size
|| 0;
568 _('Require approx. %.1024mB size for %d package(s) to install.')
569 .format(totalsize
, totalpkgs
));
572 tree
= E('li', '<strong>%s:</strong>'.format(_('Dependencies')));
573 tree
.appendChild(deps
);
576 if (pkg
.description
) {
577 desc
= E('div', {}, [
578 E('h5', {}, _('Description')),
579 E('p', {}, pkg
.description
)
583 L
.showModal(_('Details for package <em>%h</em>').format(pkg
.name
), [
585 E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg
.version
)),
586 E('li', '<strong>%s:</strong> %h'.format(_('Size'), size
)),
591 E('div', { 'class': 'right' }, [
592 E('label', { 'class': 'cbi-checkbox', 'style': 'float:left; padding-top:.5em' }, [
593 E('input', { 'type': 'checkbox', 'name': 'overwrite' }), ' ',
594 _('Overwrite files from other package(s)')
602 'data-command': 'install',
603 'data-package': name
,
604 'class': 'btn cbi-button-action',
611 function handleManualInstall(ev
)
613 var name_or_url
= document
.querySelector('input[name="install"]').value
,
615 'class': 'btn cbi-button-action',
616 'data-command': 'install',
617 'data-package': name_or_url
,
618 'click': function(ev
) {
619 document
.querySelector('input[name="install"]').value
= '';
622 }, _('Install')), warning
;
624 if (!name_or_url
.length
) {
627 else if (name_or_url
.indexOf('/') !== -1) {
628 warning
= E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(name_or_url
));
630 else if (!packages
.available
.providers
[name_or_url
]) {
631 warning
= E('p', {}, _('The package <em>%h</em> is not available in any configured repository.').format(name_or_url
));
635 warning
= E('p', {}, _('Really attempt to install <em>%h</em>?').format(name_or_url
));
638 L
.showModal(_('Manually install package'), [
640 E('div', { 'class': 'right' }, [
642 'click': L
.hideModal
,
643 'class': 'btn cbi-button-neutral'
650 function handleConfig(ev
)
652 L
.showModal(_('OPKG Configuration'), [
653 E('p', { 'class': 'spinning' }, _('Loading configuration data…'))
656 L
.get('admin/system/opkg/config', null, function(xhr
, conf
) {
658 E('p', {}, _('Below is a listing of the various configuration files used by <em>opkg</em>. Use <em>opkg.conf</em> for global settings and <em>customfeeds.conf</em> for custom repository entries. The configuration in the other files may be changed but is usually not preserved by <em>sysupgrade</em>.'))
661 Object
.keys(conf
).sort().forEach(function(file
) {
662 body
.push(E('h5', {}, '%h'.format(file
)));
663 body
.push(E('textarea', {
665 'rows': Math
.max(Math
.min(L
.toArray(conf
[file
].match(/\n/g)).length
, 10), 3)
666 }, '%h'.format(conf
[file
])));
669 body
.push(E('div', { 'class': 'right' }, [
671 'class': 'btn cbi-button-neutral',
676 'class': 'btn cbi-button-positive',
677 'click': function(ev
) {
679 findParent(ev
.target
, '.modal').querySelectorAll('textarea[name]')
680 .forEach(function(textarea
) {
681 data
[textarea
.getAttribute('name')] = textarea
.value
684 L
.showModal(_('OPKG Configuration'), [
685 E('p', { 'class': 'spinning' }, _('Saving configuration data…'))
688 L
.post('admin/system/opkg/config', { data
: JSON
.stringify(data
) }, L
.hideModal
);
693 L
.showModal(_('OPKG Configuration'), body
);
697 function handleRemove(ev
)
699 var name
= ev
.target
.getAttribute('data-package'),
700 pkg
= packages
.installed
.pkgs
[name
],
701 avail
= packages
.available
.pkgs
[name
] || {},
704 if (avail
.installsize
)
705 size
= _('~%.1024mB installed').format(avail
.installsize
);
707 size
= _('~%.1024mB compressed').format(avail
.size
);
711 if (avail
.description
) {
712 desc
= E('div', {}, [
713 E('h5', {}, _('Description')),
714 E('p', {}, avail
.description
)
718 L
.showModal(_('Remove package <em>%h</em>').format(pkg
.name
), [
720 E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg
.version
)),
721 E('li', '<strong>%s:</strong> %h'.format(_('Size'), size
))
724 E('div', { 'style': 'display:flex; justify-content:space-between; flex-wrap:wrap' }, [
726 E('input', { type
: 'checkbox', checked
: 'checked', name
: 'autoremove' }),
727 _('Automatically remove unused dependencies')
729 E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [
736 'data-command': 'remove',
737 'data-package': name
,
738 'class': 'btn cbi-button-negative',
746 function handleOpkg(ev
)
748 return new Promise(function(resolveFn
, rejectFn
) {
749 var cmd
= ev
.target
.getAttribute('data-command'),
750 pkg
= ev
.target
.getAttribute('data-package'),
751 rem
= document
.querySelector('input[name="autoremove"]'),
752 owr
= document
.querySelector('input[name="overwrite"]'),
753 url
= 'admin/system/opkg/exec/' + encodeURIComponent(cmd
);
755 var dlg
= L
.showModal(_('Executing package manager'), [
756 E('p', { 'class': 'spinning' },
757 _('Waiting for the <em>opkg %h</em> command to complete…').format(cmd
))
760 L
.post(url
, { package: pkg
, autoremove
: rem
? rem
.checked
: false, overwrite
: owr
? owr
.checked
: false }, function(xhr
, res
) {
761 dlg
.removeChild(dlg
.lastChild
);
764 dlg
.appendChild(E('pre', [ res
.stdout
]));
767 dlg
.appendChild(E('h5', _('Errors')));
768 dlg
.appendChild(E('pre', { 'class': 'errors' }, [ res
.stderr
]));
772 dlg
.appendChild(E('p', _('The <em>opkg %h</em> command failed with code <code>%d</code>.').format(cmd
, (res
.code
& 0xff) || -1)));
774 dlg
.appendChild(E('div', { 'class': 'right' },
777 'click': L
.bind(function(res
) {
778 if (L
.ui
.menu
&& L
.ui
.menu
.flushCache
)
779 L
.ui
.menu
.flushCache();
785 rejectFn(new Error(res
.stderr
|| 'opkg error %d'.format(res
.code
)));
794 function handleUpload(ev
)
796 var path
= '/tmp/upload.ipk';
797 return L
.ui
.uploadFile(path
).then(L
.bind(function(btn
, res
) {
798 L
.showModal(_('Manually install package'), [
799 E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(res
.name
)),
801 res
.size
? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res
.size
)) : '',
802 res
.checksum
? E('li', {}, '%s: %s'.format(_('MD5'), res
.checksum
)) : '',
803 res
.sha256sum
? E('li', {}, '%s: %s'.format(_('SHA256'), res
.sha256sum
)) : ''
805 E('div', { 'class': 'right' }, [
807 'click': function(ev
) {
811 'class': 'btn cbi-button-neutral'
812 }, _('Cancel')), ' ',
814 'class': 'btn cbi-button-action',
815 'data-command': 'install',
816 'data-package': path
,
817 'click': function(ev
) {
818 handleOpkg(ev
).finally(function() {
825 }, this, ev
.target
));
828 function updateLists()
830 cbi_update_table('#packages', [],
831 E('div', { 'class': 'spinning' }, _('Loading package information…')));
833 packages
.available
= { providers
: {}, pkgs
: {} };
834 packages
.installed
= { providers
: {}, pkgs
: {} };
836 L
.get('admin/system/opkg/statvfs', null, function(xhr
, stat
) {
837 var pg
= document
.querySelector('.cbi-progressbar'),
838 total
= stat
.blocks
|| 0,
839 free
= stat
.bfree
|| 0;
841 pg
.firstElementChild
.style
.width
= Math
.floor(total
? ((100 / total
) * free
) : 100) + '%';
842 pg
.setAttribute('title', '%s (%.1024mB)'.format(pg
.firstElementChild
.style
.width
, free
* (stat
.frsize
|| 0)));
844 L
.get('admin/system/opkg/list/available', null, function(xhr
) {
845 parseList(xhr
.responseText
, packages
.available
);
846 L
.get('admin/system/opkg/list/installed', null, function(xhr
) {
847 parseList(xhr
.responseText
, packages
.installed
);
848 display(document
.querySelector('input[name="filter"]').value
);
854 window
.requestAnimationFrame(function() {
855 var filter
= document
.querySelector('input[name="filter"]'),
858 filter
.value
= filter
.getAttribute('value');
859 filter
.addEventListener('keyup',
861 if (keyTimeout
!== null)
862 window
.clearTimeout(keyTimeout
);
864 keyTimeout
= window
.setTimeout(function() {
865 display(ev
.target
.value
);
869 document
.querySelector('#pager > .prev').addEventListener('click', handlePage
);
870 document
.querySelector('#pager > .next').addEventListener('click', handlePage
);
871 document
.querySelector('.cbi-tabmenu.mode').addEventListener('click', handleMode
);