luci-app-opkg: move JS code into external file
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
This commit is contained in:
parent
05860e31f2
commit
b8e341c20e
2 changed files with 814 additions and 820 deletions
|
@ -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();
|
||||
});
|
|
@ -81,826 +81,6 @@
|
|||
}
|
||||
</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">
|
||||
|
@ -955,4 +135,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="<%=resource%>/view/opkg.js"></script>
|
||||
|
||||
<%+footer%>
|
||||
|
|
Loading…
Reference in a new issue