switch (installed) {
case 'installed':
- pkg.installed = true;
+ pkg.status = ['installed'];
break;
}
break;
}
}
+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 (const p of provides) {
+ 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 avail = packages.available.pkgs[name];
const inst = packages.installed.pkgs[name];
- if (!inst || !inst.installed)
+ if (!isPkgInstalled(inst))
continue;
if (!avail || compareVersion(avail.version, pkg.version) <= 0)
}, _('Upgrade…'));
}
else if (currentDisplayMode === 'installed') {
- if (!pkg.installed)
+ if (!isPkgInstalled(pkg))
continue;
ver = truncateVersion(pkg.version || '-');
ver = truncateVersion(pkg.version || '-');
- if (!inst || !inst.installed)
+ if (!isPkgInstalled(inst))
btn = E('div', {
'class': 'btn cbi-button-action',
'data-package': name,
'data-action': 'install',
'click': handleInstall
}, _('Install…'));
- else if (inst.installed && inst.version != pkg.version)
+ else if (isPkgInstalled(inst) && inst.version !== pkg.version)
btn = E('div', {
'class': 'btn cbi-button-positive',
'data-package': name,
return c.charCodeAt(0) + 256;
}
-function compareVersion(val, ref)
-{
- let vi = 0, ri = 0;
- const 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) {
- let first_diff = 0;
+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]+/);
- while ((vi < val.length && !isdigit[val.charAt(vi)]) ||
- (ri < ref.length && !isdigit[ref.charAt(ri)])) {
- const vc = orderOf(val.charAt(vi)), rc = orderOf(ref.charAt(ri));
- if (vc !== rc)
- return vc - rc;
+ // 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);
- vi++; ri++;
- }
-
- while (val.charAt(vi) === '0')
- vi++;
+ const maxLen = Math.max(v.length, r.length);
- 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++;
- }
+ 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 (isdigit[val.charAt(vi)])
- return 1;
- else if (isdigit[ref.charAt(ri)])
- return -1;
- else if (first_diff)
- return first_diff;
+ if (vNum > rNum) return 1;
+ if (vNum < rNum) return -1;
}
return 0;
return r > 0;
case '=':
- return r == 0;
+ return r === 0;
}
return false;
info.errors = info.errors || [];
info.install = info.install || [];
- if (pkg.installed) {
+ if (isPkgInstalled(pkg)) {
if (vop && !versionSatisfied(pkg.version, ver, vop)) {
let repl = null;
if (deps[i] === 'libc')
continue;
- if (deps[i].match(/^(.+?)\s+\((<=|>=|<<|>>|<|>|=)(.+?)\)/)) {
- dep = RegExp.$1.trim();
- vop = RegExp.$2.trim();
- ver = RegExp.$3.trim();
- }
- else {
+ // 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;
}
const i18n_packages = [];
let i18n_tree;
- if (luci_basename && (luci_basename[1] != 'i18n' || luci_basename[2].indexOf('base-') === 0)) {
+ if (luci_basename && (luci_basename[1] !== 'i18n' || luci_basename[2].indexOf('base-') === 0)) {
let i18n_filter;
- if (luci_basename[1] == 'i18n') {
+ if (luci_basename[1] === 'i18n') {
const basenames = [];
for (let pkgname in packages.installed.pkgs) {
const m = pkgname.match(/^luci-([^-]+)-(.+)$/);
- if (m && m[1] != 'i18n')
+ if (m && m[1] !== 'i18n')
basenames.push(m[2]);
}
if (i18n_filter) {
for (let pkgname in packages.available.pkgs)
- if (pkgname != pkg.name && pkgname.match(i18n_filter))
+ if (pkgname !== pkg.name && pkgname.match(i18n_filter))
i18n_packages.push(pkgname);
const i18ncache = {};
}
for (let i = 0; i < partials.length; i++) {
- if (partials[i].type == 'file') {
+ if (partials[i].type === 'file') {
if (L.hasSystemFeature('apk')) {
- if (partials[i].name == 'repositories')
+ 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);
const argv = [ cmd ];
- if (cmd == 'remove')
+ if (cmd === 'remove')
argv.push('--force-removal-of-dependent-packages')
if (rem && rem.checked)
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' }))
+ 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));
- parseList(data[1], packages.available);
- parseList(data[2], packages.installed);
+ parsePackageData(data[1], packages.available);
+ parsePackageData(data[2], packages.installed);
for (let pkgname in packages.installed.pkgs)
if (pkgname.indexOf('luci-i18n-base-') === 0)
E('div', { 'class': 'controls' }, [
E('div', {}, [
- E('label', {}, _('Disk space') + ':'),
+ E('label', {'id': 'disk-space-label'}, _('Disk space') + ':'),
E('div', { 'class': 'cbi-progressbar', 'title': _('unknown') }, E('div', {}, [ '\u00a0' ]))
]),
E('div', {}, [
- E('label', {}, _('Filter') + ':'),
+ 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', {}, _('Download and install package') + ':'),
+ 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', {}, _('Actions') + ':'), ' ',
+ 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-action', 'click': handleUpload, 'disabled': isReadonlyView }, [ _('Upload Package…') ]), ' ',