diff options
author | Christian Marangi <ansuelsmth@gmail.com> | 2024-10-23 01:29:10 +0200 |
---|---|---|
committer | Paul Donald <newtwen+github@gmail.com> | 2024-10-24 00:05:19 +0200 |
commit | bcd13b918e2f30b8d19027a06e3d773a1b0ec4c0 (patch) | |
tree | 6c2b5a5971bbb37255393efeb8ebc4e9d60e8fe1 /applications/luci-app-package-manager/htdocs | |
parent | 591911d1723037ecf8cd0f5e5961bc1d9fc7a1df (diff) |
luci-app-package-manager: rename from luci-app-opkg and add APK support
Rename luci-app-opkg to luci-app-package-manager and add APK support to
it.
The idea is to adapt APK to mimic OPKG output to require minimal changes
to the luci app.
Signed-off-by: Christian Marangi <ansuelsmth@gmail.com>
Diffstat (limited to 'applications/luci-app-package-manager/htdocs')
-rw-r--r-- | applications/luci-app-package-manager/htdocs/luci-static/resources/view/package-manager.js | 1276 |
1 files changed, 1276 insertions, 0 deletions
diff --git a/applications/luci-app-package-manager/htdocs/luci-static/resources/view/package-manager.js b/applications/luci-app-package-manager/htdocs/luci-static/resources/view/package-manager.js new file mode 100644 index 0000000000..74614636ad --- /dev/null +++ b/applications/luci-app-package-manager/htdocs/luci-static/resources/view/package-manager.js @@ -0,0 +1,1276 @@ +'use strict'; +'require view'; +'require fs'; +'require ui'; +'require rpc'; + +var css = ' \ + .controls { \ + display: flex; \ + margin: .5em 0 1em 0; \ + flex-wrap: wrap; \ + justify-content: space-around; \ + } \ + \ + .controls > * { \ + padding: .25em; \ + white-space: nowrap; \ + flex: 1 1 33%; \ + box-sizing: border-box; \ + display: flex; \ + flex-wrap: wrap; \ + } \ + \ + .controls > *:first-child, \ + .controls > * > label { \ + flex-basis: 100%; \ + min-width: 250px; \ + } \ + \ + .controls > *:nth-child(2), \ + .controls > *:nth-child(3) { \ + flex-basis: 20%; \ + } \ + \ + .controls > * > .btn { \ + flex-basis: 20px; \ + text-align: center; \ + } \ + \ + .controls > * > * { \ + flex-grow: 1; \ + align-self: center; \ + } \ + \ + .controls > div > input { \ + width: auto; \ + } \ + \ + .td.version, \ + .td.size { \ + white-space: nowrap; \ + } \ + \ + ul.deps, ul.deps ul, ul.errors { \ + margin-left: 1em; \ + } \ + \ + ul.deps li, ul.errors li { \ + list-style: none; \ + } \ + \ + ul.deps li:before { \ + content: "↳"; \ + display: inline-block; \ + width: 1em; \ + margin-left: -1em; \ + } \ + \ + ul.deps li > span { \ + white-space: nowrap; \ + } \ + \ + ul.errors li { \ + color: #c44; \ + font-size: 90%; \ + font-weight: bold; \ + padding-left: 1.5em; \ + } \ + \ + ul.errors li:before { \ + content: "⚠"; \ + display: inline-block; \ + width: 1.5em; \ + margin-left: -1.5em; \ + } \ +'; + +var isReadonlyView = !L.hasViewPermission() || null; + +var callMountPoints = rpc.declare({ + object: 'luci', + method: 'getMountPoints', + expect: { result: [] } +}); + +var packages = { + available: { providers: {}, pkgs: {} }, + installed: { providers: {}, pkgs: {} } +}; + +var languages = ['en']; + +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 if (pkg) { + 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'), + pagers = document.querySelectorAll('.controls > .pager'), + i18n_filter = null; + + currentDisplayRows.length = 0; + + if (typeof(pattern) === 'string' && pattern.length > 0) + pattern = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig'); + + switch (document.querySelector('input[name="filter_i18n"]:checked').value) { + case 'all': + i18n_filter = /^luci-i18n-/; + break; + + case 'lang': + i18n_filter = new RegExp('^luci-i18n-(base-.+|.+-(' + languages.join('|') + '))$'); + break; + } + + 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; + + if (name.indexOf('luci-i18n-') === 0 && (!(i18n_filter instanceof RegExp) || !name.match(i18n_filter))) + continue; + + var btn, ver; + + if (currentDisplayMode === 'updates') { + var avail = packages.available.pkgs[name], + inst = packages.installed.pkgs[name]; + + if (!inst || !inst.installed) + continue; + + if (!avail || compareVersion(avail.version, pkg.version) <= 0) + 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') { + if (!pkg.installed) + continue; + + ver = truncateVersion(pkg.version || '-'); + btn = E('div', { + 'class': 'btn cbi-button-negative', + 'data-package': name, + 'click': handleRemove + }, _('Remove…')); + } + else { + var inst = packages.installed.pkgs[name]; + + ver = truncateVersion(pkg.version || '-'); + + if (!inst || !inst.installed) + btn = E('div', { + 'class': 'btn cbi-button-action', + 'data-package': name, + 'click': handleInstall + }, _('Install…')); + else if (inst.installed && inst.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 || altsize || 0, + 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; + }); + + for (var i = 0; i < pagers.length; i++) { + pagers[i].parentNode.style.display = ''; + pagers[i].setAttribute('data-offset', 100); + } + + handlePage({ target: pagers[0].querySelector('.prev') }); +} + +function handlePage(ev) +{ + var filter = document.querySelector('input[name="filter"]'), + offset = +ev.target.parentNode.getAttribute('data-offset'), + next = ev.target.classList.contains('next'), + pagers = document.querySelectorAll('.controls > .pager'); + + if ((next && (offset + 100) >= currentDisplayRows.length) || + (!next && (offset < 100))) + return; + + offset += next ? 100 : -100; + + for (var i = 0; i < pagers.length; i++) { + pagers[i].setAttribute('data-offset', offset); + pagers[i].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) + pagers[i].querySelector('.prev').setAttribute('disabled', 'disabled'); + else + pagers[i].querySelector('.prev').removeAttribute('disabled'); + + if ((offset + 100) >= currentDisplayRows.length) + pagers[i].querySelector('.next').setAttribute('disabled', 'disabled'); + else + pagers[i].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: '#', click: handleReset }, _('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 handleI18nFilter(ev) +{ + display(document.querySelector('input[name="filter"]').value); +} + +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 || ''; + + if (val === ref) + return 0; + + 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, flat) +{ + 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) ])); + + if (!flat) { + var subdeps = renderDependencies(depends, info); + if (subdeps) + li.appendChild(subdeps); + } + + return li; +} + +function renderDependencies(depends, info, flat) +{ + var deps = depends || [], + items = []; + + info.seen = info.seen || []; + + for (var i = 0; i < deps.length; i++) { + var dep, vop, ver; + + 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, flat)); + } + + 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, + suggestsize = 0; + + if (depcache.install && depcache.install.length) + depcache.install.forEach(function(ipkg) { + totalsize += ipkg.installsize || ipkg.size || 0; + totalpkgs++; + }); + + var luci_basename = pkg.name.match(/^luci-([^-]+)-(.+)$/), + i18n_packages = [], + i18n_tree; + + if (luci_basename && (luci_basename[1] != 'i18n' || luci_basename[2].indexOf('base-') === 0)) { + var i18n_filter; + + if (luci_basename[1] == 'i18n') { + var basenames = []; + + for (var pkgname in packages.installed.pkgs) { + var m = pkgname.match(/^luci-([^-]+)-(.+)$/); + + if (m && m[1] != 'i18n') + basenames.push(m[2]); + } + + if (basenames.length) + i18n_filter = new RegExp('^luci-i18n-(' + basenames.join('|') + ')-' + pkg.name.replace(/^luci-i18n-base-/, '') + '$'); + } + else { + i18n_filter = new RegExp('^luci-i18n-' + luci_basename[2] + '-(' + languages.join('|') + ')$'); + } + + if (i18n_filter) { + for (var pkgname in packages.available.pkgs) + if (pkgname != pkg.name && pkgname.match(i18n_filter)) + i18n_packages.push(pkgname); + + var i18ncache = {}; + + i18n_tree = renderDependencies(i18n_packages, i18ncache, true); + + if (i18ncache.install && i18ncache.install.length) { + i18ncache.install.forEach(function(ipkg) { + suggestsize += ipkg.installsize || ipkg.size || 0; + }); + } + } + } + + inst = E('p', [ + _('Require approx. %1024mB size for %d package(s) to install.') + .format(totalsize, totalpkgs), + ' ', + suggestsize ? _('Suggested translations require approx. %1024mB additional space.').format(suggestsize) : '' + ]); + + 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) + ]); + } + + ui.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 || '', + i18n_packages.length ? E('li', [ + E('strong', [_('Suggested translations'), ':']), + i18n_tree + ]) : '' + ]), + desc || '', + errs || inst || '', + E('div', [ + E('hr'), + i18n_packages.length ? E('p', [ + E('label', { 'class': 'cbi-checkbox' }, [ + E('input', { + 'id': 'i18ninstall-cb', + 'type': 'checkbox', + 'name': 'i18ninstall', + 'data-packages': i18n_packages.join(' '), + 'disabled': isReadonlyView, + 'checked': true + }), ' ', + E('label', { 'for': 'i18ninstall-cb' }), ' ', + _('Install suggested translation packages as well') + ]) + ]) : '', + E('p', [ + E('label', { 'class': 'cbi-checkbox' }, [ + E('input', { + 'id': 'overwrite-cb', + 'type': 'checkbox', + 'name': 'overwrite', + 'disabled': isReadonlyView + }), ' ', + E('label', { 'for': 'overwrite-cb' }), ' ', + _('Allow overwriting conflicting package files') + ]) + ]) + ]), + E('div', { 'class': 'right' }, [ + E('div', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('div', { + 'data-command': 'install', + 'data-package': name, + 'class': 'btn cbi-button-action', + 'click': handlePkg, + 'disabled': isReadonlyView + }, _('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 = ''; + handlePkg(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)); + } + + ui.showModal(_('Manually install package'), [ + warning, + E('div', { 'class': 'right' }, [ + E('div', { + 'click': ui.hideModal, + 'class': 'btn cbi-button-neutral' + }, _('Cancel')), + ' ', install + ]) + ]); +} + +function handleConfig(ev) +{ + var conf = {}; + var base_dir = L.hasSystemFeature('apk') ? '/etc/apk' : '/etc/opkg'; + + ui.showModal(_('%s Configuration').format(L.hasSystemFeature('apk') ? 'APK' : 'OPKG'), [ + E('p', { 'class': 'spinning' }, _('Loading configuration data…')) + ]); + + fs.list(base_dir).then(function(partials) { + var files = []; + + if (!L.hasSystemFeature('apk')) + files.push(base_dir + '.conf') + + for (var i = 0; i < partials.length; i++) { + if (partials[i].type == 'file') { + if (L.hasSystemFeature('apk')) { + if (partials[i].name == 'repositories') + files.push(base_dir + '/' + partials[i].name); + } else if (partials[i].name.match(/\.conf$/)) { + files.push(base_dir + '/' + partials[i].name); + } + } + } + + return Promise.all(files.map(function(file) { + return fs.read(file) + .then(L.bind(function(conf, file, res) { conf[file] = res }, this, conf, file)) + .catch(function(err) { + ui.addNotification(null, E('p', {}, [ _('Unable to read %s: %s').format(file, err) ])); + ui.hideModal(); + throw err; + }); + })); + }).then(function() { + var opkg_text = _('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>.') + var apk_text = _('Below is a listing of the various configuration files used by <em>apk</em>. The configuration in the other files may be changed but is usually not preserved by <em>sysupgrade</em>.') + var body = [ + E('p', {}, L.hasSystemFeature('apk') ? apk_text : opkg_text) + ]; + + 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(L.toArray(conf[file].match(/\n/g)).length, 10), 3) + }, '%h'.format(conf[file]))); + }); + + body.push(E('div', { 'class': 'button-row' }, [ + E('div', { + 'class': 'btn cbi-button-neutral', + 'click': ui.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 + }); + + ui.showModal(_('%s Configuration').format(L.hasSystemFeature('apk') ? 'APK' : 'OPKG'), [ + E('p', { 'class': 'spinning' }, _('Saving configuration data…')) + ]); + + Promise.all(Object.keys(data).map(function(file) { + return fs.write(file, data[file]).catch(function(err) { + ui.addNotification(null, E('p', {}, [ _('Unable to save %s: %s').format(file, err) ])); + }); + })).then(ui.hideModal); + }, + 'disabled': isReadonlyView + }, _('Save')), + ])); + + ui.showModal(_('%s Configuration').format(L.hasSystemFeature('apk') ? 'APK' : 'OPKG'), 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) + ]); + } + + ui.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', { 'class': 'cbi-checkbox', 'style': 'float:left' }, [ + E('input', { 'id': 'autoremove-cb', 'type': 'checkbox', 'checked': 'checked', 'name': 'autoremove', 'disabled': isReadonlyView || L.hasSystemFeature('apk') }), ' ', + E('label', { 'for': 'autoremove-cb' }), ' ', + _('Automatically remove unused dependencies') + ]), + E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [ + E('div', { + 'class': 'btn', + 'click': ui.hideModal + }, _('Cancel')), + ' ', + E('div', { + 'data-command': 'remove', + 'data-package': name, + 'class': 'btn cbi-button-negative', + 'click': handlePkg, + 'disabled': isReadonlyView + }, _('Remove')) + ]) + ]) + ]); +} + +function handlePkg(ev) +{ + return new Promise(function(resolveFn, rejectFn) { + var cmd = ev.target.getAttribute('data-command'), + pkg = ev.target.getAttribute('data-package'), + rem = document.querySelector('input[name="autoremove"]'), + owr = document.querySelector('input[name="overwrite"]'), + i18n = document.querySelector('input[name="i18ninstall"]'); + + var dlg = ui.showModal(_('Executing package manager'), [ + E('p', { 'class': 'spinning' }, + _('Waiting for the <em>%s %h</em> command to complete…').format(L.hasSystemFeature('apk') ? 'apk' : 'opkg', cmd)) + ]); + + var argv = [ cmd ]; + + if (cmd == 'remove') + argv.push('--force-removal-of-dependent-packages') + + if (rem && rem.checked) + argv.push('--autoremove'); + + if (owr && owr.checked) + argv.push('--force-overwrite'); + + if (i18n && i18n.checked) + argv.push.apply(argv, i18n.getAttribute('data-packages').split(' ')); + + if (pkg != null) + argv.push(pkg); + + fs.exec_direct('/usr/libexec/package-manager-call', argv, 'json').then(function(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>%s %h</em> command failed with code <code>%d</code>.').format(L.hasSystemFeature('apk') ? 'apk' : 'opkg', cmd, (res.code & 0xff) || -1))); + + dlg.appendChild(E('div', { 'class': 'button-row' }, + E('div', { + 'class': 'btn', + 'click': L.bind(function(res) { + if (ui.menu && ui.menu.flushCache) + ui.menu.flushCache(); + + ui.hideModal(); + updateLists(); + + if (res.code !== 0) + rejectFn(new Error(res.stderr || '%s error %d'.format(L.hasSystemFeature('apk') ? 'apk' : 'opkg', res.code))); + else + resolveFn(res); + }, this, res) + }, _('Dismiss')))); + }).catch(function(err) { + ui.addNotification(null, E('p', _('Unable to execute <em>%s %s</em> command: %s').format(L.hasSystemFeature('apk') ? 'apk' : 'opkg', cmd, err))); + ui.hideModal(); + }); + }); +} + +function handleUpload(ev) +{ + var path = '/tmp/upload.%s'.format(L.hasSystemFeature('apk') ? 'apk' : 'ipk'); + return ui.uploadFile(path).then(L.bind(function(btn, res) { + ui.showModal(_('Manually install package'), [ + E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(res.name)), + E('ul', {}, [ + res.size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res.size)) : '', + res.checksum ? E('li', {}, '%s: %s'.format(_('MD5'), res.checksum)) : '', + res.sha256sum ? E('li', {}, '%s: %s'.format(_('SHA256'), res.sha256sum)) : '' + ]), + E('div', { 'class': 'right' }, [ + E('div', { + 'click': function(ev) { + ui.hideModal(); + fs.remove(path); + }, + 'class': 'btn cbi-button-neutral' + }, _('Cancel')), ' ', + E('div', { + 'class': 'btn cbi-button-action', + 'data-command': 'install', + 'data-package': path, + 'click': function(ev) { + handlePkg(ev).finally(function() { + fs.remove(path) + }); + } + }, _('Install')) + ]) + ]); + }, this, ev.target)); +} + +function downloadLists() +{ + return Promise.all([ + callMountPoints(), + fs.exec_direct('/usr/libexec/package-manager-call', [ 'list-available' ]), + fs.exec_direct('/usr/libexec/package-manager-call', [ 'list-installed' ]) + ]); +} + +function updateLists(data) +{ + cbi_update_table('#packages', [], + E('div', { 'class': 'spinning' }, _('Loading package information…'))); + + packages.available = { providers: {}, pkgs: {} }; + packages.installed = { providers: {}, pkgs: {} }; + + return (data ? Promise.resolve(data) : downloadLists()).then(function(data) { + var pg = document.querySelector('.cbi-progressbar'), + mount = L.toArray(data[0].filter(function(m) { return m.mount == '/' || m.mount == '/overlay' })) + .sort(function(a, b) { return a.mount > b.mount })[0] || { size: 0, free: 0 }; + + pg.firstElementChild.style.width = Math.floor(mount.size ? (100 / mount.size) * (mount.size - mount.free) : 100) + '%'; + pg.setAttribute('title', _('%s used (%1024mB used of %1024mB, %1024mB free)').format(pg.firstElementChild.style.width, mount.size - mount.free, mount.size, mount.free)); + + parseList(data[1], packages.available); + parseList(data[2], packages.installed); + + for (var pkgname in packages.installed.pkgs) + if (pkgname.indexOf('luci-i18n-base-') === 0) + languages.push(pkgname.substring(15)); + + display(document.querySelector('input[name="filter"]').value); + }); +} + +var inputTimeout = null; + +function handleInput(ev) { + if (inputTimeout !== null) + window.clearTimeout(inputTimeout); + + inputTimeout = window.setTimeout(function() { + display(ev.target.value); + }, 250); +} + +return view.extend({ + load: function() { + return downloadLists(); + }, + + render: function(listData) { + var query = decodeURIComponent(L.toArray(location.search.match(/\bquery=([^=]+)\b/))[1] || ''); + + var view = E([], [ + E('style', { 'type': 'text/css' }, [ css ]), + + E('h2', {}, _('Software')), + + E('div', { 'class': 'cbi-map-descr' }, [ + E('span', _('Install additional software and upgrade existing packages with %s.').format(L.hasSystemFeature('apk') ? 'apk' : 'opkg')), + E('br'), + E('span', _('<strong>Warning!</strong> Package operations can <a %s>break your system</a>.').format( + 'href="https://openwrt.org/meta/infobox/upgrade_packages_warning" target="_blank" rel="noreferrer"' + )) + ]), + + E('div', { 'class': 'controls' }, [ + E('div', {}, [ + E('label', {}, _('Disk space') + ':'), + E('div', { 'class': 'cbi-progressbar', 'title': _('unknown') }, E('div', {}, [ '\u00a0' ])) + ]), + + E('div', {}, [ + E('label', {}, _('Filter') + ':'), + E('span', { 'class': 'control-group' }, [ + E('input', { 'type': 'text', 'name': 'filter', 'placeholder': _('Type to filter…'), 'value': query, 'input': handleInput }), + E('button', { 'class': 'btn cbi-button', 'click': handleReset }, [ _('Clear') ]) + ]) + ]), + + E('div', {}, [ + E('label', {}, _('Download and install package') + ':'), + E('span', { 'class': 'control-group' }, [ + E('input', { 'type': 'text', 'name': 'install', 'placeholder': _('Package name or URL…'), 'keydown': function(ev) { if (ev.keyCode === 13) handleManualInstall(ev) }, 'disabled': isReadonlyView }), + E('button', { 'class': 'btn cbi-button cbi-button-action', 'click': handleManualInstall, 'disabled': isReadonlyView }, [ _('OK') ]) + ]) + ]), + + E('div', {}, [ + E('label', {}, _('Actions') + ':'), ' ', + E('span', { 'class': 'control-group' }, [ + E('button', { 'class': 'btn cbi-button-positive', 'data-command': 'update', 'click': handlePkg, 'disabled': isReadonlyView }, [ _('Update lists…') ]), ' ', + E('button', { 'class': 'btn cbi-button-action', 'click': handleUpload, 'disabled': isReadonlyView }, [ _('Upload Package…') ]), ' ', + E('button', { 'class': 'btn cbi-button-neutral', 'click': handleConfig }, [ _('Configure %s').format(L.hasSystemFeature('apk') ? 'apk' : 'opkg') ]) + ]) + ]), + + E('div', {}, [ + E('label', {}, _('Display LuCI translation packages') + ':'), ' ', + E('div', [ + E('label', { + 'data-tooltip': _('Display base translation packages and translation packages for already installed languages only') + }, [ + E('input', { + 'type': 'radio', + 'name': 'filter_i18n', + 'value': 'lang', + 'change': handleI18nFilter, + 'checked': true + }), + ' ', + _('filtered', 'Display translation packages') + ]), + ' \u00a0 ', + E('label', { + 'data-tooltip': _('Display all available translation packages') + }, [ + E('input', { + 'type': 'radio', + 'name': 'filter_i18n', + 'value': 'all', + 'change': handleI18nFilter + }), + ' ', + _('all', 'Display translation packages') + ]), + ' \u00a0 ', + E('label', { + 'data-tooltip': _('Hide all translation packages') + }, [ + E('input', { + 'type': 'radio', + 'name': 'filter_i18n', + 'value': 'none', + 'change': handleI18nFilter + }), + ' ', + _('none', 'Display translation packages') + ]) + ]) + ]) + ]), + + E('ul', { 'class': 'cbi-tabmenu mode' }, [ + E('li', { 'data-mode': 'available', 'class': 'available cbi-tab', 'click': handleMode }, E('a', { 'href': '#' }, [ _('Available') ])), + E('li', { 'data-mode': 'installed', 'class': 'installed cbi-tab-disabled', 'click': handleMode }, E('a', { 'href': '#' }, [ _('Installed') ])), + E('li', { 'data-mode': 'updates', 'class': 'installed cbi-tab-disabled', 'click': handleMode }, E('a', { 'href': '#' }, [ _('Updates') ])) + ]), + + E('div', { 'class': 'controls', 'style': 'display:none' }, [ + E('div', { 'class': 'pager center' }, [ + E('button', { 'class': 'btn cbi-button-neutral prev', 'aria-label': _('Previous page'), 'click': handlePage }, [ '«' ]), + E('div', { 'class': 'text' }, [ 'dummy' ]), + E('button', { 'class': 'btn cbi-button-neutral next', 'aria-label': _('Next page'), 'click': handlePage }, [ '»' ]) + ]) + ]), + + E('table', { 'id': 'packages', 'class': 'table' }, [ + E('tr', { 'class': 'tr cbi-section-table-titles' }, [ + E('th', { 'class': 'th col-2 left' }, [ _('Package name') ]), + E('th', { 'class': 'th col-2 left version' }, [ _('Version') ]), + E('th', { 'class': 'th col-1 center size'}, [ _('Size (.ipk)') ]), + E('th', { 'class': 'th col-10 left' }, [ _('Description') ]), + E('th', { 'class': 'th right cbi-section-actions' }, [ '\u00a0' ]) + ]) + ]), + + E('div', { 'class': 'controls', 'style': 'display:none' }, [ + E('div', { 'class': 'pager center' }, [ + E('button', { 'class': 'btn cbi-button-neutral prev', 'aria-label': _('Previous page'), 'click': handlePage }, [ '«' ]), + E('div', { 'class': 'text' }, [ 'dummy' ]), + E('button', { 'class': 'btn cbi-button-neutral next', 'aria-label': _('Next page'), 'click': handlePage }, [ '»' ]) + ]) + ]) + ]); + + requestAnimationFrame(function() { + updateLists(listData) + }); + + return view; + }, + + handleSave: null, + handleSaveApply: null, + handleReset: null +}); |