diff options
-rw-r--r-- | applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js | 812 | ||||
-rw-r--r-- | applications/luci-app-opkg/luasrc/view/opkg.htm | 822 |
2 files changed, 814 insertions, 820 deletions
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 index 000000000..c2fe2d9fa --- /dev/null +++ b/applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js @@ -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(); +}); diff --git a/applications/luci-app-opkg/luasrc/view/opkg.htm b/applications/luci-app-opkg/luasrc/view/opkg.htm index e610ebad3..76b3f99ae 100644 --- a/applications/luci-app-opkg/luasrc/view/opkg.htm +++ b/applications/luci-app-opkg/luasrc/view/opkg.htm @@ -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%> |