mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 01:44:16 +08:00
1421 lines
40 KiB
JavaScript
1421 lines
40 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require fs';
|
|
'require ui';
|
|
'require rpc';
|
|
|
|
const 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; \
|
|
} \
|
|
\
|
|
/* bootstrap sets 15%; shrink */ \
|
|
#packages .td.cbi-section-actions { \
|
|
width: auto; \
|
|
} \
|
|
\
|
|
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; \
|
|
} \
|
|
';
|
|
|
|
const isReadonlyView = !L.hasViewPermission() || null;
|
|
|
|
const callMountPoints = rpc.declare({
|
|
object: 'luci',
|
|
method: 'getMountPoints',
|
|
expect: { result: [] }
|
|
});
|
|
|
|
const packages = {
|
|
available: { providers: {}, pkgs: {} },
|
|
installed: { providers: {}, pkgs: {} }
|
|
};
|
|
|
|
const languages = ['en'];
|
|
|
|
let currentDisplayMode = 'available'
|
|
const currentDisplayRows = [];
|
|
|
|
function parseList(s, dest)
|
|
{
|
|
const re = /([^\n]*)\n/g;
|
|
let pkg = null, key = null, val = null, m;
|
|
let list, stat, mode, installed;
|
|
|
|
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':
|
|
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':
|
|
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.status = ['installed'];
|
|
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] = dest.pkgs[pkg.name] ? dest.pkgs[pkg.name] : pkg;
|
|
|
|
const 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 parseApkQueryJson(s, dest) {
|
|
// Parse the raw JSON text string into an array
|
|
let rawData;
|
|
try {
|
|
rawData = JSON.parse(s);
|
|
} catch (e) {
|
|
console.error("Failed to parse APK JSON:", e);
|
|
return;
|
|
}
|
|
// Ensure rawData is actually an array before iterating
|
|
if (!Array.isArray(rawData))
|
|
return;
|
|
// Ensure our storage objects exist on the destination
|
|
dest.pkgs = dest.pkgs || {};
|
|
dest.providers = dest.providers || {};
|
|
|
|
// Single pass through each item in the new JSON array
|
|
for (const item of rawData) {
|
|
|
|
// Rename 'file-size' to 'size' while pulling out 'name'
|
|
const { name, 'file-size': size, ...attributes } = item;
|
|
|
|
// Construct the package object with the expected 'size' key
|
|
const pkg = { name, size, ...attributes };
|
|
|
|
// Add/Update the package in the main map
|
|
dest.pkgs[name] = pkg;
|
|
|
|
// Determine all provided names (a package always provides itself)
|
|
const provides = [name, ...(Array.isArray(pkg.provides) ? pkg.provides : [])];
|
|
for (let p of provides) {
|
|
p = p.split('=')[0]; // Handle cases where provides are versioned
|
|
dest.providers[p] = dest.providers[p] || [];
|
|
if (!dest.providers[p].includes(pkg))
|
|
dest.providers[p].push(pkg);
|
|
}
|
|
}
|
|
}
|
|
|
|
function parsePackageData(s, dest) {
|
|
if (!s) return;
|
|
|
|
// Check if the input is JSON (starts with [)
|
|
if (s.trim().charAt(0) === '[') {
|
|
parseApkQueryJson(s, dest);
|
|
} else {
|
|
parseList(s, dest);
|
|
}
|
|
}
|
|
|
|
function isPkgInstalled(pkg) {
|
|
return pkg && Array.isArray(pkg.status) && pkg.status.includes('installed');
|
|
}
|
|
|
|
function display(pattern)
|
|
{
|
|
const src = packages[currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode];
|
|
const pagers = document.querySelectorAll('.controls > .pager');
|
|
let 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 (let name in src.pkgs) {
|
|
const pkg = src.pkgs[name];
|
|
let desc = pkg.description || '';
|
|
let 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;
|
|
|
|
let btn, ver;
|
|
|
|
if (currentDisplayMode === 'updates') {
|
|
const avail = packages.available.pkgs[name];
|
|
const inst = packages.installed.pkgs[name];
|
|
|
|
if (!isPkgInstalled(inst) || pkg.name.includes('busybox') || pkg.name.includes('base-files'))
|
|
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,
|
|
'data-action': 'upgrade',
|
|
'click': handleInstall
|
|
}, _('Upgrade…'));
|
|
}
|
|
else if (currentDisplayMode === 'installed') {
|
|
if (!isPkgInstalled(pkg))
|
|
continue;
|
|
|
|
ver = truncateVersion(pkg.version || '-');
|
|
btn = E('div', {
|
|
'class': 'btn cbi-button-negative',
|
|
'data-package': name,
|
|
'click': handleRemove
|
|
}, _('Remove…'));
|
|
}
|
|
else {
|
|
const inst = packages.installed.pkgs[name];
|
|
|
|
ver = truncateVersion(pkg.version || '-');
|
|
|
|
if (!isPkgInstalled(inst))
|
|
btn = E('div', {
|
|
'class': 'btn cbi-button-action',
|
|
'data-package': name,
|
|
'data-action': 'install',
|
|
'click': handleInstall
|
|
}, _('Install…'));
|
|
else if (isPkgInstalled(inst) && compareVersion(pkg.version, inst.version) > 0)
|
|
btn = E('div', {
|
|
'class': 'btn cbi-button-positive',
|
|
'data-package': name,
|
|
'data-action': 'upgrade',
|
|
'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 (let pager of pagers) {
|
|
pager.parentNode.style.display = '';
|
|
pager.setAttribute('data-offset', 100);
|
|
}
|
|
|
|
handlePage({ target: pagers[0].querySelector('.prev') });
|
|
}
|
|
|
|
function handlePage(ev)
|
|
{
|
|
const filter = document.querySelector('input[name="filter"]');
|
|
let offset = +ev.target.parentNode.getAttribute('data-offset');
|
|
const next = ev.target.classList.contains('next');
|
|
const pagers = document.querySelectorAll('.controls > .pager');
|
|
|
|
if ((next && (offset + 100) >= currentDisplayRows.length) ||
|
|
(!next && (offset < 100)))
|
|
return;
|
|
|
|
offset += next ? 100 : -100;
|
|
|
|
for (let pager of pagers) {
|
|
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');
|
|
}
|
|
|
|
let 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)
|
|
{
|
|
const 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) {
|
|
// 1. Split into parts by dots or any non-digit sequences
|
|
const vParts = (val || '').split(/[^0-9]+/);
|
|
const rParts = (ref || '').split(/[^0-9]+/);
|
|
|
|
// 2. Filter out empty strings caused by leading/trailing non-digits
|
|
const v = vParts.filter(x => x.length > 0);
|
|
const r = rParts.filter(x => x.length > 0);
|
|
|
|
const maxLen = Math.max(v.length, r.length);
|
|
|
|
for (let i = 0; i < maxLen; i++) {
|
|
// Convert to integer (base 10) to automatically ignore leading zeros
|
|
const vNum = parseInt(v[i] || 0, 10);
|
|
const rNum = parseInt(r[i] || 0, 10);
|
|
|
|
if (vNum > rNum) return 1;
|
|
if (vNum < rNum) return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
function versionSatisfied(ver, ref, vop)
|
|
{
|
|
const 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, count)
|
|
{
|
|
info.errors = info.errors || [];
|
|
info.install = info.install || [];
|
|
|
|
if (isPkgInstalled(pkg)) {
|
|
if (vop && !versionSatisfied(pkg.version, ver, vop)) {
|
|
let 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)) {
|
|
if (count !== false)
|
|
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)
|
|
{
|
|
const li = E('li');
|
|
const vop = dep.version ? dep.version[0] : null;
|
|
const ver = dep.version ? dep.version[1] : null;
|
|
const depends = [];
|
|
|
|
// A dependency with several providers (alternatives) is satisfied by any
|
|
// single one of them, so apk only ever installs one. Mirror the apk
|
|
// solver's provider selection - an already installed one, otherwise the
|
|
// available one with the highest provider-priority, otherwise the first
|
|
// available one - and count only that towards the size estimate and
|
|
// subdependencies instead of every alternative.
|
|
let effective = -1;
|
|
|
|
for (let i = 0; dep.pkgs && i < dep.pkgs.length; i++) {
|
|
if (isPkgInstalled(packages.installed.pkgs[dep.pkgs[i]])) {
|
|
effective = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
const satisfied = effective >= 0;
|
|
|
|
if (!satisfied && dep.pkgs && dep.pkgs.length) {
|
|
let prio = null;
|
|
|
|
for (let i = 0; i < dep.pkgs.length; i++) {
|
|
const p = packages.available.pkgs[dep.pkgs[i]];
|
|
if (!p)
|
|
continue;
|
|
|
|
const pprio = Number(p['provider-priority']) || 0;
|
|
if (prio === null || pprio > prio) {
|
|
effective = i;
|
|
prio = pprio;
|
|
}
|
|
}
|
|
|
|
if (effective < 0)
|
|
effective = 0;
|
|
}
|
|
|
|
for (let i = 0; dep.pkgs && i < dep.pkgs.length; i++) {
|
|
const pkg = packages.installed.pkgs[dep.pkgs[i]] ||
|
|
packages.available.pkgs[dep.pkgs[i]] ||
|
|
{ name: dep.name };
|
|
|
|
if (i > 0)
|
|
li.appendChild(document.createTextNode(' | '));
|
|
|
|
let 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, !satisfied && i === effective) ]));
|
|
|
|
if (i === effective)
|
|
(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) {
|
|
const subdeps = renderDependencies(depends, info);
|
|
if (subdeps)
|
|
li.appendChild(subdeps);
|
|
}
|
|
|
|
return li;
|
|
}
|
|
|
|
function renderDependencies(depends, info, flat)
|
|
{
|
|
const deps = depends || [];
|
|
const items = [];
|
|
|
|
info.seen = info.seen || [];
|
|
|
|
for (let i = 0; i < deps.length; i++) {
|
|
let dep, vop, ver;
|
|
|
|
if (deps[i] === 'libc')
|
|
continue;
|
|
|
|
// This regex handles "name", "name>=ver", and "name (>=ver)"
|
|
const match = deps[i].match(/^([^><=~\s]+)\s*\(?([><=~]+)?\s*([^)]+)?\)?$/);
|
|
|
|
if (match) {
|
|
// Destructure the match array: [full_match, name, operator, version]
|
|
const [, matchedDep, matchedVop, matchedVer] = match;
|
|
dep = (matchedDep || '').trim();
|
|
vop = (matchedVop || '').trim() || null;
|
|
ver = (matchedVer || '').trim() || null;
|
|
} else {
|
|
// Fallback if the string is just a plain name with no operators
|
|
dep = deps[i].trim();
|
|
vop = ver = null;
|
|
}
|
|
|
|
if (info.seen[dep])
|
|
continue;
|
|
|
|
const 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)
|
|
{
|
|
const filter = document.querySelector('input[name="filter"]');
|
|
|
|
filter.value = '';
|
|
display();
|
|
}
|
|
|
|
function handleInstall(ev)
|
|
{
|
|
const name = ev.target.getAttribute('data-package');
|
|
const installcmd = ev.target.getAttribute('data-action');
|
|
const pkg = packages.available.pkgs[name];
|
|
const depcache = {};
|
|
let size;
|
|
|
|
if (pkg.installsize)
|
|
size = _('~%1024mB installed').format(pkg.installsize);
|
|
else if (pkg.size)
|
|
size = _('~%1024mB compressed').format(pkg.size);
|
|
else
|
|
size = _('unknown');
|
|
|
|
const deps = renderDependencies(pkg.depends, depcache);
|
|
let 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));
|
|
});
|
|
}
|
|
|
|
let totalsize = pkg.installsize || pkg.size || 0;
|
|
let totalpkgs = 1;
|
|
let suggestsize = 0;
|
|
|
|
if (depcache.install && depcache.install.length)
|
|
depcache.install.forEach(function(ipkg) {
|
|
totalsize += ipkg.installsize || ipkg.size || 0;
|
|
totalpkgs++;
|
|
});
|
|
|
|
const luci_basename = pkg.name.match(/^luci-([^-]+)-(.+)$/);
|
|
const i18n_packages = [];
|
|
let i18n_tree;
|
|
|
|
if (luci_basename && (luci_basename[1] !== 'i18n' || luci_basename[2].indexOf('base-') === 0)) {
|
|
let i18n_filter;
|
|
|
|
if (luci_basename[1] === 'i18n') {
|
|
const basenames = [];
|
|
|
|
for (let pkgname in packages.installed.pkgs) {
|
|
const 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 (let pkgname in packages.available.pkgs)
|
|
if (pkgname !== pkg.name && pkgname.match(i18n_filter))
|
|
i18n_packages.push(pkgname);
|
|
|
|
const 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,
|
|
'checked': true
|
|
}), ' ',
|
|
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': installcmd,
|
|
'data-package': name,
|
|
'class': 'btn cbi-button-action',
|
|
'click': handlePkg,
|
|
'disabled': isReadonlyView
|
|
}, _('Install'))
|
|
])
|
|
]);
|
|
}
|
|
|
|
function handleManualInstall(ev)
|
|
{
|
|
const name_or_url = document.querySelector('input[name="install"]').value;
|
|
let 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'));
|
|
let 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)
|
|
{
|
|
const conf = {};
|
|
const 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) {
|
|
const files = [];
|
|
|
|
if (L.hasSystemFeature('apk')) {
|
|
files.push(base_dir + '/' + 'repositories.d/customfeeds.list',
|
|
base_dir + '/' + 'repositories.d/distfeeds.list'
|
|
)
|
|
} else {
|
|
files.push(base_dir + '.conf')
|
|
}
|
|
|
|
for (let 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() {
|
|
const 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>.')
|
|
const 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>.')
|
|
const 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) + 1
|
|
}, '%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) {
|
|
const 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)
|
|
{
|
|
const name = ev.target.getAttribute('data-package');
|
|
const pkg = packages.installed.pkgs[name];
|
|
const avail = packages.available.pkgs[name] || {};
|
|
let 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') || null }), ' ',
|
|
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) {
|
|
const cmd = ev.target.getAttribute('data-command');
|
|
const pkg = ev.target.getAttribute('data-package');
|
|
const rem = document.querySelector('input[name="autoremove"]');
|
|
const owr = document.querySelector('input[name="overwrite"]');
|
|
const i18n = document.querySelector('input[name="i18ninstall"]');
|
|
|
|
const 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))
|
|
]);
|
|
|
|
const 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.pkmcmd)
|
|
dlg.appendChild(E('pre', [ res.pkmcmd ]));
|
|
|
|
const showModalFlag = (cmd !== 'update' && pkg) || res.stderr;
|
|
if (showModalFlag) {
|
|
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'))));
|
|
} else {
|
|
ui.hideModal();
|
|
updateLists();
|
|
}
|
|
}).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)
|
|
{
|
|
const 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) {
|
|
const pg = document.querySelector('.cbi-progressbar');
|
|
const 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));
|
|
|
|
parsePackageData(data[1], packages.available);
|
|
parsePackageData(data[2], packages.installed);
|
|
|
|
for (let pkgname in packages.installed.pkgs)
|
|
if (pkgname.indexOf('luci-i18n-base-') === 0)
|
|
languages.push(pkgname.substring(15));
|
|
|
|
display(document.querySelector('input[name="filter"]').value);
|
|
});
|
|
}
|
|
|
|
let inputTimeout = null;
|
|
|
|
function handleInput(ev) {
|
|
if (inputTimeout !== null)
|
|
window.clearTimeout(inputTimeout);
|
|
|
|
inputTimeout = window.setTimeout(function() {
|
|
display(ev.target.value);
|
|
}, 250);
|
|
}
|
|
|
|
return view.extend({
|
|
load() {
|
|
return downloadLists();
|
|
},
|
|
|
|
render(listData) {
|
|
const checkUpdateNeeded = function() {
|
|
return Promise.all([
|
|
L.resolveDefault(fs.stat('/tmp/opkg-lists/kwrt_kiddin9'), null),
|
|
L.resolveDefault(fs.read('/tmp/resolv.conf.d/resolv.conf.auto'), '')
|
|
]).then(function(results) {
|
|
const stat = results[0];
|
|
const resolvContent = results[1];
|
|
|
|
let needUpdate = false;
|
|
|
|
if (stat) {
|
|
const currentDate = new Date();
|
|
const lastUpdateDate = new Date(stat.ctime * 1000); // Convert seconds to milliseconds
|
|
// 检查是否在今天的零点之后更新过
|
|
const today = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate());
|
|
needUpdate = lastUpdateDate < today;
|
|
} else {
|
|
needUpdate = true;
|
|
}
|
|
|
|
// 检查 resolv.conf.auto 文件内容
|
|
const hasResolvContent = resolvContent && resolvContent.trim().length > 0;
|
|
|
|
// 只有当需要更新且 resolv.conf.auto 不为空时,才返回 true
|
|
return needUpdate && hasResolvContent;
|
|
}).catch(function(error) {
|
|
console.error('Error checking update status:', error);
|
|
return false; // 如果出错,不执行更新
|
|
});
|
|
};
|
|
const query = decodeURIComponent(L.toArray(location.search.match(/\bquery=([^=]+)\b/))[1] || '');
|
|
|
|
const 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': 'cbi-section' }, E('div', { 'class': 'controls' }, [
|
|
E('div', {}, [
|
|
E('label', {'id': 'disk-space-label'}, _('Disk space') + ':'),
|
|
E('div', { 'class': 'cbi-progressbar', 'title': _('unknown') }, E('div', {}, [ '\u00a0' ]))
|
|
]),
|
|
|
|
E('div', {}, [
|
|
E('label', {'id': 'filter-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', {'id': 'download-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', {'id': 'action-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-negative', 'data-command': 'upgradeall', 'click': handlePkg, 'disabled': isReadonlyView }, [ _('Upgrade all…') ]), ' ',
|
|
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': 'cbi-section' }, [
|
|
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 (%s)').format(L.hasSystemFeature('apk') ? '.apk' : '.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);
|
|
checkUpdateNeeded().then(function(needUpdate) {
|
|
if (needUpdate) {
|
|
setTimeout(function() {
|
|
const updateButton = document.querySelector('button[data-command="update"]');
|
|
if (updateButton) {
|
|
updateButton.click();
|
|
}
|
|
}, 10)
|
|
}
|
|
});
|
|
});
|
|
|
|
return view;
|
|
},
|
|
|
|
handleSave: null,
|
|
handleSaveApply: null,
|
|
handleReset: null
|
|
});
|