10 margin: .5em 0 1em 0; \
12 justify-content: space-around; \
17 white-space: nowrap; \
19 box-sizing: border-box; \
24 .controls > *:first-child, \
25 .controls > * > label { \
30 .controls > *:nth-child(2), \
31 .controls > *:nth-child(3) { \
35 .controls > * > .btn { \
45 .controls > div > input { \
51 white-space: nowrap; \
54 ul.deps, ul.deps ul, ul.errors { \
58 ul.deps li, ul.errors li { \
64 display: inline-block; \
70 white-space: nowrap; \
77 padding-left: 1.5em; \
80 ul.errors li:before { \
82 display: inline-block; \
84 margin-left: -1.5em; \
88 var isReadonlyView
= !L
.hasViewPermission() || null;
90 var callMountPoints
= rpc
.declare({
92 method
: 'getMountPoints',
93 expect
: { result
: [] }
97 available
: { providers
: {}, pkgs
: {} },
98 installed
: { providers
: {}, pkgs
: {} }
101 var currentDisplayMode
= 'available', currentDisplayRows
= [];
103 function parseList(s
, dest
)
105 var re
= /([^\n]*)\n/g,
106 pkg
= null, key
= null, val
= null, m
;
108 while ((m
= re
.exec(s
)) !== null) {
109 if (m
[1].match(/^\s(.*)$/)) {
110 if (pkg
!== null && key
!== null && val
!== null)
111 val
+= '\n' + RegExp
.$1.trim();
116 if (key
!== null && val
!== null) {
124 var list
= val
.split(/\s*,\s*/);
125 if (list
.length
!== 1 || list
[0].length
> 0)
129 case 'installed-time':
130 pkg
.installtime
= new Date(+val
* 1000);
133 case 'installed-size':
134 pkg
.installsize
= +val
;
138 var stat
= val
.split(/\s+/),
151 pkg
.installed
= true;
158 pkg
.essential
= true;
166 case 'auto-installed':
180 if (m
[1].trim().match(/^([\w-]+)\s*:(.+)$/)) {
181 key
= RegExp
.$1.toLowerCase();
182 val
= RegExp
.$2.trim();
185 dest
.pkgs
[pkg
.name
] = pkg
;
187 var provides
= dest
.providers
[pkg
.name
] ? [] : [ pkg
.name
];
190 provides
.push
.apply(provides
, pkg
.provides
);
192 provides
.forEach(function(p
) {
193 dest
.providers
[p
] = dest
.providers
[p
] || [];
194 dest
.providers
[p
].push(pkg
);
200 function display(pattern
)
202 var src
= packages
[currentDisplayMode
=== 'updates' ? 'installed' : currentDisplayMode
],
203 table
= document
.querySelector('#packages'),
204 pager
= document
.querySelector('#pager');
206 currentDisplayRows
.length
= 0;
208 if (typeof(pattern
) === 'string' && pattern
.length
> 0)
209 pattern
= new RegExp(pattern
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig');
211 for (var name
in src
.pkgs
) {
212 var pkg
= src
.pkgs
[name
],
213 desc
= pkg
.description
|| '',
216 if (!pkg
.size
&& packages
.available
.pkgs
[name
])
217 altsize
= packages
.available
.pkgs
[name
].size
;
219 if (!desc
&& packages
.available
.pkgs
[name
])
220 desc
= packages
.available
.pkgs
[name
].description
|| '';
222 desc
= desc
.split(/\n/);
223 desc
= desc
[0].trim() + (desc
.length
> 1 ? '…' : '');
225 if ((pattern
instanceof RegExp
) &&
226 !name
.match(pattern
) && !desc
.match(pattern
))
231 if (currentDisplayMode
=== 'updates') {
232 var avail
= packages
.available
.pkgs
[name
],
233 inst
= packages
.installed
.pkgs
[name
];
235 if (!inst
|| !inst
.installed
)
238 if (!avail
|| compareVersion(avail
.version
, pkg
.version
) <= 0)
241 ver
= '%s » %s'.format(
242 truncateVersion(pkg
.version
|| '-'),
243 truncateVersion(avail
.version
|| '-'));
246 'class': 'btn cbi-button-positive',
247 'data-package': name
,
248 'click': handleInstall
251 else if (currentDisplayMode
=== 'installed') {
255 ver
= truncateVersion(pkg
.version
|| '-');
257 'class': 'btn cbi-button-negative',
258 'data-package': name
,
259 'click': handleRemove
263 var inst
= packages
.installed
.pkgs
[name
];
265 ver
= truncateVersion(pkg
.version
|| '-');
267 if (!inst
|| !inst
.installed
)
269 'class': 'btn cbi-button-action',
270 'data-package': name
,
271 'click': handleInstall
273 else if (inst
.installed
&& inst
.version
!= pkg
.version
)
275 'class': 'btn cbi-button-positive',
276 'data-package': name
,
277 'click': handleInstall
281 'class': 'btn cbi-button-neutral',
282 'disabled': 'disabled'
286 name
= '%h'.format(name
);
287 desc
= '%h'.format(desc
|| '-');
290 name
= name
.replace(pattern
, '<ins>$&</ins>');
291 desc
= desc
.replace(pattern
, '<ins>$&</ins>');
294 currentDisplayRows
.push([
297 pkg
.size
? '%.1024mB'.format(pkg
.size
)
298 : (altsize
? '~%.1024mB'.format(altsize
) : '-'),
304 currentDisplayRows
.sort(function(a
, b
) {
307 else if (a
[0] > b
[0])
313 pager
.parentNode
.style
.display
= '';
314 pager
.setAttribute('data-offset', 100);
315 handlePage({ target
: pager
.querySelector('.prev') });
318 function handlePage(ev
)
320 var filter
= document
.querySelector('input[name="filter"]'),
321 pager
= ev
.target
.parentNode
,
322 offset
= +pager
.getAttribute('data-offset'),
323 next
= ev
.target
.classList
.contains('next');
325 if ((next
&& (offset
+ 100) >= currentDisplayRows
.length
) ||
326 (!next
&& (offset
< 100)))
329 offset
+= next
? 100 : -100;
330 pager
.setAttribute('data-offset', offset
);
331 pager
.querySelector('.text').firstChild
.data
= currentDisplayRows
.length
332 ? _('Displaying %d-%d of %d').format(1 + offset
, Math
.min(offset
+ 100, currentDisplayRows
.length
), currentDisplayRows
.length
)
336 pager
.querySelector('.prev').setAttribute('disabled', 'disabled');
338 pager
.querySelector('.prev').removeAttribute('disabled');
340 if ((offset
+ 100) >= currentDisplayRows
.length
)
341 pager
.querySelector('.next').setAttribute('disabled', 'disabled');
343 pager
.querySelector('.next').removeAttribute('disabled');
345 var placeholder
= _('No information available');
349 E('span', {}, _('No packages matching "<strong>%h</strong>".').format(filter
.value
)), ' (',
350 E('a', { href
: '#', click
: handleReset
}, _('Reset')), ')'
353 cbi_update_table('#packages', currentDisplayRows
.slice(offset
, offset
+ 100),
357 function handleMode(ev
)
359 var tab
= findParent(ev
.target
, 'li');
360 if (tab
.getAttribute('data-mode') === currentDisplayMode
)
363 tab
.parentNode
.querySelectorAll('li').forEach(function(li
) {
364 li
.classList
.remove('cbi-tab');
365 li
.classList
.add('cbi-tab-disabled');
368 tab
.classList
.remove('cbi-tab-disabled');
369 tab
.classList
.add('cbi-tab');
371 currentDisplayMode
= tab
.getAttribute('data-mode');
373 display(document
.querySelector('input[name="filter"]').value
);
383 else if (c
=== '' || c
>= '0' && c
<= '9')
385 else if ((c
>= 'a' && c
<= 'z') || (c
>= 'A' && c
<= 'Z'))
386 return c
.charCodeAt(0);
388 return c
.charCodeAt(0) + 256;
391 function compareVersion(val
, ref
)
394 isdigit
= { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 };
402 while (vi
< val
.length
|| ri
< ref
.length
) {
405 while ((vi
< val
.length
&& !isdigit
[val
.charAt(vi
)]) ||
406 (ri
< ref
.length
&& !isdigit
[ref
.charAt(ri
)])) {
407 var vc
= orderOf(val
.charAt(vi
)), rc
= orderOf(ref
.charAt(ri
));
414 while (val
.charAt(vi
) === '0')
417 while (ref
.charAt(ri
) === '0')
420 while (isdigit
[val
.charAt(vi
)] && isdigit
[ref
.charAt(ri
)]) {
421 first_diff
= first_diff
|| (val
.charCodeAt(vi
) - ref
.charCodeAt(ri
));
425 if (isdigit
[val
.charAt(vi
)])
427 else if (isdigit
[ref
.charAt(ri
)])
436 function versionSatisfied(ver
, ref
, vop
)
438 var r
= compareVersion(ver
, ref
);
462 function pkgStatus(pkg
, vop
, ver
, info
)
464 info
.errors
= info
.errors
|| [];
465 info
.install
= info
.install
|| [];
468 if (vop
&& !versionSatisfied(pkg
.version
, ver
, vop
)) {
471 (packages
.available
.providers
[pkg
.name
] || []).forEach(function(p
) {
472 if (!repl
&& versionSatisfied(p
.version
, ver
, vop
))
477 info
.install
.push(repl
);
480 'data-tooltip': _('Requires update to %h %h')
481 .format(repl
.name
, repl
.version
)
482 }, _('Needs upgrade'));
485 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
)));
488 'class': 'label warning',
489 'data-tooltip': _('Require version %h %h,\ninstalled %h')
490 .format(vop
, ver
, pkg
.version
)
491 }, _('Version incompatible'));
494 return E('span', { 'class': 'label notice' }, _('Installed'));
496 else if (!pkg
.missing
) {
497 if (!vop
|| versionSatisfied(pkg
.version
, ver
, vop
)) {
498 info
.install
.push(pkg
);
499 return E('span', { 'class': 'label' }, _('Not installed'));
502 info
.errors
.push(_('The repository version of package <em>%h</em> is not compatible, require %s but only %s is available.')
503 .format(pkg
.name
, truncateVersion(ver
, vop
), truncateVersion(pkg
.version
)));
506 'class': 'label warning',
507 'data-tooltip': _('Require version %h %h,\ninstalled %h')
508 .format(vop
, ver
, pkg
.version
)
509 }, _('Version incompatible'));
512 info
.errors
.push(_('Required dependency package <em>%h</em> is not available in any repository.').format(pkg
.name
));
514 return E('span', { 'class': 'label warning' }, _('Not available'));
518 function renderDependencyItem(dep
, info
)
521 vop
= dep
.version
? dep
.version
[0] : null,
522 ver
= dep
.version
? dep
.version
[1] : null,
525 for (var i
= 0; dep
.pkgs
&& i
< dep
.pkgs
.length
; i
++) {
526 var pkg
= packages
.installed
.pkgs
[dep
.pkgs
[i
]] ||
527 packages
.available
.pkgs
[dep
.pkgs
[i
]] ||
531 li
.appendChild(document
.createTextNode(' | '));
536 text
+= ' (%.1024mB)'.format(pkg
.installsize
);
538 text
+= ' (~%.1024mB)'.format(pkg
.size
);
540 li
.appendChild(E('span', { 'data-tooltip': pkg
.description
},
541 [ text
, ' ', pkgStatus(pkg
, vop
, ver
, info
) ]));
543 (pkg
.depends
|| []).forEach(function(d
) {
544 if (depends
.indexOf(d
) === -1)
550 li
.appendChild(E('span', {},
552 pkgStatus({ name
: dep
.name
, missing
: true }, vop
, ver
, info
) ]));
554 var subdeps
= renderDependencies(depends
, info
);
556 li
.appendChild(subdeps
);
561 function renderDependencies(depends
, info
)
563 var deps
= depends
|| [],
566 info
.seen
= info
.seen
|| [];
568 for (var i
= 0; i
< deps
.length
; i
++) {
571 if (deps
[i
] === 'libc')
574 if (deps
[i
].match(/^(.+)\s+\((<=|<|>|>=|=|<<|>>)(.+)\)$/)) {
575 dep
= RegExp
.$1.trim();
576 vop
= RegExp
.$2.trim();
577 ver
= RegExp
.$3.trim();
580 dep
= deps
[i
].trim();
589 (packages
.installed
.providers
[dep
] || []).forEach(function(p
) {
590 if (pkgs
.indexOf(p
.name
) === -1) pkgs
.push(p
.name
);
593 (packages
.available
.providers
[dep
] || []).forEach(function(p
) {
594 if (pkgs
.indexOf(p
.name
) === -1) pkgs
.push(p
.name
);
603 items
.push(renderDependencyItem(info
.seen
[dep
], info
));
607 return E('ul', { 'class': 'deps' }, items
);
612 function truncateVersion(v
, op
)
614 v
= v
.replace(/\b(([a-f0-9]{8})[a-f0-9]{24,32})\b/,
615 '<span data-tooltip="$1">$2…</span>');
617 if (!op
|| op
=== '=')
620 return '%h %h'.format(op
, v
);
623 function handleReset(ev
)
625 var filter
= document
.querySelector('input[name="filter"]');
631 function handleInstall(ev
)
633 var name
= ev
.target
.getAttribute('data-package'),
634 pkg
= packages
.available
.pkgs
[name
],
639 size
= _('~%.1024mB installed').format(pkg
.installsize
);
641 size
= _('~%.1024mB compressed').format(pkg
.size
);
645 var deps
= renderDependencies(pkg
.depends
, depcache
),
646 tree
= null, errs
= null, inst
= null, desc
= null;
648 if (depcache
.errors
&& depcache
.errors
.length
) {
649 errs
= E('ul', { 'class': 'errors' });
650 depcache
.errors
.forEach(function(err
) {
651 errs
.appendChild(E('li', {}, err
));
655 var totalsize
= pkg
.installsize
|| pkg
.size
|| 0,
658 if (depcache
.install
&& depcache
.install
.length
)
659 depcache
.install
.forEach(function(ipkg
) {
660 totalsize
+= ipkg
.installsize
|| ipkg
.size
|| 0;
665 _('Require approx. %.1024mB size for %d package(s) to install.')
666 .format(totalsize
, totalpkgs
));
669 tree
= E('li', '<strong>%s:</strong>'.format(_('Dependencies')));
670 tree
.appendChild(deps
);
673 if (pkg
.description
) {
674 desc
= E('div', {}, [
675 E('h5', {}, _('Description')),
676 E('p', {}, pkg
.description
)
680 ui
.showModal(_('Details for package <em>%h</em>').format(pkg
.name
), [
682 E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg
.version
)),
683 E('li', '<strong>%s:</strong> %h'.format(_('Size'), size
)),
688 E('div', { 'class': 'right' }, [
689 E('label', { 'class': 'cbi-checkbox', 'style': 'float:left' }, [
690 E('input', { 'id': 'overwrite-cb', 'type': 'checkbox', 'name': 'overwrite', 'disabled': isReadonlyView
}), ' ',
691 E('label', { 'for': 'overwrite-cb' }), ' ',
692 _('Overwrite files from other package(s)')
696 'click': ui
.hideModal
700 'data-command': 'install',
701 'data-package': name
,
702 'class': 'btn cbi-button-action',
704 'disabled': isReadonlyView
710 function handleManualInstall(ev
)
712 var name_or_url
= document
.querySelector('input[name="install"]').value
,
714 'class': 'btn cbi-button-action',
715 'data-command': 'install',
716 'data-package': name_or_url
,
717 'click': function(ev
) {
718 document
.querySelector('input[name="install"]').value
= '';
721 }, _('Install')), warning
;
723 if (!name_or_url
.length
) {
726 else if (name_or_url
.indexOf('/') !== -1) {
727 warning
= E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(name_or_url
));
729 else if (!packages
.available
.providers
[name_or_url
]) {
730 warning
= E('p', {}, _('The package <em>%h</em> is not available in any configured repository.').format(name_or_url
));
734 warning
= E('p', {}, _('Really attempt to install <em>%h</em>?').format(name_or_url
));
737 ui
.showModal(_('Manually install package'), [
739 E('div', { 'class': 'right' }, [
741 'click': ui
.hideModal
,
742 'class': 'btn cbi-button-neutral'
749 function handleConfig(ev
)
753 ui
.showModal(_('OPKG Configuration'), [
754 E('p', { 'class': 'spinning' }, _('Loading configuration data…'))
757 fs
.list('/etc/opkg').then(function(partials
) {
758 var files
= [ '/etc/opkg.conf' ];
760 for (var i
= 0; i
< partials
.length
; i
++)
761 if (partials
[i
].type
== 'file' && partials
[i
].name
.match(/\.conf$/))
762 files
.push('/etc/opkg/' + partials
[i
].name
);
764 return Promise
.all(files
.map(function(file
) {
766 .then(L
.bind(function(conf
, file
, res
) { conf
[file
] = res
}, this, conf
, file
))
767 .catch(function(err
) {
768 ui
.addNotification(null, E('p', {}, [ _('Unable to read %s: %s').format(file
, err
) ]));
775 E('p', {}, _('Below is a listing of the various configuration files used by <em>opkg</em>. Use <em>opkg.conf</em> for global settings and <em>customfeeds.conf</em> for custom repository entries. The configuration in the other files may be changed but is usually not preserved by <em>sysupgrade</em>.'))
778 Object
.keys(conf
).sort().forEach(function(file
) {
779 body
.push(E('h5', {}, '%h'.format(file
)));
780 body
.push(E('textarea', {
782 'rows': Math
.max(Math
.min(L
.toArray(conf
[file
].match(/\n/g)).length
, 10), 3)
783 }, '%h'.format(conf
[file
])));
786 body
.push(E('div', { 'class': 'right' }, [
788 'class': 'btn cbi-button-neutral',
789 'click': ui
.hideModal
793 'class': 'btn cbi-button-positive',
794 'click': function(ev
) {
796 findParent(ev
.target
, '.modal').querySelectorAll('textarea[name]')
797 .forEach(function(textarea
) {
798 data
[textarea
.getAttribute('name')] = textarea
.value
801 ui
.showModal(_('OPKG Configuration'), [
802 E('p', { 'class': 'spinning' }, _('Saving configuration data…'))
805 Promise
.all(Object
.keys(data
).map(function(file
) {
806 return fs
.write(file
, data
[file
]).catch(function(err
) {
807 ui
.addNotification(null, E('p', {}, [ _('Unable to save %s: %s').format(file
, err
) ]));
809 })).then(ui
.hideModal
);
811 'disabled': isReadonlyView
815 ui
.showModal(_('OPKG Configuration'), body
);
819 function handleRemove(ev
)
821 var name
= ev
.target
.getAttribute('data-package'),
822 pkg
= packages
.installed
.pkgs
[name
],
823 avail
= packages
.available
.pkgs
[name
] || {},
826 if (avail
.installsize
)
827 size
= _('~%.1024mB installed').format(avail
.installsize
);
829 size
= _('~%.1024mB compressed').format(avail
.size
);
833 if (avail
.description
) {
834 desc
= E('div', {}, [
835 E('h5', {}, _('Description')),
836 E('p', {}, avail
.description
)
840 ui
.showModal(_('Remove package <em>%h</em>').format(pkg
.name
), [
842 E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg
.version
)),
843 E('li', '<strong>%s:</strong> %h'.format(_('Size'), size
))
846 E('div', { 'style': 'display:flex; justify-content:space-between; flex-wrap:wrap' }, [
847 E('label', { 'class': 'cbi-checkbox', 'style': 'float:left' }, [
848 E('input', { 'id': 'autoremove-cb', 'type': 'checkbox', 'checked': 'checked', 'name': 'autoremove', 'disabled': isReadonlyView
}), ' ',
849 E('label', { 'for': 'autoremove-cb' }), ' ',
850 _('Automatically remove unused dependencies')
852 E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [
855 'click': ui
.hideModal
859 'data-command': 'remove',
860 'data-package': name
,
861 'class': 'btn cbi-button-negative',
863 'disabled': isReadonlyView
870 function handleOpkg(ev
)
872 return new Promise(function(resolveFn
, rejectFn
) {
873 var cmd
= ev
.target
.getAttribute('data-command'),
874 pkg
= ev
.target
.getAttribute('data-package'),
875 rem
= document
.querySelector('input[name="autoremove"]'),
876 owr
= document
.querySelector('input[name="overwrite"]');
878 var dlg
= ui
.showModal(_('Executing package manager'), [
879 E('p', { 'class': 'spinning' },
880 _('Waiting for the <em>opkg %h</em> command to complete…').format(cmd
))
883 var argv
= [ cmd
, '--force-removal-of-dependent-packages' ];
885 if (rem
&& rem
.checked
)
886 argv
.push('--autoremove');
888 if (owr
&& owr
.checked
)
889 argv
.push('--force-overwrite');
894 fs
.exec_direct('/usr/libexec/opkg-call', argv
, 'json').then(function(res
) {
895 dlg
.removeChild(dlg
.lastChild
);
898 dlg
.appendChild(E('pre', [ res
.stdout
]));
901 dlg
.appendChild(E('h5', _('Errors')));
902 dlg
.appendChild(E('pre', { 'class': 'errors' }, [ res
.stderr
]));
906 dlg
.appendChild(E('p', _('The <em>opkg %h</em> command failed with code <code>%d</code>.').format(cmd
, (res
.code
& 0xff) || -1)));
908 dlg
.appendChild(E('div', { 'class': 'right' },
911 'click': L
.bind(function(res
) {
912 if (ui
.menu
&& ui
.menu
.flushCache
)
913 ui
.menu
.flushCache();
919 rejectFn(new Error(res
.stderr
|| 'opkg error %d'.format(res
.code
)));
924 }).catch(function(err
) {
925 ui
.addNotification(null, E('p', _('Unable to execute <em>opkg %s</em> command: %s').format(cmd
, err
)));
931 function handleUpload(ev
)
933 var path
= '/tmp/upload.ipk';
934 return ui
.uploadFile(path
).then(L
.bind(function(btn
, res
) {
935 ui
.showModal(_('Manually install package'), [
936 E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(res
.name
)),
938 res
.size
? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res
.size
)) : '',
939 res
.checksum
? E('li', {}, '%s: %s'.format(_('MD5'), res
.checksum
)) : '',
940 res
.sha256sum
? E('li', {}, '%s: %s'.format(_('SHA256'), res
.sha256sum
)) : ''
942 E('div', { 'class': 'right' }, [
944 'click': function(ev
) {
948 'class': 'btn cbi-button-neutral'
949 }, _('Cancel')), ' ',
951 'class': 'btn cbi-button-action',
952 'data-command': 'install',
953 'data-package': path
,
954 'click': function(ev
) {
955 handleOpkg(ev
).finally(function() {
962 }, this, ev
.target
));
965 function downloadLists()
969 fs
.exec_direct('/usr/libexec/opkg-call', [ 'list-available' ]),
970 fs
.exec_direct('/usr/libexec/opkg-call', [ 'list-installed' ])
974 function updateLists(data
)
976 cbi_update_table('#packages', [],
977 E('div', { 'class': 'spinning' }, _('Loading package information…')));
979 packages
.available
= { providers
: {}, pkgs
: {} };
980 packages
.installed
= { providers
: {}, pkgs
: {} };
982 return (data
? Promise
.resolve(data
) : downloadLists()).then(function(data
) {
983 var pg
= document
.querySelector('.cbi-progressbar'),
984 mount
= L
.toArray(data
[0].filter(function(m
) { return m
.mount
== '/' || m
.mount
== '/overlay' }))
985 .sort(function(a
, b
) { return a
.mount
> b
.mount
})[0] || { size
: 0, free
: 0 };
987 pg
.firstElementChild
.style
.width
= Math
.floor(mount
.size
? ((100 / mount
.size
) * mount
.free
) : 100) + '%';
988 pg
.setAttribute('title', '%s (%.1024mB)'.format(pg
.firstElementChild
.style
.width
, mount
.free
));
990 parseList(data
[1], packages
.available
);
991 parseList(data
[2], packages
.installed
);
993 display(document
.querySelector('input[name="filter"]').value
);
997 var inputTimeout
= null;
999 function handleInput(ev
) {
1000 if (inputTimeout
!== null)
1001 window
.clearTimeout(inputTimeout
);
1003 inputTimeout
= window
.setTimeout(function() {
1004 display(ev
.target
.value
);
1008 return view
.extend({
1010 return downloadLists();
1013 render: function(listData
) {
1014 var query
= decodeURIComponent(L
.toArray(location
.search
.match(/\bquery=([^=]+)\b/))[1] || '');
1017 E('style', { 'type': 'text/css' }, [ css
]),
1019 E('h2', {}, _('Software')),
1021 E('div', { 'class': 'controls' }, [
1023 E('label', {}, _('Free space') + ':'),
1024 E('div', { 'class': 'cbi-progressbar', 'title': _('unknown') }, E('div', {}, [ '\u00a0' ]))
1028 E('label', {}, _('Filter') + ':'),
1029 E('span', { 'class': 'control-group' }, [
1030 E('input', { 'type': 'text', 'name': 'filter', 'placeholder': _('Type to filter…'), 'value': query
, 'input': handleInput
}),
1031 E('button', { 'class': 'btn cbi-button', 'click': handleReset
}, [ _('Clear') ])
1036 E('label', {}, _('Download and install package') + ':'),
1037 E('span', { 'class': 'control-group' }, [
1038 E('input', { 'type': 'text', 'name': 'install', 'placeholder': _('Package name or URL…'), 'keydown': function(ev
) { if (ev
.keyCode
=== 13) handleManualInstall(ev
) }, 'disabled': isReadonlyView
}),
1039 E('button', { 'class': 'btn cbi-button cbi-button-action', 'click': handleManualInstall
, 'disabled': isReadonlyView
}, [ _('OK') ])
1044 E('label', {}, _('Actions') + ':'), ' ',
1045 E('span', { 'class': 'control-group' }, [
1046 E('button', { 'class': 'btn cbi-button-positive', 'data-command': 'update', 'click': handleOpkg
, 'disabled': isReadonlyView
}, [ _('Update lists…') ]), ' ',
1047 E('button', { 'class': 'btn cbi-button-action', 'click': handleUpload
, 'disabled': isReadonlyView
}, [ _('Upload Package…') ]), ' ',
1048 E('button', { 'class': 'btn cbi-button-neutral', 'click': handleConfig
}, [ _('Configure opkg…') ])
1053 E('ul', { 'class': 'cbi-tabmenu mode' }, [
1054 E('li', { 'data-mode': 'available', 'class': 'available cbi-tab', 'click': handleMode
}, E('a', { 'href': '#' }, [ _('Available') ])),
1055 E('li', { 'data-mode': 'installed', 'class': 'installed cbi-tab-disabled', 'click': handleMode
}, E('a', { 'href': '#' }, [ _('Installed') ])),
1056 E('li', { 'data-mode': 'updates', 'class': 'installed cbi-tab-disabled', 'click': handleMode
}, E('a', { 'href': '#' }, [ _('Updates') ]))
1059 E('div', { 'class': 'controls', 'style': 'display:none' }, [
1060 E('div', { 'id': 'pager', 'class': 'center' }, [
1061 E('button', { 'class': 'btn cbi-button-neutral prev', 'aria-label': _('Previous page'), 'click': handlePage
}, [ '«' ]),
1062 E('div', { 'class': 'text' }, [ 'dummy' ]),
1063 E('button', { 'class': 'btn cbi-button-neutral next', 'aria-label': _('Next page'), 'click': handlePage
}, [ '»' ])
1067 E('table', { 'id': 'packages', 'class': 'table' }, [
1068 E('tr', { 'class': 'tr cbi-section-table-titles' }, [
1069 E('th', { 'class': 'th col-2 left' }, [ _('Package name') ]),
1070 E('th', { 'class': 'th col-2 left version' }, [ _('Version') ]),
1071 E('th', { 'class': 'th col-1 center size'}, [ _('Size (.ipk)') ]),
1072 E('th', { 'class': 'th col-10 left' }, [ _('Description') ]),
1073 E('th', { 'class': 'th right cbi-section-actions' }, [ '\u00a0' ])
1078 requestAnimationFrame(function() {
1079 updateLists(listData
)
1086 handleSaveApply
: null,