2 Copyright
2018 Jo-Philipp Wich
<jo@mein.io
>
3 Licensed to the public under the Apache License
2.0.
8 <style type=
"text/css">
13 justify-content: space-around;
20 box-sizing: border-box;
25 .controls
> *:first-child,
26 .controls
> *
> label {
31 .controls
> *
> .btn {
41 .controls
> div
> input {
50 ul.deps, ul.deps ul, ul.errors {
54 ul.deps li, ul.errors li {
60 display: inline-block;
78 display: inline-block;
84 <script type=
"text/javascript">//<![CDATA[
86 available: { providers: {}, pkgs: {} },
87 installed: { providers: {}, pkgs: {} }
90 var currentDisplayMode = 'available', currentDisplayRows = [];
92 function parseList(s, dest)
94 var re = /([^\n]*)\n/g,
95 pkg = null, key = null, val = null, m;
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();
105 if (key !== null && val !== null) {
113 var list = val.split(/\s*,\s*/);
114 if (list.length !==
1 || list[
0].length
> 0)
118 case 'installed-time':
119 pkg.installtime = new Date(+val *
1000);
122 case 'installed-size':
123 pkg.installsize = +val;
127 var stat = val.split(/\s+/),
140 pkg.installed = true;
147 pkg.essential = true;
155 case 'auto-installed':
169 if (m[
1].trim().match(/^([\w-]+)\s*:(.+)$/)) {
170 key = RegExp.$
1.toLowerCase();
171 val = RegExp.$
2.trim();
174 dest.pkgs[pkg.name] = pkg;
176 var provides = dest.providers[pkg.name] ? [] : [ pkg.name ];
179 provides.push.apply(provides, pkg.provides);
181 provides.forEach(function(p) {
182 dest.providers[p] = dest.providers[p] || [];
183 dest.providers[p].push(pkg);
189 function display(pattern)
191 var src = packages[currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode],
192 table = document.querySelector('#packages'),
193 pager = document.querySelector('#pager');
195 currentDisplayRows.length =
0;
197 if (typeof(pattern) === 'string' && pattern.length
> 0)
198 pattern = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig');
200 for (var name in src.pkgs) {
201 var pkg = src.pkgs[name],
202 desc = pkg.description || '',
205 if (!pkg.size && packages.available.pkgs[name])
206 altsize = packages.available.pkgs[name].size;
208 if (!desc && packages.available.pkgs[name])
209 desc = packages.available.pkgs[name].description || '';
211 desc = desc.split(/\n/);
212 desc = desc[
0].trim() + (desc.length
> 1 ? '…' : '');
214 if ((pattern instanceof RegExp) &&
215 !name.match(pattern) && !desc.match(pattern))
220 if (currentDisplayMode === 'updates') {
221 var avail = packages.available.pkgs[name];
222 if (!avail || avail.version === pkg.version)
225 ver = '%s » %s'.format(
226 truncateVersion(pkg.version || '-'),
227 truncateVersion(avail.version || '-'));
230 'class': 'btn cbi-button-positive',
231 'data-package': name,
232 'click': handleInstall
235 else if (currentDisplayMode === 'installed') {
236 ver = truncateVersion(pkg.version || '-');
238 'class': 'btn cbi-button-negative',
239 'data-package': name,
240 'click': handleRemove
244 ver = truncateVersion(pkg.version || '-');
246 if (!packages.installed.pkgs[name])
248 'class': 'btn cbi-button-action',
249 'data-package': name,
250 'click': handleInstall
252 else if (packages.installed.pkgs[name].version != pkg.version)
254 'class': 'btn cbi-button-positive',
255 'data-package': name,
256 'click': handleInstall
260 'class': 'btn cbi-button-neutral',
261 'disabled': 'disabled'
265 name = '%h'.format(name);
266 desc = '%h'.format(desc || '-');
269 name = name.replace(pattern, '
<ins>$&
</ins>');
270 desc = desc.replace(pattern, '
<ins>$&
</ins>');
273 currentDisplayRows.push([
276 pkg.size ? '%
.1024mB'.format(pkg.size)
277 : (altsize ? '~%
.1024mB'.format(altsize) : '-'),
283 currentDisplayRows.sort(function(a, b) {
286 else if (a[
0]
> b[
0])
292 pager.parentNode.style.display = '';
293 pager.setAttribute('data-offset',
100);
294 handlePage({ target: pager.querySelector('.prev') });
297 function handlePage(ev)
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');
304 if ((next && (offset +
100)
>= currentDisplayRows.length) ||
305 (!next && (offset <
100)))
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)
315 pager.querySelector('.prev').setAttribute('disabled', 'disabled');
317 pager.querySelector('.prev').removeAttribute('disabled');
319 if ((offset +
100)
>= currentDisplayRows.length)
320 pager.querySelector('.next').setAttribute('disabled', 'disabled');
322 pager.querySelector('.next').removeAttribute('disabled');
324 var placeholder = _('No information available');
328 E('span', {}, _('No packages matching
"<strong>%h</strong>".').format(filter.value)), ' (',
329 E('a', { href: '#', onclick: 'handleReset(event)' }, _('Reset')), ')'
332 cbi_update_table('#packages', currentDisplayRows.slice(offset, offset +
100),
336 function handleMode(ev)
338 var tab = findParent(ev.target, 'li');
339 if (tab.getAttribute('data-mode') === currentDisplayMode)
342 tab.parentNode.querySelectorAll('li').forEach(function(li) {
343 li.classList.remove('cbi-tab');
344 li.classList.add('cbi-tab-disabled');
347 tab.classList.remove('cbi-tab-disabled');
348 tab.classList.add('cbi-tab');
350 currentDisplayMode = tab.getAttribute('data-mode');
352 display(document.querySelector('input[
name=
"filter"]').value);
362 else if (c === '' || c
>= '
0' && c <= '
9')
364 else if ((c
>= 'a' && c <= 'z') || (c
>= 'A' && c <= 'Z'))
365 return c.charCodeAt(
0);
367 return c.charCodeAt(
0) +
256;
370 function compareVersion(val, ref)
373 isdigit = {
0:
1,
1:
1,
2:
1,
3:
1,
4:
1,
5:
1,
6:
1,
7:
1,
8:
1,
9:
1 };
378 while (vi < val.length || ri < ref.length) {
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));
390 while (val.charAt(vi) === '
0')
393 while (ref.charAt(ri) === '
0')
396 while (isdigit[val.charAt(vi)] && isdigit[ref.charAt(ri)]) {
397 first_diff = first_diff || (val.charCodeAt(vi) - ref.charCodeAt(ri));
401 if (isdigit[val.charAt(vi)])
403 else if (isdigit[ref.charAt(ri)])
412 function versionSatisfied(ver, ref, vop)
414 var r = compareVersion(ver, ref);
438 function pkgStatus(pkg, vop, ver, info)
440 info.errors = info.errors || [];
441 info.install = info.install || [];
444 if (vop && !versionSatisfied(pkg.version, ver, vop)) {
447 (packages.available.providers[pkg.name] || []).forEach(function(p) {
448 if (!repl && versionSatisfied(p.version, ver, vop))
453 info.install.push(repl);
456 'data-tooltip': _('Requires update to %h %h')
457 .format(repl.name, repl.version)
458 }, _('Needs upgrade'));
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)));
464 'class': 'label warning',
465 'data-tooltip': _('Require version %h %h,\ninstalled %h')
466 .format(vop, ver, pkg.version)
467 }, _('Version incompatible'));
470 return E('span', { 'class': 'label notice' }, _('Installed'));
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'));
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)));
482 'class': 'label warning',
483 'data-tooltip': _('Require version %h %h,\ninstalled %h')
484 .format(vop, ver, pkg.version)
485 }, _('Version incompatible'));
488 info.errors.push(_('Required dependency package
<em>%h
</em> is not available in any repository.').format(pkg.name));
490 return E('span', { 'class': 'label warning' }, _('Not available'));
494 function renderDependencyItem(dep, info)
497 vop = dep.version ? dep.version[
0] : null,
498 ver = dep.version ? dep.version[
1] : null,
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]] ||
507 li.appendChild(document.createTextNode(' | '));
512 text += ' (%
.1024mB)'.format(pkg.installsize);
514 text += ' (~%
.1024mB)'.format(pkg.size);
516 li.appendChild(E('span', { 'data-tooltip': pkg.description },
517 [ text, ' ', pkgStatus(pkg, vop, ver, info) ]));
519 (pkg.depends || []).forEach(function(d) {
520 if (depends.indexOf(d) === -
1)
526 li.appendChild(E('span', {},
528 pkgStatus({ name: dep.name, missing: true }, vop, ver, info) ]));
530 var subdeps = renderDependencies(depends, info);
532 li.appendChild(subdeps);
537 function renderDependencies(depends, info)
539 var deps = depends || [],
542 info.seen = info.seen || [];
544 for (var i =
0; i < deps.length; i++) {
545 if (deps[i] === 'libc')
548 if (deps[i].match(/^(.+)\s+\((<=|<|
>|
>=|=|<<|
>>)(.+)\)$/)) {
549 dep = RegExp.$
1.trim();
550 vop = RegExp.$
2.trim();
551 ver = RegExp.$
3.trim();
554 dep = deps[i].trim();
563 (packages.installed.providers[dep] || []).forEach(function(p) {
564 if (pkgs.indexOf(p.name) === -
1) pkgs.push(p.name);
567 (packages.available.providers[dep] || []).forEach(function(p) {
568 if (pkgs.indexOf(p.name) === -
1) pkgs.push(p.name);
577 items.push(renderDependencyItem(info.seen[dep], info));
581 return E('ul', { 'class': 'deps' }, items);
586 function truncateVersion(v, op)
588 v = v.replace(/\b(([a-f0-
9]{
8})[a-f0-
9]{
24,
32})\b/,
589 '
<span data-tooltip=
"$1">$
2…
</span>');
591 if (!op || op === '=')
594 return '%h %h'.format(op, v);
597 function handleReset(ev)
599 var filter = document.querySelector('input[
name=
"filter"]');
605 function handleInstall(ev)
607 var name = ev.target.getAttribute('data-package'),
608 pkg = packages.available.pkgs[name],
613 size = _('~%
.1024mB installed').format(pkg.installsize);
615 size = _('~%
.1024mB compressed').format(pkg.size);
619 var deps = renderDependencies(pkg.depends, depcache),
620 tree = null, errs = null, inst = null, desc = null;
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));
629 var totalsize = pkg.installsize || pkg.size ||
0,
632 if (depcache.install && depcache.install.length)
633 depcache.install.forEach(function(ipkg) {
634 totalsize += ipkg.installsize || ipkg.size ||
0;
639 _('Require approx. %
.1024mB size for %d package(s) to install.')
640 .format(totalsize, totalpkgs));
643 tree = E('li', '
<strong>%s:
</strong>'.format(_('Dependencies')));
644 tree.appendChild(deps);
647 if (pkg.description) {
648 desc = E('div', {}, [
649 E('h5', {}, _('Description')),
650 E('p', {}, pkg.description)
654 showModal(_('Details for package
<em>%h
</em>').format(pkg.name), [
656 E('li', '
<strong>%s:
</strong> %h'.format(_('Version'), pkg.version)),
657 E('li', '
<strong>%s:
</strong> %h'.format(_('Size'), size)),
662 E('div', { 'class': 'right' }, [
669 'data-command': 'install',
670 'data-package': name,
671 'class': 'btn cbi-button-action',
678 function handleManualInstall(ev)
680 var name_or_url = document.querySelector('input[
name=
"install"]').value,
681 install = E('button', {
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 = '';
689 }, _('Install')), warning;
691 if (!name_or_url.length) {
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));
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));
702 warning = E('p', {}, _('Really attempt to install
<em>%h
</em>?').format(name_or_url));
705 showModal(_('Manually install package'), [
707 E('div', { 'class': 'right' }, [
710 'class': 'btn cbi-button-neutral'
717 function handleConfig(ev)
719 showModal(_('OPKG Configuration'), [
720 E('p', { 'class': 'spinning' }, _('Loading configuration data…'))
723 XHR.get('<%=url(
"admin/system/opkg/config")%
>', null, function(xhr, conf) {
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>.'))
728 Object.keys(conf).sort().forEach(function(file) {
729 body.push(E('h5', {}, '%h'.format(file)));
730 body.push(E('textarea', {
732 'rows': Math.max(Math.min(conf[file].match(/\n/g).length,
10),
3)
733 }, '%h'.format(conf[file])));
736 body.push(E('div', { 'class': 'right' }, [
738 'class': 'btn cbi-button-neutral',
743 'class': 'btn cbi-button-positive',
744 'click': function(ev) {
746 findParent(ev.target, '.modal').querySelectorAll('textarea[name]')
747 .forEach(function(textarea) {
748 data[textarea.getAttribute('name')] = textarea.value
751 showModal(_('OPKG Configuration'), [
752 E('p', { 'class': 'spinning' }, _('Saving configuration data…'))
755 (new XHR()).post('<%=url(
"admin/system/opkg/config")%
>',
756 { token: '<%=token%
>', data: JSON.stringify(data) }, hideModal);
761 showModal(_('OPKG Configuration'), body);
765 function handleRemove(ev)
767 var name = ev.target.getAttribute('data-package'),
768 pkg = packages.installed.pkgs[name],
769 avail = packages.available.pkgs[name] || {},
772 if (avail.installsize)
773 size = _('~%
.1024mB installed').format(avail.installsize);
775 size = _('~%
.1024mB compressed').format(avail.size);
779 if (avail.description) {
780 desc = E('div', {}, [
781 E('h5', {}, _('Description')),
782 E('p', {}, avail.description)
786 showModal(_('Remove package
<em>%h
</em>').format(pkg.name), [
788 E('li', '
<strong>%s:
</strong> %h'.format(_('Version'), pkg.version)),
789 E('li', '
<strong>%s:
</strong> %h'.format(_('Size'), size))
792 E('div', { 'style': 'display:flex; justify-content:space-between; flex-wrap:wrap' }, [
794 E('input', { type: 'checkbox', checked: 'checked', name: 'autoremove' }),
795 _('Automatically remove unused dependencies')
797 E('div', { 'style': 'flex-grow:
1', 'class': 'right' }, [
804 'data-command': 'remove',
805 'data-package': name,
806 'class': 'btn cbi-button-negative',
814 function handleOpkg(ev)
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);
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))
826 (new XHR()).post(url, {
829 autoremove: rem ? rem.checked : false
830 }, function(xhr, res) {
831 dlg.removeChild(dlg.lastChild);
834 dlg.appendChild(E('pre', [ res.stdout ]));
837 dlg.appendChild(E('h5', _('Errors')));
838 dlg.appendChild(E('pre', { 'class': 'errors' }, [ res.stderr ]));
842 dlg.appendChild(E('p', _('The
<em>opkg %h
</em> command failed with code
<code>%d
</code>.').format(cmd, (res.code &
0xff) || -
1)));
844 dlg.appendChild(E('div', { 'class': 'right' },
847 'click': function() {
855 function updateLists()
857 cbi_update_table('#packages', [],
858 E('div', { 'class': 'spinning' }, _('Loading package information…')));
860 packages.available = { providers: {}, pkgs: {} };
861 packages.installed = { providers: {}, pkgs: {} };
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;
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)));
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);
881 window.requestAnimationFrame(function() {
882 var filter = document.querySelector('input[
name=
"filter"]'),
886 filter.addEventListener('keyup',
888 if (keyTimeout !== null)
889 window.clearTimeout(keyTimeout);
891 keyTimeout = window.setTimeout(function() {
892 display(ev.target.value);
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);
904 <h2><%:Software%
></h2>
906 <div class=
"controls">
908 <label><%:Free space%
>:
</label>
909 <div class=
"cbi-progressbar" title=
"<%:unknown%>">
915 <label><%:Filter%
>:
</label>
916 <input type=
"text" name=
"filter" placeholder=
"<%:Type to filter…%>" /><!--
917 --><button class=
"btn cbi-button" onclick=
"handleReset(event)"><%:Clear%
></button>
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 --><button class=
"btn cbi-button cbi-button-action" onclick=
"handleManualInstall(event)"><%:OK%
></button>
927 <label><%:Actions%
>:
</label>
928 <button class=
"btn cbi-button-positive" data-command=
"update" onclick=
"handleOpkg(event)"><%:Update lists…%
></button>
930 <button class=
"btn cbi-button-neutral" onclick=
"handleConfig(event)"><%:Configure opkg…%
></button>
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>
940 <div class=
"controls" style=
"display:none">
941 <div id=
"pager" class=
"center">
942 <button class=
"btn cbi-button-neutral prev" aria-label=
"<%:Previous page%>">«
</button>
943 <div class=
"text">dummy
</div>
944 <button class=
"btn cbi-button-neutral next" aria-label=
"<%:Next page%>">»
</button>
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"> </div>