2020-02-24 15:23:49 +00:00
'use strict' ;
2020-04-03 08:00:06 +00:00
'require view' ;
2020-02-24 15:23:49 +00:00
'require fs' ;
'require ui' ;
'require rpc' ;
var css = ' \
. controls { \
display : flex ; \
margin : . 5 em 0 1 em 0 ; \
flex - wrap : wrap ; \
justify - content : space - around ; \
} \
\
. controls > * { \
padding : . 25 em ; \
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 : 250 px ; \
} \
\
. controls > * : nth - child ( 2 ) , \
. controls > * : nth - child ( 3 ) { \
flex - basis : 20 % ; \
} \
\
. controls > * > . btn { \
flex - basis : 20 px ; \
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 : 1 em ; \
} \
\
ul . deps li , ul . errors li { \
list - style : none ; \
} \
\
ul . deps li : before { \
content : "↳" ; \
display : inline - block ; \
width : 1 em ; \
margin - left : - 1 em ; \
} \
\
ul . deps li > span { \
white - space : nowrap ; \
} \
\
ul . errors li { \
color : # c44 ; \
font - size : 90 % ; \
font - weight : bold ; \
padding - left : 1.5 em ; \
} \
\
ul . errors li : before { \
content : "⚠" ; \
display : inline - block ; \
width : 1.5 em ; \
margin - left : - 1.5 em ; \
} \
' ;
2020-04-16 13:30:44 +00:00
var isReadonlyView = ! L . hasViewPermission ( ) || null ;
2020-02-24 15:23:49 +00:00
var callMountPoints = rpc . declare ( {
object : 'luci' ,
method : 'getMountPoints' ,
expect : { result : [ ] }
} ) ;
2018-11-21 19:01:54 +00:00
var packages = {
available : { providers : { } , pkgs : { } } ,
installed : { providers : { } , pkgs : { } }
} ;
var currentDisplayMode = 'available' , currentDisplayRows = [ ] ;
function parseList ( s , dest )
{
var re = /([^\n]*)\n/g ,
pkg = null , key = null , val = null , m ;
while ( ( m = re . exec ( s ) ) !== null ) {
if ( m [ 1 ] . match ( /^\s(.*)$/ ) ) {
if ( pkg !== null && key !== null && val !== null )
val += '\n' + RegExp . $1 . trim ( ) ;
continue ;
}
if ( key !== null && val !== null ) {
switch ( key ) {
case 'package' :
pkg = { name : val } ;
break ;
case 'depends' :
case 'provides' :
var list = val . split ( /\s*,\s*/ ) ;
if ( list . length !== 1 || list [ 0 ] . length > 0 )
pkg [ key ] = list ;
break ;
case 'installed-time' :
pkg . installtime = new Date ( + val * 1000 ) ;
break ;
case 'installed-size' :
pkg . installsize = + val ;
break ;
case 'status' :
var stat = val . split ( /\s+/ ) ,
mode = stat [ 1 ] ,
installed = stat [ 2 ] ;
switch ( mode ) {
case 'user' :
case 'hold' :
pkg [ mode ] = true ;
break ;
}
switch ( installed ) {
case 'installed' :
pkg . installed = true ;
break ;
}
break ;
case 'essential' :
if ( val === 'yes' )
pkg . essential = true ;
break ;
case 'size' :
pkg . size = + val ;
break ;
case 'architecture' :
case 'auto-installed' :
case 'filename' :
case 'sha256sum' :
case 'section' :
break ;
default :
pkg [ key ] = val ;
break ;
}
key = val = null ;
}
if ( m [ 1 ] . trim ( ) . match ( /^([\w-]+)\s*:(.+)$/ ) ) {
key = RegExp . $1 . toLowerCase ( ) ;
val = RegExp . $2 . trim ( ) ;
}
else {
dest . pkgs [ pkg . name ] = pkg ;
var provides = dest . providers [ pkg . name ] ? [ ] : [ pkg . name ] ;
if ( pkg . provides )
provides . push . apply ( provides , pkg . provides ) ;
provides . forEach ( function ( p ) {
dest . providers [ p ] = dest . providers [ p ] || [ ] ;
dest . providers [ p ] . push ( pkg ) ;
} ) ;
}
}
}
function display ( pattern )
{
var src = packages [ currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode ] ,
table = document . querySelector ( '#packages' ) ,
pager = document . querySelector ( '#pager' ) ;
currentDisplayRows . length = 0 ;
if ( typeof ( pattern ) === 'string' && pattern . length > 0 )
pattern = new RegExp ( pattern . replace ( /[.*+?^${}()|[\]\\]/g , '\\$&' ) , 'ig' ) ;
for ( var name in src . pkgs ) {
var pkg = src . pkgs [ name ] ,
desc = pkg . description || '' ,
altsize = null ;
if ( ! pkg . size && packages . available . pkgs [ name ] )
altsize = packages . available . pkgs [ name ] . size ;
if ( ! desc && packages . available . pkgs [ name ] )
desc = packages . available . pkgs [ name ] . description || '' ;
desc = desc . split ( /\n/ ) ;
desc = desc [ 0 ] . trim ( ) + ( desc . length > 1 ? '…' : '' ) ;
if ( ( pattern instanceof RegExp ) &&
! name . match ( pattern ) && ! desc . match ( pattern ) )
continue ;
var btn , ver ;
if ( currentDisplayMode === 'updates' ) {
2019-06-19 09:21:18 +00:00
var avail = packages . available . pkgs [ name ] ,
inst = packages . installed . pkgs [ name ] ;
if ( ! inst || ! inst . installed )
continue ;
2019-01-04 08:15:49 +00:00
if ( ! avail || compareVersion ( avail . version , pkg . version ) <= 0 )
2018-11-21 19:01:54 +00:00
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' ) {
2019-06-19 09:21:18 +00:00
if ( ! pkg . installed )
continue ;
2018-11-21 19:01:54 +00:00
ver = truncateVersion ( pkg . version || '-' ) ;
btn = E ( 'div' , {
'class' : 'btn cbi-button-negative' ,
'data-package' : name ,
'click' : handleRemove
2019-06-25 19:28:44 +00:00
} , _ ( 'Remove…' ) ) ;
2018-11-21 19:01:54 +00:00
}
else {
2019-06-19 09:21:18 +00:00
var inst = packages . installed . pkgs [ name ] ;
2018-11-21 19:01:54 +00:00
ver = truncateVersion ( pkg . version || '-' ) ;
2019-06-19 09:21:18 +00:00
if ( ! inst || ! inst . installed )
2018-11-21 19:01:54 +00:00
btn = E ( 'div' , {
'class' : 'btn cbi-button-action' ,
'data-package' : name ,
'click' : handleInstall
} , _ ( 'Install…' ) ) ;
2019-06-19 09:21:18 +00:00
else if ( inst . installed && inst . version != pkg . version )
2018-11-21 19:01:54 +00:00
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 ? '%.1024mB' . format ( pkg . size )
: ( altsize ? '~%.1024mB' . format ( altsize ) : '-' ) ,
desc ,
btn
] ) ;
}
currentDisplayRows . sort ( function ( a , b ) {
if ( a [ 0 ] < b [ 0 ] )
return - 1 ;
else if ( a [ 0 ] > b [ 0 ] )
return 1 ;
else
return 0 ;
} ) ;
pager . parentNode . style . display = '' ;
pager . setAttribute ( 'data-offset' , 100 ) ;
handlePage ( { target : pager . querySelector ( '.prev' ) } ) ;
}
function handlePage ( ev )
{
var filter = document . querySelector ( 'input[name="filter"]' ) ,
pager = ev . target . parentNode ,
offset = + pager . getAttribute ( 'data-offset' ) ,
next = ev . target . classList . contains ( 'next' ) ;
if ( ( next && ( offset + 100 ) >= currentDisplayRows . length ) ||
( ! next && ( offset < 100 ) ) )
return ;
offset += next ? 100 : - 100 ;
pager . setAttribute ( 'data-offset' , offset ) ;
pager . querySelector ( '.text' ) . firstChild . data = currentDisplayRows . length
? _ ( 'Displaying %d-%d of %d' ) . format ( 1 + offset , Math . min ( offset + 100 , currentDisplayRows . length ) , currentDisplayRows . length )
: _ ( 'No packages' ) ;
if ( offset < 100 )
pager . querySelector ( '.prev' ) . setAttribute ( 'disabled' , 'disabled' ) ;
else
pager . querySelector ( '.prev' ) . removeAttribute ( 'disabled' ) ;
if ( ( offset + 100 ) >= currentDisplayRows . length )
pager . querySelector ( '.next' ) . setAttribute ( 'disabled' , 'disabled' ) ;
else
pager . querySelector ( '.next' ) . removeAttribute ( 'disabled' ) ;
var placeholder = _ ( 'No information available' ) ;
if ( filter . value )
placeholder = [
E ( 'span' , { } , _ ( 'No packages matching "<strong>%h</strong>".' ) . format ( filter . value ) ) , ' (' ,
E ( 'a' , { href : '#' , onclick : 'handleReset(event)' } , _ ( 'Reset' ) ) , ')'
] ;
cbi _update _table ( '#packages' , currentDisplayRows . slice ( offset , offset + 100 ) ,
placeholder ) ;
}
function handleMode ( ev )
{
var tab = findParent ( ev . target , 'li' ) ;
if ( tab . getAttribute ( 'data-mode' ) === currentDisplayMode )
return ;
tab . parentNode . querySelectorAll ( 'li' ) . forEach ( function ( li ) {
li . classList . remove ( 'cbi-tab' ) ;
li . classList . add ( 'cbi-tab-disabled' ) ;
} ) ;
tab . classList . remove ( 'cbi-tab-disabled' ) ;
tab . classList . add ( 'cbi-tab' ) ;
currentDisplayMode = tab . getAttribute ( 'data-mode' ) ;
display ( document . querySelector ( 'input[name="filter"]' ) . value ) ;
ev . target . blur ( ) ;
ev . preventDefault ( ) ;
}
function orderOf ( c )
{
if ( c === '~' )
return - 1 ;
else if ( c === '' || c >= '0' && c <= '9' )
return 0 ;
else if ( ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' ) )
return c . charCodeAt ( 0 ) ;
else
return c . charCodeAt ( 0 ) + 256 ;
}
function compareVersion ( val , ref )
{
var vi = 0 , ri = 0 ,
isdigit = { 0 : 1 , 1 : 1 , 2 : 1 , 3 : 1 , 4 : 1 , 5 : 1 , 6 : 1 , 7 : 1 , 8 : 1 , 9 : 1 } ;
val = val || '' ;
ref = ref || '' ;
2019-01-04 08:15:49 +00:00
if ( val === ref )
return 0 ;
2018-11-21 19:01:54 +00:00
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 )
{
var li = E ( 'li' ) ,
vop = dep . version ? dep . version [ 0 ] : null ,
ver = dep . version ? dep . version [ 1 ] : null ,
depends = [ ] ;
for ( var i = 0 ; dep . pkgs && i < dep . pkgs . length ; i ++ ) {
var pkg = packages . installed . pkgs [ dep . pkgs [ i ] ] ||
packages . available . pkgs [ dep . pkgs [ i ] ] ||
{ name : dep . name } ;
if ( i > 0 )
li . appendChild ( document . createTextNode ( ' | ' ) ) ;
var text = pkg . name ;
if ( pkg . installsize )
text += ' (%.1024mB)' . format ( pkg . installsize ) ;
else if ( pkg . size )
text += ' (~%.1024mB)' . format ( pkg . size ) ;
li . appendChild ( E ( 'span' , { 'data-tooltip' : pkg . description } ,
[ text , ' ' , pkgStatus ( pkg , vop , ver , info ) ] ) ) ;
( pkg . depends || [ ] ) . forEach ( function ( d ) {
if ( depends . indexOf ( d ) === - 1 )
depends . push ( d ) ;
} ) ;
}
if ( ! li . firstChild )
li . appendChild ( E ( 'span' , { } ,
[ dep . name , ' ' ,
pkgStatus ( { name : dep . name , missing : true } , vop , ver , info ) ] ) ) ;
var subdeps = renderDependencies ( depends , info ) ;
if ( subdeps )
li . appendChild ( subdeps ) ;
return li ;
}
function renderDependencies ( depends , info )
{
var deps = depends || [ ] ,
items = [ ] ;
info . seen = info . seen || [ ] ;
for ( var i = 0 ; i < deps . length ; i ++ ) {
2020-02-24 15:23:49 +00:00
var dep , vop , ver ;
2018-11-21 19:01:54 +00:00
if ( deps [ i ] === 'libc' )
continue ;
if ( deps [ i ] . match ( /^(.+)\s+\((<=|<|>|>=|=|<<|>>)(.+)\)$/ ) ) {
dep = RegExp . $1 . trim ( ) ;
vop = RegExp . $2 . trim ( ) ;
ver = RegExp . $3 . trim ( ) ;
}
else {
dep = deps [ i ] . trim ( ) ;
vop = ver = null ;
}
if ( info . seen [ dep ] )
continue ;
var pkgs = [ ] ;
( packages . installed . providers [ dep ] || [ ] ) . forEach ( function ( p ) {
if ( pkgs . indexOf ( p . name ) === - 1 ) pkgs . push ( p . name ) ;
} ) ;
( packages . available . providers [ dep ] || [ ] ) . forEach ( function ( p ) {
if ( pkgs . indexOf ( p . name ) === - 1 ) pkgs . push ( p . name ) ;
} ) ;
info . seen [ dep ] = {
name : dep ,
pkgs : pkgs ,
version : [ vop , ver ]
} ;
items . push ( renderDependencyItem ( info . seen [ dep ] , info ) ) ;
}
if ( items . length )
return E ( 'ul' , { 'class' : 'deps' } , items ) ;
return null ;
}
function truncateVersion ( v , op )
{
v = v . replace ( /\b(([a-f0-9]{8})[a-f0-9]{24,32})\b/ ,
'<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 ;
if ( depcache . install && depcache . install . length )
depcache . install . forEach ( function ( ipkg ) {
totalsize += ipkg . installsize || ipkg . size || 0 ;
totalpkgs ++ ;
} ) ;
inst = E ( 'p' , { } ,
_ ( 'Require approx. %.1024mB size for %d package(s) to install.' )
. format ( totalsize , totalpkgs ) ) ;
if ( deps ) {
tree = E ( 'li' , '<strong>%s:</strong>' . format ( _ ( 'Dependencies' ) ) ) ;
tree . appendChild ( deps ) ;
}
if ( pkg . description ) {
desc = E ( 'div' , { } , [
E ( 'h5' , { } , _ ( 'Description' ) ) ,
E ( 'p' , { } , pkg . description )
] ) ;
}
2020-02-24 15:23:49 +00:00
ui . showModal ( _ ( 'Details for package <em>%h</em>' ) . format ( pkg . name ) , [
2018-11-21 19:01:54 +00:00
E ( 'ul' , { } , [
E ( 'li' , '<strong>%s:</strong> %h' . format ( _ ( 'Version' ) , pkg . version ) ) ,
E ( 'li' , '<strong>%s:</strong> %h' . format ( _ ( 'Size' ) , size ) ) ,
tree || '' ,
] ) ,
desc || '' ,
errs || inst || '' ,
E ( 'div' , { 'class' : 'right' } , [
2020-03-23 21:16:50 +00:00
E ( 'label' , { 'class' : 'cbi-checkbox' , 'style' : 'float:left' } , [
2020-04-16 13:30:44 +00:00
E ( 'input' , { 'id' : 'overwrite-cb' , 'type' : 'checkbox' , 'name' : 'overwrite' , 'disabled' : isReadonlyView } ) , ' ' ,
2020-03-23 21:16:50 +00:00
E ( 'label' , { 'for' : 'overwrite-cb' } ) , ' ' ,
2019-06-19 11:29:19 +00:00
_ ( 'Overwrite files from other package(s)' )
] ) ,
2018-11-21 19:01:54 +00:00
E ( 'div' , {
'class' : 'btn' ,
2020-02-24 15:23:49 +00:00
'click' : ui . hideModal
2018-11-21 19:01:54 +00:00
} , _ ( 'Cancel' ) ) ,
' ' ,
E ( 'div' , {
'data-command' : 'install' ,
'data-package' : name ,
'class' : 'btn cbi-button-action' ,
2020-04-16 13:30:44 +00:00
'click' : handleOpkg ,
'disabled' : isReadonlyView
2018-11-21 19:01:54 +00:00
} , _ ( 'Install' ) )
] )
] ) ;
}
function handleManualInstall ( ev )
{
var name _or _url = document . querySelector ( 'input[name="install"]' ) . value ,
install = E ( 'div' , {
'class' : 'btn cbi-button-action' ,
'data-command' : 'install' ,
'data-package' : name _or _url ,
'click' : function ( ev ) {
document . querySelector ( 'input[name="install"]' ) . value = '' ;
handleOpkg ( ev ) ;
}
} , _ ( 'Install' ) ) , warning ;
if ( ! name _or _url . length ) {
return ;
}
else if ( name _or _url . indexOf ( '/' ) !== - 1 ) {
warning = E ( 'p' , { } , _ ( 'Installing packages from untrusted sources is a potential security risk! Really attempt to install <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 ) ) ;
}
2020-02-24 15:23:49 +00:00
ui . showModal ( _ ( 'Manually install package' ) , [
2018-11-21 19:01:54 +00:00
warning ,
E ( 'div' , { 'class' : 'right' } , [
E ( 'div' , {
2020-02-24 15:23:49 +00:00
'click' : ui . hideModal ,
2018-11-21 19:01:54 +00:00
'class' : 'btn cbi-button-neutral'
} , _ ( 'Cancel' ) ) ,
' ' , install
] )
] ) ;
}
function handleConfig ( ev )
{
2020-02-24 15:23:49 +00:00
var conf = { } ;
ui . showModal ( _ ( 'OPKG Configuration' ) , [
2018-11-21 19:01:54 +00:00
E ( 'p' , { 'class' : 'spinning' } , _ ( 'Loading configuration data…' ) )
] ) ;
2020-02-24 15:23:49 +00:00
fs . list ( '/etc/opkg' ) . then ( function ( partials ) {
var files = [ '/etc/opkg.conf' ] ;
for ( var i = 0 ; i < partials . length ; i ++ )
if ( partials [ i ] . type == 'file' && partials [ i ] . name . match ( /\.conf$/ ) )
files . push ( '/etc/opkg/' + 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 ( ) {
2018-11-21 19:01:54 +00:00
var body = [
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>.' ) )
] ;
Object . keys ( conf ) . sort ( ) . forEach ( function ( file ) {
body . push ( E ( 'h5' , { } , '%h' . format ( file ) ) ) ;
body . push ( E ( 'textarea' , {
'name' : file ,
2020-02-23 20:16:23 +00:00
'rows' : Math . max ( Math . min ( L . toArray ( conf [ file ] . match ( /\n/g ) ) . length , 10 ) , 3 )
2018-11-21 19:01:54 +00:00
} , '%h' . format ( conf [ file ] ) ) ) ;
} ) ;
body . push ( E ( 'div' , { 'class' : 'right' } , [
E ( 'div' , {
'class' : 'btn cbi-button-neutral' ,
2020-02-24 15:23:49 +00:00
'click' : ui . hideModal
2018-11-21 19:01:54 +00:00
} , _ ( '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
} ) ;
2020-02-24 15:23:49 +00:00
ui . showModal ( _ ( 'OPKG Configuration' ) , [
2018-11-21 19:01:54 +00:00
E ( 'p' , { 'class' : 'spinning' } , _ ( 'Saving configuration data…' ) )
] ) ;
2020-02-24 15:23:49 +00:00
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 ) ;
2020-04-16 13:30:44 +00:00
} ,
'disabled' : isReadonlyView
2018-11-21 19:01:54 +00:00
} , _ ( 'Save' ) ) ,
] ) ) ;
2020-02-24 15:23:49 +00:00
ui . showModal ( _ ( 'OPKG Configuration' ) , body ) ;
2018-11-21 19:01:54 +00:00
} ) ;
}
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 )
] ) ;
}
2020-02-24 15:23:49 +00:00
ui . showModal ( _ ( 'Remove package <em>%h</em>' ) . format ( pkg . name ) , [
2018-11-21 19:01:54 +00:00
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' } , [
2020-04-16 13:30:44 +00:00
E ( 'label' , { 'class' : 'cbi-checkbox' , 'style' : 'float:left' } , [
E ( 'input' , { 'id' : 'autoremove-cb' , 'type' : 'checkbox' , 'checked' : 'checked' , 'name' : 'autoremove' , 'disabled' : isReadonlyView } ) , ' ' ,
E ( 'label' , { 'for' : 'autoremove-cb' } ) , ' ' ,
2018-11-21 19:01:54 +00:00
_ ( 'Automatically remove unused dependencies' )
] ) ,
E ( 'div' , { 'style' : 'flex-grow:1' , 'class' : 'right' } , [
E ( 'div' , {
'class' : 'btn' ,
2020-02-24 15:23:49 +00:00
'click' : ui . hideModal
2018-11-21 19:01:54 +00:00
} , _ ( 'Cancel' ) ) ,
' ' ,
E ( 'div' , {
'data-command' : 'remove' ,
'data-package' : name ,
'class' : 'btn cbi-button-negative' ,
2020-04-16 13:30:44 +00:00
'click' : handleOpkg ,
'disabled' : isReadonlyView
2019-06-25 19:28:44 +00:00
} , _ ( 'Remove' ) )
2018-11-21 19:01:54 +00:00
] )
] )
] ) ;
}
function handleOpkg ( ev )
{
2019-10-23 12:22:11 +00:00
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"]' ) ,
2020-02-24 15:23:49 +00:00
owr = document . querySelector ( 'input[name="overwrite"]' ) ;
2019-10-23 12:22:11 +00:00
2020-02-24 15:23:49 +00:00
var dlg = ui . showModal ( _ ( 'Executing package manager' ) , [
2019-10-23 12:22:11 +00:00
E ( 'p' , { 'class' : 'spinning' } ,
_ ( 'Waiting for the <em>opkg %h</em> command to complete…' ) . format ( cmd ) )
] ) ;
2018-11-21 19:01:54 +00:00
2020-03-03 14:19:21 +00:00
var argv = [ cmd , '--force-removal-of-dependent-packages' ] ;
2020-02-24 15:23:49 +00:00
if ( rem && rem . checked )
argv . push ( '--autoremove' ) ;
if ( owr && owr . checked )
argv . push ( '--force-overwrite' ) ;
if ( pkg != null )
argv . push ( pkg ) ;
2020-03-03 14:19:21 +00:00
fs . exec _direct ( '/usr/libexec/opkg-call' , argv , 'json' ) . then ( function ( res ) {
2019-10-23 12:22:11 +00:00
dlg . removeChild ( dlg . lastChild ) ;
2018-11-21 19:01:54 +00:00
2019-10-23 12:22:11 +00:00
if ( res . stdout )
dlg . appendChild ( E ( 'pre' , [ res . stdout ] ) ) ;
2018-11-21 19:01:54 +00:00
2019-10-23 12:22:11 +00:00
if ( res . stderr ) {
dlg . appendChild ( E ( 'h5' , _ ( 'Errors' ) ) ) ;
dlg . appendChild ( E ( 'pre' , { 'class' : 'errors' } , [ res . stderr ] ) ) ;
}
2018-11-21 19:01:54 +00:00
2019-10-23 12:22:11 +00:00
if ( res . code !== 0 )
dlg . appendChild ( E ( 'p' , _ ( 'The <em>opkg %h</em> command failed with code <code>%d</code>.' ) . format ( cmd , ( res . code & 0xff ) || - 1 ) ) ) ;
2018-11-21 19:01:54 +00:00
2019-10-23 12:22:11 +00:00
dlg . appendChild ( E ( 'div' , { 'class' : 'right' } ,
E ( 'div' , {
'class' : 'btn' ,
'click' : L . bind ( function ( res ) {
2020-04-16 13:30:44 +00:00
if ( ui . menu && ui . menu . flushCache )
ui . menu . flushCache ( ) ;
2020-02-24 15:23:49 +00:00
ui . hideModal ( ) ;
2019-10-23 12:22:11 +00:00
updateLists ( ) ;
if ( res . code !== 0 )
rejectFn ( new Error ( res . stderr || 'opkg error %d' . format ( res . code ) ) ) ;
else
resolveFn ( res ) ;
} , this , res )
} , _ ( 'Dismiss' ) ) ) ) ;
2020-02-24 15:23:49 +00:00
} ) . catch ( function ( err ) {
ui . addNotification ( null , E ( 'p' , _ ( 'Unable to execute <em>opkg %s</em> command: %s' ) . format ( cmd , err ) ) ) ;
ui . hideModal ( ) ;
2019-10-23 12:22:11 +00:00
} ) ;
2018-11-21 19:01:54 +00:00
} ) ;
}
2019-10-23 12:22:11 +00:00
function handleUpload ( ev )
{
var path = '/tmp/upload.ipk' ;
2020-02-24 15:23:49 +00:00
return ui . uploadFile ( path ) . then ( L . bind ( function ( btn , res ) {
ui . showModal ( _ ( 'Manually install package' ) , [
2019-10-23 12:22:11 +00:00
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 ) {
2020-02-24 15:23:49 +00:00
ui . hideModal ( ) ;
fs . remove ( path ) ;
2019-10-23 12:22:11 +00:00
} ,
'class' : 'btn cbi-button-neutral'
} , _ ( 'Cancel' ) ) , ' ' ,
E ( 'div' , {
'class' : 'btn cbi-button-action' ,
'data-command' : 'install' ,
'data-package' : path ,
'click' : function ( ev ) {
handleOpkg ( ev ) . finally ( function ( ) {
2020-02-24 15:23:49 +00:00
fs . remove ( path )
2019-10-23 12:22:11 +00:00
} ) ;
}
} , _ ( 'Install' ) )
] )
] ) ;
} , this , ev . target ) ) ;
}
2020-02-24 15:23:49 +00:00
function downloadLists ( )
{
return Promise . all ( [
callMountPoints ( ) ,
2020-03-03 14:19:21 +00:00
fs . exec _direct ( '/usr/libexec/opkg-call' , [ 'list-available' ] ) ,
fs . exec _direct ( '/usr/libexec/opkg-call' , [ 'list-installed' ] )
2020-02-24 15:23:49 +00:00
] ) ;
}
function updateLists ( data )
2018-11-21 19:01:54 +00:00
{
cbi _update _table ( '#packages' , [ ] ,
E ( 'div' , { 'class' : 'spinning' } , _ ( 'Loading package information…' ) ) ) ;
packages . available = { providers : { } , pkgs : { } } ;
packages . installed = { providers : { } , pkgs : { } } ;
2020-02-24 15:23:49 +00:00
return ( data ? Promise . resolve ( data ) : downloadLists ( ) ) . then ( function ( data ) {
2018-11-21 19:01:54 +00:00
var pg = document . querySelector ( '.cbi-progressbar' ) ,
2020-02-24 15:23:49 +00:00
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 } ;
2018-11-21 19:01:54 +00:00
2020-02-24 15:23:49 +00:00
pg . firstElementChild . style . width = Math . floor ( mount . size ? ( ( 100 / mount . size ) * mount . free ) : 100 ) + '%' ;
pg . setAttribute ( 'title' , '%s (%.1024mB)' . format ( pg . firstElementChild . style . width , mount . free ) ) ;
2018-11-21 19:01:54 +00:00
2020-02-24 15:23:49 +00:00
parseList ( data [ 1 ] , packages . available ) ;
parseList ( data [ 2 ] , packages . installed ) ;
display ( document . querySelector ( 'input[name="filter"]' ) . value ) ;
2018-11-21 19:01:54 +00:00
} ) ;
}
2020-02-24 15:23:49 +00:00
var keyTimeout = null ;
function handleKeyUp ( ev ) {
if ( keyTimeout !== null )
window . clearTimeout ( keyTimeout ) ;
keyTimeout = window . setTimeout ( function ( ) {
display ( ev . target . value ) ;
} , 250 ) ;
}
2020-04-03 08:00:06 +00:00
return view . extend ( {
2020-02-24 15:23:49 +00:00
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' : 'controls' } , [
E ( 'div' , { } , [
E ( 'label' , { } , _ ( 'Free space' ) + ':' ) ,
E ( 'div' , { 'class' : 'cbi-progressbar' , 'title' : _ ( 'unknown' ) } , E ( 'div' , { } , [ '\u00a0' ] ) )
] ) ,
E ( 'div' , { } , [
E ( 'label' , { } , _ ( 'Filter' ) + ':' ) ,
2020-03-23 21:16:50 +00:00
E ( 'span' , { 'class' : 'control-group' } , [
E ( 'input' , { 'type' : 'text' , 'name' : 'filter' , 'placeholder' : _ ( 'Type to filter…' ) , 'value' : query , 'keyup' : handleKeyUp } ) ,
E ( 'button' , { 'class' : 'btn cbi-button' , 'click' : handleReset } , [ _ ( 'Clear' ) ] )
] )
2020-02-24 15:23:49 +00:00
] ) ,
E ( 'div' , { } , [
E ( 'label' , { } , _ ( 'Download and install package' ) + ':' ) ,
2020-03-23 21:16:50 +00:00
E ( 'span' , { 'class' : 'control-group' } , [
2020-04-16 13:30:44 +00:00
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' ) ] )
2020-03-23 21:16:50 +00:00
] )
2020-02-24 15:23:49 +00:00
] ) ,
E ( 'div' , { } , [
E ( 'label' , { } , _ ( 'Actions' ) + ':' ) , ' ' ,
2020-03-23 21:16:50 +00:00
E ( 'span' , { 'class' : 'control-group' } , [
2020-04-16 13:30:44 +00:00
E ( 'button' , { 'class' : 'btn cbi-button-positive' , 'data-command' : 'update' , 'click' : handleOpkg , 'disabled' : isReadonlyView } , [ _ ( 'Update lists…' ) ] ) , ' ' ,
E ( 'button' , { 'class' : 'btn cbi-button-action' , 'click' : handleUpload , 'disabled' : isReadonlyView } , [ _ ( 'Upload Package…' ) ] ) , ' ' ,
2020-03-23 21:16:50 +00:00
E ( 'button' , { 'class' : 'btn cbi-button-neutral' , 'click' : handleConfig } , [ _ ( 'Configure opkg…' ) ] )
] )
2020-02-24 15:23:49 +00:00
] )
] ) ,
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' ) ] ) )
] ) ,
2018-11-21 19:01:54 +00:00
2020-02-24 15:23:49 +00:00
E ( 'div' , { 'class' : 'controls' , 'style' : 'display:none' } , [
E ( 'div' , { 'id' : 'pager' , 'class' : '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 ( 'div' , { 'id' : 'packages' , 'class' : 'table' } , [
E ( 'div' , { 'class' : 'tr cbi-section-table-titles' } , [
E ( 'div' , { 'class' : 'th col-2 left' } , [ _ ( 'Package name' ) ] ) ,
E ( 'div' , { 'class' : 'th col-2 left version' } , [ _ ( 'Version' ) ] ) ,
E ( 'div' , { 'class' : 'th col-1 center size' } , [ _ ( 'Size (.ipk)' ) ] ) ,
E ( 'div' , { 'class' : 'th col-10 left' } , [ _ ( 'Description' ) ] ) ,
2020-03-23 21:16:50 +00:00
E ( 'div' , { 'class' : 'th right cbi-section-actions' } , [ '\u00a0' ] )
2020-02-24 15:23:49 +00:00
] )
] )
] ) ;
2018-11-21 19:01:54 +00:00
2020-02-24 15:23:49 +00:00
requestAnimationFrame ( function ( ) {
updateLists ( listData )
2018-11-21 19:01:54 +00:00
} ) ;
2020-02-24 15:23:49 +00:00
return view ;
} ,
2018-11-21 19:01:54 +00:00
2020-02-24 15:23:49 +00:00
handleSave : null ,
handleSaveApply : null ,
handleReset : null
2018-11-21 19:01:54 +00:00
} ) ;