From b8e341c20ef88136180c95e3b2db9adf299eaf62 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Wed, 21 Nov 2018 20:01:54 +0100 Subject: [PATCH] luci-app-opkg: move JS code into external file Signed-off-by: Jo-Philipp Wich --- .../htdocs/luci-static/resources/view/opkg.js | 812 +++++++++++++++++ .../luci-app-opkg/luasrc/view/opkg.htm | 822 +----------------- 2 files changed, 814 insertions(+), 820 deletions(-) create mode 100644 applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js 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 0000000000..c2fe2d9fa9 --- /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, '$&'); + desc = desc.replace(pattern, '$&'); + } + + 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 "%h".').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 %h 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 %h 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 %h 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/, + '$2…'); + + 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', '%s:'.format(_('Dependencies'))); + tree.appendChild(deps); + } + + if (pkg.description) { + desc = E('div', {}, [ + E('h5', {}, _('Description')), + E('p', {}, pkg.description) + ]); + } + + L.showModal(_('Details for package %h').format(pkg.name), [ + E('ul', {}, [ + E('li', '%s: %h'.format(_('Version'), pkg.version)), + E('li', '%s: %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 %h?').format(name_or_url)); + } + else if (!packages.available.providers[name_or_url]) { + warning = E('p', {}, _('The package %h is not available in any configured repository.').format(name_or_url)); + install = ''; + } + else { + warning = E('p', {}, _('Really attempt to install %h?').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 opkg. Use opkg.conf for global settings and customfeeds.conf for custom repository entries. The configuration in the other files may be changed but is usually not preserved by sysupgrade.')) + ]; + + 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 %h').format(pkg.name), [ + E('ul', {}, [ + E('li', '%s: %h'.format(_('Version'), pkg.version)), + E('li', '%s: %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 opkg %h 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 opkg %h command failed with code %d.').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 e610ebad34..76b3f99ae7 100644 --- a/applications/luci-app-opkg/luasrc/view/opkg.htm +++ b/applications/luci-app-opkg/luasrc/view/opkg.htm @@ -81,826 +81,6 @@ } - -

<%:Software%>

@@ -955,4 +135,6 @@
+ + <%+footer%> -- 2.30.2