--- /dev/null
+var packages = {
+ available: { providers: {}, pkgs: {} },
+ installed: { providers: {}, pkgs: {} }
+};
+
+var currentDisplayMode = 'available', currentDisplayRows = [];
+
+function parseList(s, dest)
+{
+ var re = /([^\n]*)\n/g,
+ pkg = null, key = null, val = null, m;
+
+ while ((m = re.exec(s)) !== null) {
+ if (m[1].match(/^\s(.*)$/)) {
+ if (pkg !== null && key !== null && val !== null)
+ val += '\n' + RegExp.$1.trim();
+
+ continue;
+ }
+
+ if (key !== null && val !== null) {
+ switch (key) {
+ case 'package':
+ pkg = { name: val };
+ break;
+
+ case 'depends':
+ case 'provides':
+ var list = val.split(/\s*,\s*/);
+ if (list.length !== 1 || list[0].length > 0)
+ pkg[key] = list;
+ break;
+
+ case 'installed-time':
+ pkg.installtime = new Date(+val * 1000);
+ break;
+
+ case 'installed-size':
+ pkg.installsize = +val;
+ break;
+
+ case 'status':
+ var stat = val.split(/\s+/),
+ mode = stat[1],
+ installed = stat[2];
+
+ switch (mode) {
+ case 'user':
+ case 'hold':
+ pkg[mode] = true;
+ break;
+ }
+
+ switch (installed) {
+ case 'installed':
+ pkg.installed = true;
+ break;
+ }
+ break;
+
+ case 'essential':
+ if (val === 'yes')
+ pkg.essential = true;
+ break;
+
+ case 'size':
+ pkg.size = +val;
+ break;
+
+ case 'architecture':
+ case 'auto-installed':
+ case 'filename':
+ case 'sha256sum':
+ case 'section':
+ break;
+
+ default:
+ pkg[key] = val;
+ break;
+ }
+
+ key = val = null;
+ }
+
+ if (m[1].trim().match(/^([\w-]+)\s*:(.+)$/)) {
+ key = RegExp.$1.toLowerCase();
+ val = RegExp.$2.trim();
+ }
+ else {
+ dest.pkgs[pkg.name] = pkg;
+
+ var provides = dest.providers[pkg.name] ? [] : [ pkg.name ];
+
+ if (pkg.provides)
+ provides.push.apply(provides, pkg.provides);
+
+ provides.forEach(function(p) {
+ dest.providers[p] = dest.providers[p] || [];
+ dest.providers[p].push(pkg);
+ });
+ }
+ }
+}
+
+function display(pattern)
+{
+ var src = packages[currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode],
+ table = document.querySelector('#packages'),
+ pager = document.querySelector('#pager');
+
+ currentDisplayRows.length = 0;
+
+ if (typeof(pattern) === 'string' && pattern.length > 0)
+ pattern = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig');
+
+ for (var name in src.pkgs) {
+ var pkg = src.pkgs[name],
+ desc = pkg.description || '',
+ altsize = null;
+
+ if (!pkg.size && packages.available.pkgs[name])
+ altsize = packages.available.pkgs[name].size;
+
+ if (!desc && packages.available.pkgs[name])
+ desc = packages.available.pkgs[name].description || '';
+
+ desc = desc.split(/\n/);
+ desc = desc[0].trim() + (desc.length > 1 ? '…' : '');
+
+ if ((pattern instanceof RegExp) &&
+ !name.match(pattern) && !desc.match(pattern))
+ continue;
+
+ var btn, ver;
+
+ if (currentDisplayMode === 'updates') {
+ var avail = packages.available.pkgs[name];
+ if (!avail || avail.version === pkg.version)
+ continue;
+
+ ver = '%s » %s'.format(
+ truncateVersion(pkg.version || '-'),
+ truncateVersion(avail.version || '-'));
+
+ btn = E('div', {
+ 'class': 'btn cbi-button-positive',
+ 'data-package': name,
+ 'click': handleInstall
+ }, _('Upgrade…'));
+ }
+ else if (currentDisplayMode === 'installed') {
+ ver = truncateVersion(pkg.version || '-');
+ btn = E('div', {
+ 'class': 'btn cbi-button-negative',
+ 'data-package': name,
+ 'click': handleRemove
+ }, _('Remove'));
+ }
+ else {
+ ver = truncateVersion(pkg.version || '-');
+
+ if (!packages.installed.pkgs[name])
+ btn = E('div', {
+ 'class': 'btn cbi-button-action',
+ 'data-package': name,
+ 'click': handleInstall
+ }, _('Install…'));
+ else if (packages.installed.pkgs[name].version != pkg.version)
+ btn = E('div', {
+ 'class': 'btn cbi-button-positive',
+ 'data-package': name,
+ 'click': handleInstall
+ }, _('Upgrade…'));
+ else
+ btn = E('div', {
+ 'class': 'btn cbi-button-neutral',
+ 'disabled': 'disabled'
+ }, _('Installed'));
+ }
+
+ name = '%h'.format(name);
+ desc = '%h'.format(desc || '-');
+
+ if (pattern) {
+ name = name.replace(pattern, '<ins>$&</ins>');
+ desc = desc.replace(pattern, '<ins>$&</ins>');
+ }
+
+ currentDisplayRows.push([
+ name,
+ ver,
+ pkg.size ? '%.1024mB'.format(pkg.size)
+ : (altsize ? '~%.1024mB'.format(altsize) : '-'),
+ desc,
+ btn
+ ]);
+ }
+
+ currentDisplayRows.sort(function(a, b) {
+ if (a[0] < b[0])
+ return -1;
+ else if (a[0] > b[0])
+ return 1;
+ else
+ return 0;
+ });
+
+ pager.parentNode.style.display = '';
+ pager.setAttribute('data-offset', 100);
+ handlePage({ target: pager.querySelector('.prev') });
+}
+
+function handlePage(ev)
+{
+ var filter = document.querySelector('input[name="filter"]'),
+ pager = ev.target.parentNode,
+ offset = +pager.getAttribute('data-offset'),
+ next = ev.target.classList.contains('next');
+
+ if ((next && (offset + 100) >= currentDisplayRows.length) ||
+ (!next && (offset < 100)))
+ return;
+
+ offset += next ? 100 : -100;
+ pager.setAttribute('data-offset', offset);
+ pager.querySelector('.text').firstChild.data = currentDisplayRows.length
+ ? _('Displaying %d-%d of %d').format(1 + offset, Math.min(offset + 100, currentDisplayRows.length), currentDisplayRows.length)
+ : _('No packages');
+
+ if (offset < 100)
+ pager.querySelector('.prev').setAttribute('disabled', 'disabled');
+ else
+ pager.querySelector('.prev').removeAttribute('disabled');
+
+ if ((offset + 100) >= currentDisplayRows.length)
+ pager.querySelector('.next').setAttribute('disabled', 'disabled');
+ else
+ pager.querySelector('.next').removeAttribute('disabled');
+
+ var placeholder = _('No information available');
+
+ if (filter.value)
+ placeholder = [
+ E('span', {}, _('No packages matching "<strong>%h</strong>".').format(filter.value)), ' (',
+ E('a', { href: '#', onclick: 'handleReset(event)' }, _('Reset')), ')'
+ ];
+
+ cbi_update_table('#packages', currentDisplayRows.slice(offset, offset + 100),
+ placeholder);
+}
+
+function handleMode(ev)
+{
+ var tab = findParent(ev.target, 'li');
+ if (tab.getAttribute('data-mode') === currentDisplayMode)
+ return;
+
+ tab.parentNode.querySelectorAll('li').forEach(function(li) {
+ li.classList.remove('cbi-tab');
+ li.classList.add('cbi-tab-disabled');
+ });
+
+ tab.classList.remove('cbi-tab-disabled');
+ tab.classList.add('cbi-tab');
+
+ currentDisplayMode = tab.getAttribute('data-mode');
+
+ display(document.querySelector('input[name="filter"]').value);
+
+ ev.target.blur();
+ ev.preventDefault();
+}
+
+function orderOf(c)
+{
+ if (c === '~')
+ return -1;
+ else if (c === '' || c >= '0' && c <= '9')
+ return 0;
+ else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
+ return c.charCodeAt(0);
+ else
+ return c.charCodeAt(0) + 256;
+}
+
+function compareVersion(val, ref)
+{
+ var vi = 0, ri = 0,
+ isdigit = { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 };
+
+ val = val || '';
+ ref = ref || '';
+
+ while (vi < val.length || ri < ref.length) {
+ var first_diff = 0;
+
+ while ((vi < val.length && !isdigit[val.charAt(vi)]) ||
+ (ri < ref.length && !isdigit[ref.charAt(ri)])) {
+ var vc = orderOf(val.charAt(vi)), rc = orderOf(ref.charAt(ri));
+ if (vc !== rc)
+ return vc - rc;
+
+ vi++; ri++;
+ }
+
+ while (val.charAt(vi) === '0')
+ vi++;
+
+ while (ref.charAt(ri) === '0')
+ ri++;
+
+ while (isdigit[val.charAt(vi)] && isdigit[ref.charAt(ri)]) {
+ first_diff = first_diff || (val.charCodeAt(vi) - ref.charCodeAt(ri));
+ vi++; ri++;
+ }
+
+ if (isdigit[val.charAt(vi)])
+ return 1;
+ else if (isdigit[ref.charAt(ri)])
+ return -1;
+ else if (first_diff)
+ return first_diff;
+ }
+
+ return 0;
+}
+
+function versionSatisfied(ver, ref, vop)
+{
+ var r = compareVersion(ver, ref);
+
+ switch (vop) {
+ case '<':
+ case '<=':
+ return r <= 0;
+
+ case '>':
+ case '>=':
+ return r >= 0;
+
+ case '<<':
+ return r < 0;
+
+ case '>>':
+ return r > 0;
+
+ case '=':
+ return r == 0;
+ }
+
+ return false;
+}
+
+function pkgStatus(pkg, vop, ver, info)
+{
+ info.errors = info.errors || [];
+ info.install = info.install || [];
+
+ if (pkg.installed) {
+ if (vop && !versionSatisfied(pkg.version, ver, vop)) {
+ var repl = null;
+
+ (packages.available.providers[pkg.name] || []).forEach(function(p) {
+ if (!repl && versionSatisfied(p.version, ver, vop))
+ repl = p;
+ });
+
+ if (repl) {
+ info.install.push(repl);
+ return E('span', {
+ 'class': 'label',
+ 'data-tooltip': _('Requires update to %h %h')
+ .format(repl.name, repl.version)
+ }, _('Needs upgrade'));
+ }
+
+ 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)));
+
+ return E('span', {
+ 'class': 'label warning',
+ 'data-tooltip': _('Require version %h %h,\ninstalled %h')
+ .format(vop, ver, pkg.version)
+ }, _('Version incompatible'));
+ }
+
+ return E('span', { 'class': 'label notice' }, _('Installed'));
+ }
+ else if (!pkg.missing) {
+ if (!vop || versionSatisfied(pkg.version, ver, vop)) {
+ info.install.push(pkg);
+ return E('span', { 'class': 'label' }, _('Not installed'));
+ }
+
+ info.errors.push(_('The repository version of package <em>%h</em> is not compatible, require %s but only %s is available.')
+ .format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version)));
+
+ return E('span', {
+ 'class': 'label warning',
+ 'data-tooltip': _('Require version %h %h,\ninstalled %h')
+ .format(vop, ver, pkg.version)
+ }, _('Version incompatible'));
+ }
+ else {
+ info.errors.push(_('Required dependency package <em>%h</em> is not available in any repository.').format(pkg.name));
+
+ return E('span', { 'class': 'label warning' }, _('Not available'));
+ }
+}
+
+function renderDependencyItem(dep, info)
+{
+ var li = E('li'),
+ vop = dep.version ? dep.version[0] : null,
+ ver = dep.version ? dep.version[1] : null,
+ depends = [];
+
+ for (var i = 0; dep.pkgs && i < dep.pkgs.length; i++) {
+ var pkg = packages.installed.pkgs[dep.pkgs[i]] ||
+ packages.available.pkgs[dep.pkgs[i]] ||
+ { name: dep.name };
+
+ if (i > 0)
+ li.appendChild(document.createTextNode(' | '));
+
+ var text = pkg.name;
+
+ if (pkg.installsize)
+ text += ' (%.1024mB)'.format(pkg.installsize);
+ else if (pkg.size)
+ text += ' (~%.1024mB)'.format(pkg.size);
+
+ li.appendChild(E('span', { 'data-tooltip': pkg.description },
+ [ text, ' ', pkgStatus(pkg, vop, ver, info) ]));
+
+ (pkg.depends || []).forEach(function(d) {
+ if (depends.indexOf(d) === -1)
+ depends.push(d);
+ });
+ }
+
+ if (!li.firstChild)
+ li.appendChild(E('span', {},
+ [ dep.name, ' ',
+ pkgStatus({ name: dep.name, missing: true }, vop, ver, info) ]));
+
+ var subdeps = renderDependencies(depends, info);
+ if (subdeps)
+ li.appendChild(subdeps);
+
+ return li;
+}
+
+function renderDependencies(depends, info)
+{
+ var deps = depends || [],
+ items = [];
+
+ info.seen = info.seen || [];
+
+ for (var i = 0; i < deps.length; i++) {
+ if (deps[i] === 'libc')
+ continue;
+
+ if (deps[i].match(/^(.+)\s+\((<=|<|>|>=|=|<<|>>)(.+)\)$/)) {
+ dep = RegExp.$1.trim();
+ vop = RegExp.$2.trim();
+ ver = RegExp.$3.trim();
+ }
+ else {
+ dep = deps[i].trim();
+ vop = ver = null;
+ }
+
+ if (info.seen[dep])
+ continue;
+
+ var pkgs = [];
+
+ (packages.installed.providers[dep] || []).forEach(function(p) {
+ if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
+ });
+
+ (packages.available.providers[dep] || []).forEach(function(p) {
+ if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
+ });
+
+ info.seen[dep] = {
+ name: dep,
+ pkgs: pkgs,
+ version: [vop, ver]
+ };
+
+ items.push(renderDependencyItem(info.seen[dep], info));
+ }
+
+ if (items.length)
+ return E('ul', { 'class': 'deps' }, items);
+
+ return null;
+}
+
+function truncateVersion(v, op)
+{
+ v = v.replace(/\b(([a-f0-9]{8})[a-f0-9]{24,32})\b/,
+ '<span data-tooltip="$1">$2…</span>');
+
+ if (!op || op === '=')
+ return v;
+
+ return '%h %h'.format(op, v);
+}
+
+function handleReset(ev)
+{
+ var filter = document.querySelector('input[name="filter"]');
+
+ filter.value = '';
+ display();
+}
+
+function handleInstall(ev)
+{
+ var name = ev.target.getAttribute('data-package'),
+ pkg = packages.available.pkgs[name],
+ depcache = {},
+ size;
+
+ if (pkg.installsize)
+ size = _('~%.1024mB installed').format(pkg.installsize);
+ else if (pkg.size)
+ size = _('~%.1024mB compressed').format(pkg.size);
+ else
+ size = _('unknown');
+
+ var deps = renderDependencies(pkg.depends, depcache),
+ tree = null, errs = null, inst = null, desc = null;
+
+ if (depcache.errors && depcache.errors.length) {
+ errs = E('ul', { 'class': 'errors' });
+ depcache.errors.forEach(function(err) {
+ errs.appendChild(E('li', {}, err));
+ });
+ }
+
+ var totalsize = pkg.installsize || pkg.size || 0,
+ totalpkgs = 1;
+
+ if (depcache.install && depcache.install.length)
+ depcache.install.forEach(function(ipkg) {
+ totalsize += ipkg.installsize || ipkg.size || 0;
+ totalpkgs++;
+ });
+
+ inst = E('p', {},
+ _('Require approx. %.1024mB size for %d package(s) to install.')
+ .format(totalsize, totalpkgs));
+
+ if (deps) {
+ tree = E('li', '<strong>%s:</strong>'.format(_('Dependencies')));
+ tree.appendChild(deps);
+ }
+
+ if (pkg.description) {
+ desc = E('div', {}, [
+ E('h5', {}, _('Description')),
+ E('p', {}, pkg.description)
+ ]);
+ }
+
+ L.showModal(_('Details for package <em>%h</em>').format(pkg.name), [
+ E('ul', {}, [
+ E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
+ E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)),
+ tree || '',
+ ]),
+ desc || '',
+ errs || inst || '',
+ E('div', { 'class': 'right' }, [
+ E('div', {
+ 'class': 'btn',
+ 'click': L.hideModal
+ }, _('Cancel')),
+ ' ',
+ E('div', {
+ 'data-command': 'install',
+ 'data-package': name,
+ 'class': 'btn cbi-button-action',
+ 'click': handleOpkg
+ }, _('Install'))
+ ])
+ ]);
+}
+
+function handleManualInstall(ev)
+{
+ var name_or_url = document.querySelector('input[name="install"]').value,
+ install = E('div', {
+ 'class': 'btn cbi-button-action',
+ 'data-command': 'install',
+ 'data-package': name_or_url,
+ 'click': function(ev) {
+ document.querySelector('input[name="install"]').value = '';
+ handleOpkg(ev);
+ }
+ }, _('Install')), warning;
+
+ if (!name_or_url.length) {
+ return;
+ }
+ else if (name_or_url.indexOf('/') !== -1) {
+ warning = E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(name_or_url));
+ }
+ else if (!packages.available.providers[name_or_url]) {
+ warning = E('p', {}, _('The package <em>%h</em> is not available in any configured repository.').format(name_or_url));
+ install = '';
+ }
+ else {
+ warning = E('p', {}, _('Really attempt to install <em>%h</em>?').format(name_or_url));
+ }
+
+ L.showModal(_('Manually install package'), [
+ warning,
+ E('div', { 'class': 'right' }, [
+ E('div', {
+ 'click': L.hideModal,
+ 'class': 'btn cbi-button-neutral'
+ }, _('Cancel')),
+ ' ', install
+ ])
+ ]);
+}
+
+function handleConfig(ev)
+{
+ L.showModal(_('OPKG Configuration'), [
+ E('p', { 'class': 'spinning' }, _('Loading configuration data…'))
+ ]);
+
+ L.get('admin/system/opkg/config', null, function(xhr, conf) {
+ var body = [
+ 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>.'))
+ ];
+
+ Object.keys(conf).sort().forEach(function(file) {
+ body.push(E('h5', {}, '%h'.format(file)));
+ body.push(E('textarea', {
+ 'name': file,
+ 'rows': Math.max(Math.min(conf[file].match(/\n/g).length, 10), 3)
+ }, '%h'.format(conf[file])));
+ });
+
+ body.push(E('div', { 'class': 'right' }, [
+ E('div', {
+ 'class': 'btn cbi-button-neutral',
+ 'click': L.hideModal
+ }, _('Cancel')),
+ ' ',
+ E('div', {
+ 'class': 'btn cbi-button-positive',
+ 'click': function(ev) {
+ var data = {};
+ findParent(ev.target, '.modal').querySelectorAll('textarea[name]')
+ .forEach(function(textarea) {
+ data[textarea.getAttribute('name')] = textarea.value
+ });
+
+ L.showModal(_('OPKG Configuration'), [
+ E('p', { 'class': 'spinning' }, _('Saving configuration data…'))
+ ]);
+
+ L.post('admin/system/opkg/config', { data: JSON.stringify(data) }, L.hideModal);
+ }
+ }, _('Save')),
+ ]));
+
+ L.showModal(_('OPKG Configuration'), body);
+ });
+}
+
+function handleRemove(ev)
+{
+ var name = ev.target.getAttribute('data-package'),
+ pkg = packages.installed.pkgs[name],
+ avail = packages.available.pkgs[name] || {},
+ size, desc;
+
+ if (avail.installsize)
+ size = _('~%.1024mB installed').format(avail.installsize);
+ else if (avail.size)
+ size = _('~%.1024mB compressed').format(avail.size);
+ else
+ size = _('unknown');
+
+ if (avail.description) {
+ desc = E('div', {}, [
+ E('h5', {}, _('Description')),
+ E('p', {}, avail.description)
+ ]);
+ }
+
+ L.showModal(_('Remove package <em>%h</em>').format(pkg.name), [
+ E('ul', {}, [
+ E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
+ E('li', '<strong>%s:</strong> %h'.format(_('Size'), size))
+ ]),
+ desc || '',
+ E('div', { 'style': 'display:flex; justify-content:space-between; flex-wrap:wrap' }, [
+ E('label', {}, [
+ E('input', { type: 'checkbox', checked: 'checked', name: 'autoremove' }),
+ _('Automatically remove unused dependencies')
+ ]),
+ E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [
+ E('div', {
+ 'class': 'btn',
+ 'click': L.hideModal
+ }, _('Cancel')),
+ ' ',
+ E('div', {
+ 'data-command': 'remove',
+ 'data-package': name,
+ 'class': 'btn cbi-button-negative',
+ 'click': handleOpkg
+ }, _('Remove'))
+ ])
+ ])
+ ]);
+}
+
+function handleOpkg(ev)
+{
+ var cmd = ev.target.getAttribute('data-command'),
+ pkg = ev.target.getAttribute('data-package'),
+ rem = document.querySelector('input[name="autoremove"]'),
+ url = 'admin/system/opkg/exec/' + encodeURIComponent(cmd);
+
+ var dlg = L.showModal(_('Executing package manager'), [
+ E('p', { 'class': 'spinning' },
+ _('Waiting for the <em>opkg %h</em> command to complete…').format(cmd))
+ ]);
+
+ L.post(url, { package: pkg, autoremove: rem ? rem.checked : false }, function(xhr, res) {
+ dlg.removeChild(dlg.lastChild);
+
+ if (res.stdout)
+ dlg.appendChild(E('pre', [ res.stdout ]));
+
+ if (res.stderr) {
+ dlg.appendChild(E('h5', _('Errors')));
+ dlg.appendChild(E('pre', { 'class': 'errors' }, [ res.stderr ]));
+ }
+
+ if (res.code !== 0)
+ dlg.appendChild(E('p', _('The <em>opkg %h</em> command failed with code <code>%d</code>.').format(cmd, (res.code & 0xff) || -1)));
+
+ dlg.appendChild(E('div', { 'class': 'right' },
+ E('div', {
+ 'class': 'btn',
+ 'click': function() {
+ L.hideModal();
+ updateLists();
+ }
+ }, _('Dismiss'))));
+ });
+}
+
+function updateLists()
+{
+ cbi_update_table('#packages', [],
+ E('div', { 'class': 'spinning' }, _('Loading package information…')));
+
+ packages.available = { providers: {}, pkgs: {} };
+ packages.installed = { providers: {}, pkgs: {} };
+
+ L.get('admin/system/opkg/statvfs', null, function(xhr, stat) {
+ var pg = document.querySelector('.cbi-progressbar'),
+ total = stat.blocks || 0,
+ free = stat.bfree || 0;
+
+ pg.firstElementChild.style.width = Math.floor(total ? ((100 / total) * free) : 100) + '%';
+ pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, free * (stat.frsize || 0)));
+
+ L.get('admin/system/opkg/list/available', null, function(xhr) {
+ parseList(xhr.responseText, packages.available);
+ L.get('admin/system/opkg/list/installed', null, function(xhr) {
+ parseList(xhr.responseText, packages.installed);
+ display(document.querySelector('input[name="filter"]').value);
+ });
+ });
+ });
+}
+
+window.requestAnimationFrame(function() {
+ var filter = document.querySelector('input[name="filter"]'),
+ keyTimeout = null;
+
+ filter.value = '';
+ filter.addEventListener('keyup',
+ function(ev) {
+ if (keyTimeout !== null)
+ window.clearTimeout(keyTimeout);
+
+ keyTimeout = window.setTimeout(function() {
+ display(ev.target.value);
+ }, 250);
+ });
+
+ document.querySelector('#pager > .prev').addEventListener('click', handlePage);
+ document.querySelector('#pager > .next').addEventListener('click', handlePage);
+ document.querySelector('.cbi-tabmenu.mode').addEventListener('click', handleMode);
+
+ updateLists();
+});
}
</style>
-<script type="text/javascript">//<![CDATA[
- var packages = {
- available: { providers: {}, pkgs: {} },
- installed: { providers: {}, pkgs: {} }
- };
-
- var currentDisplayMode = 'available', currentDisplayRows = [];
-
- function parseList(s, dest)
- {
- var re = /([^\n]*)\n/g,
- pkg = null, key = null, val = null, m;
-
- while ((m = re.exec(s)) !== null) {
- if (m[1].match(/^\s(.*)$/)) {
- if (pkg !== null && key !== null && val !== null)
- val += '\n' + RegExp.$1.trim();
-
- continue;
- }
-
- if (key !== null && val !== null) {
- switch (key) {
- case 'package':
- pkg = { name: val };
- break;
-
- case 'depends':
- case 'provides':
- var list = val.split(/\s*,\s*/);
- if (list.length !== 1 || list[0].length > 0)
- pkg[key] = list;
- break;
-
- case 'installed-time':
- pkg.installtime = new Date(+val * 1000);
- break;
-
- case 'installed-size':
- pkg.installsize = +val;
- break;
-
- case 'status':
- var stat = val.split(/\s+/),
- mode = stat[1],
- installed = stat[2];
-
- switch (mode) {
- case 'user':
- case 'hold':
- pkg[mode] = true;
- break;
- }
-
- switch (installed) {
- case 'installed':
- pkg.installed = true;
- break;
- }
- break;
-
- case 'essential':
- if (val === 'yes')
- pkg.essential = true;
- break;
-
- case 'size':
- pkg.size = +val;
- break;
-
- case 'architecture':
- case 'auto-installed':
- case 'filename':
- case 'sha256sum':
- case 'section':
- break;
-
- default:
- pkg[key] = val;
- break;
- }
-
- key = val = null;
- }
-
- if (m[1].trim().match(/^([\w-]+)\s*:(.+)$/)) {
- key = RegExp.$1.toLowerCase();
- val = RegExp.$2.trim();
- }
- else {
- dest.pkgs[pkg.name] = pkg;
-
- var provides = dest.providers[pkg.name] ? [] : [ pkg.name ];
-
- if (pkg.provides)
- provides.push.apply(provides, pkg.provides);
-
- provides.forEach(function(p) {
- dest.providers[p] = dest.providers[p] || [];
- dest.providers[p].push(pkg);
- });
- }
- }
- }
-
- function display(pattern)
- {
- var src = packages[currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode],
- table = document.querySelector('#packages'),
- pager = document.querySelector('#pager');
-
- currentDisplayRows.length = 0;
-
- if (typeof(pattern) === 'string' && pattern.length > 0)
- pattern = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig');
-
- for (var name in src.pkgs) {
- var pkg = src.pkgs[name],
- desc = pkg.description || '',
- altsize = null;
-
- if (!pkg.size && packages.available.pkgs[name])
- altsize = packages.available.pkgs[name].size;
-
- if (!desc && packages.available.pkgs[name])
- desc = packages.available.pkgs[name].description || '';
-
- desc = desc.split(/\n/);
- desc = desc[0].trim() + (desc.length > 1 ? '…' : '');
-
- if ((pattern instanceof RegExp) &&
- !name.match(pattern) && !desc.match(pattern))
- continue;
-
- var btn, ver;
-
- if (currentDisplayMode === 'updates') {
- var avail = packages.available.pkgs[name];
- if (!avail || avail.version === pkg.version)
- continue;
-
- ver = '%s » %s'.format(
- truncateVersion(pkg.version || '-'),
- truncateVersion(avail.version || '-'));
-
- btn = E('button', {
- 'class': 'btn cbi-button-positive',
- 'data-package': name,
- 'click': handleInstall
- }, _('Upgrade…'));
- }
- else if (currentDisplayMode === 'installed') {
- ver = truncateVersion(pkg.version || '-');
- btn = E('button', {
- 'class': 'btn cbi-button-negative',
- 'data-package': name,
- 'click': handleRemove
- }, _('Remove'));
- }
- else {
- ver = truncateVersion(pkg.version || '-');
-
- if (!packages.installed.pkgs[name])
- btn = E('button', {
- 'class': 'btn cbi-button-action',
- 'data-package': name,
- 'click': handleInstall
- }, _('Install…'));
- else if (packages.installed.pkgs[name].version != pkg.version)
- btn = E('button', {
- 'class': 'btn cbi-button-positive',
- 'data-package': name,
- 'click': handleInstall
- }, _('Upgrade…'));
- else
- btn = E('button', {
- 'class': 'btn cbi-button-neutral',
- 'disabled': 'disabled'
- }, _('Installed'));
- }
-
- name = '%h'.format(name);
- desc = '%h'.format(desc || '-');
-
- if (pattern) {
- name = name.replace(pattern, '<ins>$&</ins>');
- desc = desc.replace(pattern, '<ins>$&</ins>');
- }
-
- currentDisplayRows.push([
- name,
- ver,
- pkg.size ? '%.1024mB'.format(pkg.size)
- : (altsize ? '~%.1024mB'.format(altsize) : '-'),
- desc,
- btn
- ]);
- }
-
- currentDisplayRows.sort(function(a, b) {
- if (a[0] < b[0])
- return -1;
- else if (a[0] > b[0])
- return 1;
- else
- return 0;
- });
-
- pager.parentNode.style.display = '';
- pager.setAttribute('data-offset', 100);
- handlePage({ target: pager.querySelector('.prev') });
- }
-
- function handlePage(ev)
- {
- var filter = document.querySelector('input[name="filter"]'),
- pager = ev.target.parentNode,
- offset = +pager.getAttribute('data-offset'),
- next = ev.target.classList.contains('next');
-
- if ((next && (offset + 100) >= currentDisplayRows.length) ||
- (!next && (offset < 100)))
- return;
-
- offset += next ? 100 : -100;
- pager.setAttribute('data-offset', offset);
- pager.querySelector('.text').firstChild.data = currentDisplayRows.length
- ? _('Displaying %d-%d of %d').format(1 + offset, Math.min(offset + 100, currentDisplayRows.length), currentDisplayRows.length)
- : _('No packages');
-
- if (offset < 100)
- pager.querySelector('.prev').setAttribute('disabled', 'disabled');
- else
- pager.querySelector('.prev').removeAttribute('disabled');
-
- if ((offset + 100) >= currentDisplayRows.length)
- pager.querySelector('.next').setAttribute('disabled', 'disabled');
- else
- pager.querySelector('.next').removeAttribute('disabled');
-
- var placeholder = _('No information available');
-
- if (filter.value)
- placeholder = [
- E('span', {}, _('No packages matching "<strong>%h</strong>".').format(filter.value)), ' (',
- E('a', { href: '#', onclick: 'handleReset(event)' }, _('Reset')), ')'
- ];
-
- cbi_update_table('#packages', currentDisplayRows.slice(offset, offset + 100),
- placeholder);
- }
-
- function handleMode(ev)
- {
- var tab = findParent(ev.target, 'li');
- if (tab.getAttribute('data-mode') === currentDisplayMode)
- return;
-
- tab.parentNode.querySelectorAll('li').forEach(function(li) {
- li.classList.remove('cbi-tab');
- li.classList.add('cbi-tab-disabled');
- });
-
- tab.classList.remove('cbi-tab-disabled');
- tab.classList.add('cbi-tab');
-
- currentDisplayMode = tab.getAttribute('data-mode');
-
- display(document.querySelector('input[name="filter"]').value);
-
- ev.target.blur();
- ev.preventDefault();
- }
-
- function orderOf(c)
- {
- if (c === '~')
- return -1;
- else if (c === '' || c >= '0' && c <= '9')
- return 0;
- else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'))
- return c.charCodeAt(0);
- else
- return c.charCodeAt(0) + 256;
- }
-
- function compareVersion(val, ref)
- {
- var vi = 0, ri = 0,
- isdigit = { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 };
-
- val = val || '';
- ref = ref || '';
-
- while (vi < val.length || ri < ref.length) {
- var first_diff = 0;
-
- while ((vi < val.length && !isdigit[val.charAt(vi)]) ||
- (ri < ref.length && !isdigit[ref.charAt(ri)])) {
- var vc = orderOf(val.charAt(vi)), rc = orderOf(ref.charAt(ri));
- if (vc !== rc)
- return vc - rc;
-
- vi++; ri++;
- }
-
- while (val.charAt(vi) === '0')
- vi++;
-
- while (ref.charAt(ri) === '0')
- ri++;
-
- while (isdigit[val.charAt(vi)] && isdigit[ref.charAt(ri)]) {
- first_diff = first_diff || (val.charCodeAt(vi) - ref.charCodeAt(ri));
- vi++; ri++;
- }
-
- if (isdigit[val.charAt(vi)])
- return 1;
- else if (isdigit[ref.charAt(ri)])
- return -1;
- else if (first_diff)
- return first_diff;
- }
-
- return 0;
- }
-
- function versionSatisfied(ver, ref, vop)
- {
- var r = compareVersion(ver, ref);
-
- switch (vop) {
- case '<':
- case '<=':
- return r <= 0;
-
- case '>':
- case '>=':
- return r >= 0;
-
- case '<<':
- return r < 0;
-
- case '>>':
- return r > 0;
-
- case '=':
- return r == 0;
- }
-
- return false;
- }
-
- function pkgStatus(pkg, vop, ver, info)
- {
- info.errors = info.errors || [];
- info.install = info.install || [];
-
- if (pkg.installed) {
- if (vop && !versionSatisfied(pkg.version, ver, vop)) {
- var repl = null;
-
- (packages.available.providers[pkg.name] || []).forEach(function(p) {
- if (!repl && versionSatisfied(p.version, ver, vop))
- repl = p;
- });
-
- if (repl) {
- info.install.push(repl);
- return E('span', {
- 'class': 'label',
- 'data-tooltip': _('Requires update to %h %h')
- .format(repl.name, repl.version)
- }, _('Needs upgrade'));
- }
-
- 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)));
-
- return E('span', {
- 'class': 'label warning',
- 'data-tooltip': _('Require version %h %h,\ninstalled %h')
- .format(vop, ver, pkg.version)
- }, _('Version incompatible'));
- }
-
- return E('span', { 'class': 'label notice' }, _('Installed'));
- }
- else if (!pkg.missing) {
- if (!vop || versionSatisfied(pkg.version, ver, vop)) {
- info.install.push(pkg);
- return E('span', { 'class': 'label' }, _('Not installed'));
- }
-
- info.errors.push(_('The repository version of package <em>%h</em> is not compatible, require %s but only %s is available.')
- .format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version)));
-
- return E('span', {
- 'class': 'label warning',
- 'data-tooltip': _('Require version %h %h,\ninstalled %h')
- .format(vop, ver, pkg.version)
- }, _('Version incompatible'));
- }
- else {
- info.errors.push(_('Required dependency package <em>%h</em> is not available in any repository.').format(pkg.name));
-
- return E('span', { 'class': 'label warning' }, _('Not available'));
- }
- }
-
- function renderDependencyItem(dep, info)
- {
- var li = E('li'),
- vop = dep.version ? dep.version[0] : null,
- ver = dep.version ? dep.version[1] : null,
- depends = [];
-
- for (var i = 0; dep.pkgs && i < dep.pkgs.length; i++) {
- var pkg = packages.installed.pkgs[dep.pkgs[i]] ||
- packages.available.pkgs[dep.pkgs[i]] ||
- { name: dep.name };
-
- if (i > 0)
- li.appendChild(document.createTextNode(' | '));
-
- var text = pkg.name;
-
- if (pkg.installsize)
- text += ' (%.1024mB)'.format(pkg.installsize);
- else if (pkg.size)
- text += ' (~%.1024mB)'.format(pkg.size);
-
- li.appendChild(E('span', { 'data-tooltip': pkg.description },
- [ text, ' ', pkgStatus(pkg, vop, ver, info) ]));
-
- (pkg.depends || []).forEach(function(d) {
- if (depends.indexOf(d) === -1)
- depends.push(d);
- });
- }
-
- if (!li.firstChild)
- li.appendChild(E('span', {},
- [ dep.name, ' ',
- pkgStatus({ name: dep.name, missing: true }, vop, ver, info) ]));
-
- var subdeps = renderDependencies(depends, info);
- if (subdeps)
- li.appendChild(subdeps);
-
- return li;
- }
-
- function renderDependencies(depends, info)
- {
- var deps = depends || [],
- items = [];
-
- info.seen = info.seen || [];
-
- for (var i = 0; i < deps.length; i++) {
- if (deps[i] === 'libc')
- continue;
-
- if (deps[i].match(/^(.+)\s+\((<=|<|>|>=|=|<<|>>)(.+)\)$/)) {
- dep = RegExp.$1.trim();
- vop = RegExp.$2.trim();
- ver = RegExp.$3.trim();
- }
- else {
- dep = deps[i].trim();
- vop = ver = null;
- }
-
- if (info.seen[dep])
- continue;
-
- var pkgs = [];
-
- (packages.installed.providers[dep] || []).forEach(function(p) {
- if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
- });
-
- (packages.available.providers[dep] || []).forEach(function(p) {
- if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name);
- });
-
- info.seen[dep] = {
- name: dep,
- pkgs: pkgs,
- version: [vop, ver]
- };
-
- items.push(renderDependencyItem(info.seen[dep], info));
- }
-
- if (items.length)
- return E('ul', { 'class': 'deps' }, items);
-
- return null;
- }
-
- function truncateVersion(v, op)
- {
- v = v.replace(/\b(([a-f0-9]{8})[a-f0-9]{24,32})\b/,
- '<span data-tooltip="$1">$2…</span>');
-
- if (!op || op === '=')
- return v;
-
- return '%h %h'.format(op, v);
- }
-
- function handleReset(ev)
- {
- var filter = document.querySelector('input[name="filter"]');
-
- filter.value = '';
- display();
- }
-
- function handleInstall(ev)
- {
- var name = ev.target.getAttribute('data-package'),
- pkg = packages.available.pkgs[name],
- depcache = {},
- size;
-
- if (pkg.installsize)
- size = _('~%.1024mB installed').format(pkg.installsize);
- else if (pkg.size)
- size = _('~%.1024mB compressed').format(pkg.size);
- else
- size = _('unknown');
-
- var deps = renderDependencies(pkg.depends, depcache),
- tree = null, errs = null, inst = null, desc = null;
-
- if (depcache.errors && depcache.errors.length) {
- errs = E('ul', { 'class': 'errors' });
- depcache.errors.forEach(function(err) {
- errs.appendChild(E('li', {}, err));
- });
- }
-
- var totalsize = pkg.installsize || pkg.size || 0,
- totalpkgs = 1;
-
- if (depcache.install && depcache.install.length)
- depcache.install.forEach(function(ipkg) {
- totalsize += ipkg.installsize || ipkg.size || 0;
- totalpkgs++;
- });
-
- inst = E('p', {},
- _('Require approx. %.1024mB size for %d package(s) to install.')
- .format(totalsize, totalpkgs));
-
- if (deps) {
- tree = E('li', '<strong>%s:</strong>'.format(_('Dependencies')));
- tree.appendChild(deps);
- }
-
- if (pkg.description) {
- desc = E('div', {}, [
- E('h5', {}, _('Description')),
- E('p', {}, pkg.description)
- ]);
- }
-
- showModal(_('Details for package <em>%h</em>').format(pkg.name), [
- E('ul', {}, [
- E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
- E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)),
- tree || '',
- ]),
- desc || '',
- errs || inst || '',
- E('div', { 'class': 'right' }, [
- E('button', {
- 'class': 'btn',
- 'click': hideModal
- }, _('Cancel')),
- ' ',
- E('button', {
- 'data-command': 'install',
- 'data-package': name,
- 'class': 'btn cbi-button-action',
- 'click': handleOpkg
- }, _('Install'))
- ])
- ]);
- }
-
- function handleManualInstall(ev)
- {
- var name_or_url = document.querySelector('input[name="install"]').value,
- install = E('button', {
- 'class': 'btn cbi-button-action',
- 'data-command': 'install',
- 'data-package': name_or_url,
- 'click': function(ev) {
- document.querySelector('input[name="install"]').value = '';
- handleOpkg(ev);
- }
- }, _('Install')), warning;
-
- if (!name_or_url.length) {
- return;
- }
- else if (name_or_url.indexOf('/') !== -1) {
- warning = E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(name_or_url));
- }
- else if (!packages.available.providers[name_or_url]) {
- warning = E('p', {}, _('The package <em>%h</em> is not available in any configured repository.').format(name_or_url));
- install = '';
- }
- else {
- warning = E('p', {}, _('Really attempt to install <em>%h</em>?').format(name_or_url));
- }
-
- showModal(_('Manually install package'), [
- warning,
- E('div', { 'class': 'right' }, [
- E('button', {
- 'click': hideModal,
- 'class': 'btn cbi-button-neutral'
- }, _('Cancel')),
- ' ', install
- ])
- ]);
- }
-
- function handleConfig(ev)
- {
- showModal(_('OPKG Configuration'), [
- E('p', { 'class': 'spinning' }, _('Loading configuration data…'))
- ]);
-
- XHR.get('<%=url("admin/system/opkg/config")%>', null, function(xhr, conf) {
- var body = [
- 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>.'))
- ];
-
- Object.keys(conf).sort().forEach(function(file) {
- body.push(E('h5', {}, '%h'.format(file)));
- body.push(E('textarea', {
- 'name': file,
- 'rows': Math.max(Math.min(conf[file].match(/\n/g).length, 10), 3)
- }, '%h'.format(conf[file])));
- });
-
- body.push(E('div', { 'class': 'right' }, [
- E('button', {
- 'class': 'btn cbi-button-neutral',
- 'click': hideModal
- }, _('Cancel')),
- ' ',
- E('button', {
- 'class': 'btn cbi-button-positive',
- 'click': function(ev) {
- var data = {};
- findParent(ev.target, '.modal').querySelectorAll('textarea[name]')
- .forEach(function(textarea) {
- data[textarea.getAttribute('name')] = textarea.value
- });
-
- showModal(_('OPKG Configuration'), [
- E('p', { 'class': 'spinning' }, _('Saving configuration data…'))
- ]);
-
- (new XHR()).post('<%=url("admin/system/opkg/config")%>',
- { token: '<%=token%>', data: JSON.stringify(data) }, hideModal);
- }
- }, _('Save')),
- ]));
-
- showModal(_('OPKG Configuration'), body);
- });
- }
-
- function handleRemove(ev)
- {
- var name = ev.target.getAttribute('data-package'),
- pkg = packages.installed.pkgs[name],
- avail = packages.available.pkgs[name] || {},
- size, desc;
-
- if (avail.installsize)
- size = _('~%.1024mB installed').format(avail.installsize);
- else if (avail.size)
- size = _('~%.1024mB compressed').format(avail.size);
- else
- size = _('unknown');
-
- if (avail.description) {
- desc = E('div', {}, [
- E('h5', {}, _('Description')),
- E('p', {}, avail.description)
- ]);
- }
-
- showModal(_('Remove package <em>%h</em>').format(pkg.name), [
- E('ul', {}, [
- E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
- E('li', '<strong>%s:</strong> %h'.format(_('Size'), size))
- ]),
- desc || '',
- E('div', { 'style': 'display:flex; justify-content:space-between; flex-wrap:wrap' }, [
- E('label', {}, [
- E('input', { type: 'checkbox', checked: 'checked', name: 'autoremove' }),
- _('Automatically remove unused dependencies')
- ]),
- E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [
- E('button', {
- 'class': 'btn',
- 'click': hideModal
- }, _('Cancel')),
- ' ',
- E('button', {
- 'data-command': 'remove',
- 'data-package': name,
- 'class': 'btn cbi-button-negative',
- 'click': handleOpkg
- }, _('Remove'))
- ])
- ])
- ]);
- }
-
- function handleOpkg(ev)
- {
- var cmd = ev.target.getAttribute('data-command'),
- pkg = ev.target.getAttribute('data-package'),
- rem = document.querySelector('input[name="autoremove"]'),
- url = '<%=url("admin/system/opkg/exec")%>/' + encodeURIComponent(cmd);
-
- var dlg = showModal(_('Executing package manager'), [
- E('p', { 'class': 'spinning' },
- _('Waiting for the <em>opkg %h</em> command to complete…').format(cmd))
- ]);
-
- (new XHR()).post(url, {
- token: '<%=token%>',
- package: pkg,
- autoremove: rem ? rem.checked : false
- }, function(xhr, res) {
- dlg.removeChild(dlg.lastChild);
-
- if (res.stdout)
- dlg.appendChild(E('pre', [ res.stdout ]));
-
- if (res.stderr) {
- dlg.appendChild(E('h5', _('Errors')));
- dlg.appendChild(E('pre', { 'class': 'errors' }, [ res.stderr ]));
- }
-
- if (res.code !== 0)
- dlg.appendChild(E('p', _('The <em>opkg %h</em> command failed with code <code>%d</code>.').format(cmd, (res.code & 0xff) || -1)));
-
- dlg.appendChild(E('div', { 'class': 'right' },
- E('button', {
- 'class': 'btn',
- 'click': function() {
- hideModal();
- updateLists();
- }
- }, _('Dismiss'))));
- });
- }
-
- function updateLists()
- {
- cbi_update_table('#packages', [],
- E('div', { 'class': 'spinning' }, _('Loading package information…')));
-
- packages.available = { providers: {}, pkgs: {} };
- packages.installed = { providers: {}, pkgs: {} };
-
- XHR.get('<%=url("admin/system/opkg/statvfs")%>', null, function(xhr, stat) {
- var pg = document.querySelector('.cbi-progressbar'),
- total = stat.blocks || 0,
- free = stat.bfree || 0;
-
- pg.firstElementChild.style.width = Math.floor(total ? ((100 / total) * free) : 100) + '%';
- pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, free * (stat.frsize || 0)));
-
- XHR.get('<%=url("admin/system/opkg/list/available")%>', null, function(xhr) {
- parseList(xhr.responseText, packages.available);
- XHR.get('<%=url("admin/system/opkg/list/installed")%>', null, function(xhr) {
- parseList(xhr.responseText, packages.installed);
- display(document.querySelector('input[name="filter"]').value);
- });
- });
- });
- }
-
- window.requestAnimationFrame(function() {
- var filter = document.querySelector('input[name="filter"]'),
- keyTimeout = null;
-
- filter.value = '';
- filter.addEventListener('keyup',
- function(ev) {
- if (keyTimeout !== null)
- window.clearTimeout(keyTimeout);
-
- keyTimeout = window.setTimeout(function() {
- display(ev.target.value);
- }, 250);
- });
-
- document.querySelector('#pager > .prev').addEventListener('click', handlePage);
- document.querySelector('#pager > .next').addEventListener('click', handlePage);
- document.querySelector('.cbi-tabmenu.mode').addEventListener('click', handleMode);
-
- updateLists();
- });
-//]]></script>
-
<h2><%:Software%></h2>
<div class="controls">
</div>
</div>
+<script type="text/javascript" src="<%=resource%>/view/opkg.js"></script>
+
<%+footer%>