luci-app-opkg: move JS code into external file
authorJo-Philipp Wich <jo@mein.io>
Wed, 21 Nov 2018 19:01:54 +0000 (20:01 +0100)
committerJo-Philipp Wich <jo@mein.io>
Thu, 22 Nov 2018 11:49:14 +0000 (12:49 +0100)
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js [new file with mode: 0644]
applications/luci-app-opkg/luasrc/view/opkg.htm

diff --git a/applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js b/applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js
new file mode 100644 (file)
index 0000000..c2fe2d9
--- /dev/null
@@ -0,0 +1,812 @@
+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();
+});
index e610ebad3472df0c4f4a69be70a79f6908db1eaf..76b3f99ae77ffb3df097d889b7da09bc0e7946ae 100644 (file)
        }
 </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%>