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 if (!avail || compareVersion(avail.version, pkg.version) <= 0)
141 ver = '%s » %s'.format(
142 truncateVersion(pkg.version || '-'),
143 truncateVersion(avail.version || '-'));
146 'class': 'btn cbi-button-positive',
147 'data-package': name,
148 'click': handleInstall
151 else if (currentDisplayMode === 'installed') {
152 ver = truncateVersion(pkg.version || '-');
154 'class': 'btn cbi-button-negative',
155 'data-package': name,
156 'click': handleRemove
160 ver = truncateVersion(pkg.version || '-');
162 if (!packages.installed.pkgs[name])
164 'class': 'btn cbi-button-action',
165 'data-package': name,
166 'click': handleInstall
168 else if (packages.installed.pkgs[name].version != pkg.version)
170 'class': 'btn cbi-button-positive',
171 'data-package': name,
172 'click': handleInstall
176 'class': 'btn cbi-button-neutral',
177 'disabled': 'disabled'
181 name = '%h'.format(name);
182 desc = '%h'.format(desc || '-');
185 name = name.replace(pattern, '<ins>$&</ins>');
186 desc = desc.replace(pattern, '<ins>$&</ins>');
189 currentDisplayRows.push([
192 pkg.size ? '%.1024mB'.format(pkg.size)
193 : (altsize ? '~%.1024mB'.format(altsize) : '-'),
199 currentDisplayRows.sort(function(a, b) {
202 else if (a[0] > b[0])
208 pager.parentNode.style.display = '';
209 pager.setAttribute('data-offset', 100);
210 handlePage({ target: pager.querySelector('.prev') });
213 function handlePage(ev)
215 var filter = document.querySelector('input[name="filter"]'),
216 pager = ev.target.parentNode,
217 offset = +pager.getAttribute('data-offset'),
218 next = ev.target.classList.contains('next');
220 if ((next && (offset + 100) >= currentDisplayRows.length) ||
221 (!next && (offset < 100)))
224 offset += next ? 100 : -100;
225 pager.setAttribute('data-offset', offset);
226 pager.querySelector('.text').firstChild.data = currentDisplayRows.length
227 ? _('Displaying %d-%d of %d').format(1 + offset, Math.min(offset + 100, currentDisplayRows.length), currentDisplayRows.length)
231 pager.querySelector('.prev').setAttribute('disabled', 'disabled');
233 pager.querySelector('.prev').removeAttribute('disabled');
235 if ((offset + 100) >= currentDisplayRows.length)
236 pager.querySelector('.next').setAttribute('disabled', 'disabled');
238 pager.querySelector('.next').removeAttribute('disabled');
240 var placeholder = _('No information available');
244 E('span', {}, _('No packages matching "<strong>%h</strong>".').format(filter.value)), ' (',
245 E('a', { href: '#', onclick: 'handleReset(event)' }, _('Reset')), ')'
248 cbi_update_table('#packages', currentDisplayRows.slice(offset, offset + 100),
252 function handleMode(ev)
254 var tab = findParent(ev.target, 'li');
255 if (tab.getAttribute('data-mode') === currentDisplayMode)
258 tab.parentNode.querySelectorAll('li').forEach(function(li) {
259 li.classList.remove('cbi-tab');
260 li.classList.add('cbi-tab-disabled');
263 tab.classList.remove('cbi-tab-disabled');
264 tab.classList.add('cbi-tab');
266 currentDisplayMode = tab.getAttribute('data-mode');
268 display(document.querySelector('input[name="filter"]').value);
278 else if (c === '' || c >= '0' && c <= '9')
280 else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
281 return c.charCodeAt(0);
283 return c.charCodeAt(0) + 256;
286 function compareVersion(val, ref)
289 isdigit = { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 };
297 while (vi < val.length || ri < ref.length) {
300 while ((vi < val.length && !isdigit[val.charAt(vi)]) ||
301 (ri < ref.length && !isdigit[ref.charAt(ri)])) {
302 var vc = orderOf(val.charAt(vi)), rc = orderOf(ref.charAt(ri));
309 while (val.charAt(vi) === '0')
312 while (ref.charAt(ri) === '0')
315 while (isdigit[val.charAt(vi)] && isdigit[ref.charAt(ri)]) {
316 first_diff = first_diff || (val.charCodeAt(vi) - ref.charCodeAt(ri));
320 if (isdigit[val.charAt(vi)])
322 else if (isdigit[ref.charAt(ri)])
331 function versionSatisfied(ver, ref, vop)
333 var r = compareVersion(ver, ref);
357 function pkgStatus(pkg, vop, ver, info)
359 info.errors = info.errors || [];
360 info.install = info.install || [];
363 if (vop && !versionSatisfied(pkg.version, ver, vop)) {
366 (packages.available.providers[pkg.name] || []).forEach(function(p) {
367 if (!repl && versionSatisfied(p.version, ver, vop))
372 info.install.push(repl);
375 'data-tooltip': _('Requires update to %h %h')
376 .format(repl.name, repl.version)
377 }, _('Needs upgrade'));
380 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)));
383 'class': 'label warning',
384 'data-tooltip': _('Require version %h %h,\ninstalled %h')
385 .format(vop, ver, pkg.version)
386 }, _('Version incompatible'));
389 return E('span', { 'class': 'label notice' }, _('Installed'));
391 else if (!pkg.missing) {
392 if (!vop || versionSatisfied(pkg.version, ver, vop)) {
393 info.install.push(pkg);
394 return E('span', { 'class': 'label' }, _('Not installed'));
397 info.errors.push(_('The repository version of package <em>%h</em> is not compatible, require %s but only %s is available.')
398 .format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version)));
401 'class': 'label warning',
402 'data-tooltip': _('Require version %h %h,\ninstalled %h')
403 .format(vop, ver, pkg.version)
404 }, _('Version incompatible'));
407 info.errors.push(_('Required dependency package <em>%h</em> is not available in any repository.').format(pkg.name));
409 return E('span', { 'class': 'label warning' }, _('Not available'));
413 function renderDependencyItem(dep, info)
416 vop = dep.version ? dep.version[0] : null,
417 ver = dep.version ? dep.version[1] : null,
420 for (var i = 0; dep.pkgs && i < dep.pkgs.length; i++) {
421 var pkg = packages.installed.pkgs[dep.pkgs[i]] ||
422 packages.available.pkgs[dep.pkgs[i]] ||
426 li.appendChild(document.createTextNode(' | '));
431 text += ' (%.1024mB)'.format(pkg.installsize);
433 text += ' (~%.1024mB)'.format(pkg.size);
435 li.appendChild(E('span', { 'data-tooltip': pkg.description },
436 [ text, ' ', pkgStatus(pkg, vop, ver, info) ]));
438 (pkg.depends || []).forEach(function(d) {
439 if (depends.indexOf(d) === -1)
445 li.appendChild(E('span', {},
447 pkgStatus({ name: dep.name, missing: true }, vop, ver, info) ]));
449 var subdeps = renderDependencies(depends, info);
451 li.appendChild(subdeps);
456 function renderDependencies(depends, info)
458 var deps = depends || [],
461 info.seen = info.seen || [];
463 for (var i = 0; i < deps.length; i++) {
464 if (deps[i] === 'libc')
467 if (deps[i].match(/^(.+)\s+\((<=|<|>|>=|=|<<|>>)(.+)\)$/)) {
468 dep = RegExp.$1.trim();
469 vop = RegExp.$2.trim();
470 ver = RegExp.$3.trim();
473 dep = deps[i].trim();
482 (packages.installed.providers[dep] || []).forEach(function(p) {
483 if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
486 (packages.available.providers[dep] || []).forEach(function(p) {
487 if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
496 items.push(renderDependencyItem(info.seen[dep], info));
500 return E('ul', { 'class': 'deps' }, items);
505 function truncateVersion(v, op)
507 v = v.replace(/\b(([a-f0-9]{8})[a-f0-9]{24,32})\b/,
508 '<span data-tooltip="$1">$2…</span>');
510 if (!op || op === '=')
513 return '%h %h'.format(op, v);
516 function handleReset(ev)
518 var filter = document.querySelector('input[name="filter"]');
524 function handleInstall(ev)
526 var name = ev.target.getAttribute('data-package'),
527 pkg = packages.available.pkgs[name],
532 size = _('~%.1024mB installed').format(pkg.installsize);
534 size = _('~%.1024mB compressed').format(pkg.size);
538 var deps = renderDependencies(pkg.depends, depcache),
539 tree = null, errs = null, inst = null, desc = null;
541 if (depcache.errors && depcache.errors.length) {
542 errs = E('ul', { 'class': 'errors' });
543 depcache.errors.forEach(function(err) {
544 errs.appendChild(E('li', {}, err));
548 var totalsize = pkg.installsize || pkg.size || 0,
551 if (depcache.install && depcache.install.length)
552 depcache.install.forEach(function(ipkg) {
553 totalsize += ipkg.installsize || ipkg.size || 0;
558 _('Require approx. %.1024mB size for %d package(s) to install.')
559 .format(totalsize, totalpkgs));
562 tree = E('li', '<strong>%s:</strong>'.format(_('Dependencies')));
563 tree.appendChild(deps);
566 if (pkg.description) {
567 desc = E('div', {}, [
568 E('h5', {}, _('Description')),
569 E('p', {}, pkg.description)
573 L.showModal(_('Details for package <em>%h</em>').format(pkg.name), [
575 E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
576 E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)),
581 E('div', { 'class': 'right' }, [
588 'data-command': 'install',
589 'data-package': name,
590 'class': 'btn cbi-button-action',
597 function handleManualInstall(ev)
599 var name_or_url = document.querySelector('input[name="install"]').value,
601 'class': 'btn cbi-button-action',
602 'data-command': 'install',
603 'data-package': name_or_url,
604 'click': function(ev) {
605 document.querySelector('input[name="install"]').value = '';
608 }, _('Install')), warning;
610 if (!name_or_url.length) {
613 else if (name_or_url.indexOf('/') !== -1) {
614 warning = E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(name_or_url));
616 else if (!packages.available.providers[name_or_url]) {
617 warning = E('p', {}, _('The package <em>%h</em> is not available in any configured repository.').format(name_or_url));
621 warning = E('p', {}, _('Really attempt to install <em>%h</em>?').format(name_or_url));
624 L.showModal(_('Manually install package'), [
626 E('div', { 'class': 'right' }, [
628 'click': L.hideModal,
629 'class': 'btn cbi-button-neutral'
636 function handleConfig(ev)
638 L.showModal(_('OPKG Configuration'), [
639 E('p', { 'class': 'spinning' }, _('Loading configuration data…'))
642 L.get('admin/system/opkg/config', null, function(xhr, conf) {
644 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>.'))
647 Object.keys(conf).sort().forEach(function(file) {
648 body.push(E('h5', {}, '%h'.format(file)));
649 body.push(E('textarea', {
651 'rows': Math.max(Math.min(conf[file].match(/\n/g).length, 10), 3)
652 }, '%h'.format(conf[file])));
655 body.push(E('div', { 'class': 'right' }, [
657 'class': 'btn cbi-button-neutral',
662 'class': 'btn cbi-button-positive',
663 'click': function(ev) {
665 findParent(ev.target, '.modal').querySelectorAll('textarea[name]')
666 .forEach(function(textarea) {
667 data[textarea.getAttribute('name')] = textarea.value
670 L.showModal(_('OPKG Configuration'), [
671 E('p', { 'class': 'spinning' }, _('Saving configuration data…'))
674 L.post('admin/system/opkg/config', { data: JSON.stringify(data) }, L.hideModal);
679 L.showModal(_('OPKG Configuration'), body);
683 function handleRemove(ev)
685 var name = ev.target.getAttribute('data-package'),
686 pkg = packages.installed.pkgs[name],
687 avail = packages.available.pkgs[name] || {},
690 if (avail.installsize)
691 size = _('~%.1024mB installed').format(avail.installsize);
693 size = _('~%.1024mB compressed').format(avail.size);
697 if (avail.description) {
698 desc = E('div', {}, [
699 E('h5', {}, _('Description')),
700 E('p', {}, avail.description)
704 L.showModal(_('Remove package <em>%h</em>').format(pkg.name), [
706 E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
707 E('li', '<strong>%s:</strong> %h'.format(_('Size'), size))
710 E('div', { 'style': 'display:flex; justify-content:space-between; flex-wrap:wrap' }, [
712 E('input', { type: 'checkbox', checked: 'checked', name: 'autoremove' }),
713 _('Automatically remove unused dependencies')
715 E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [
722 'data-command': 'remove',
723 'data-package': name,
724 'class': 'btn cbi-button-negative',
732 function handleOpkg(ev)
734 var cmd = ev.target.getAttribute('data-command'),
735 pkg = ev.target.getAttribute('data-package'),
736 rem = document.querySelector('input[name="autoremove"]'),
737 url = 'admin/system/opkg/exec/' + encodeURIComponent(cmd);
739 var dlg = L.showModal(_('Executing package manager'), [
740 E('p', { 'class': 'spinning' },
741 _('Waiting for the <em>opkg %h</em> command to complete…').format(cmd))
744 L.post(url, { package: pkg, autoremove: rem ? rem.checked : false }, function(xhr, res) {
745 dlg.removeChild(dlg.lastChild);
748 dlg.appendChild(E('pre', [ res.stdout ]));
751 dlg.appendChild(E('h5', _('Errors')));
752 dlg.appendChild(E('pre', { 'class': 'errors' }, [ res.stderr ]));
756 dlg.appendChild(E('p', _('The <em>opkg %h</em> command failed with code <code>%d</code>.').format(cmd, (res.code & 0xff) || -1)));
758 dlg.appendChild(E('div', { 'class': 'right' },
761 'click': function() {
769 function updateLists()
771 cbi_update_table('#packages', [],
772 E('div', { 'class': 'spinning' }, _('Loading package information…')));
774 packages.available = { providers: {}, pkgs: {} };
775 packages.installed = { providers: {}, pkgs: {} };
777 L.get('admin/system/opkg/statvfs', null, function(xhr, stat) {
778 var pg = document.querySelector('.cbi-progressbar'),
779 total = stat.blocks || 0,
780 free = stat.bfree || 0;
782 pg.firstElementChild.style.width = Math.floor(total ? ((100 / total) * free) : 100) + '%';
783 pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, free * (stat.frsize || 0)));
785 L.get('admin/system/opkg/list/available', null, function(xhr) {
786 parseList(xhr.responseText, packages.available);
787 L.get('admin/system/opkg/list/installed', null, function(xhr) {
788 parseList(xhr.responseText, packages.installed);
789 display(document.querySelector('input[name="filter"]').value);
795 window.requestAnimationFrame(function() {
796 var filter = document.querySelector('input[name="filter"]'),
799 filter.value = filter.getAttribute('value');
800 filter.addEventListener('keyup',
802 if (keyTimeout !== null)
803 window.clearTimeout(keyTimeout);
805 keyTimeout = window.setTimeout(function() {
806 display(ev.target.value);
810 document.querySelector('#pager > .prev').addEventListener('click', handlePage);
811 document.querySelector('#pager > .next').addEventListener('click', handlePage);
812 document.querySelector('.cbi-tabmenu.mode').addEventListener('click', handleMode);