2020-05-28 07:20:41 +00:00
let appJSON = [ ] ; // List of apps and info from apps.json
let appsInstalled = [ ] ; // list of app JSON
let appSortInfo = { } ; // list of data to sort by, from appdates.csv { created, modified }
let files = [ ] ; // list of files on Bangle
let DEFAULTSETTINGS = {
2020-04-29 08:20:18 +00:00
pretokenise : true ,
favourites : [ "boot" , "launch" , "setting" ]
} ;
2020-05-28 07:20:41 +00:00
let SETTINGS = JSON . parse ( JSON . stringify ( DEFAULTSETTINGS ) ) ; // clone
2019-11-03 11:13:21 +00:00
2019-10-30 17:33:58 +00:00
httpGet ( "apps.json" ) . then ( apps => {
2019-11-07 08:43:56 +00:00
try {
appJSON = JSON . parse ( apps ) ;
} catch ( e ) {
console . log ( e ) ;
showToast ( "App List Corrupted" , "error" ) ;
}
2019-10-30 17:33:58 +00:00
refreshLibrary ( ) ;
2020-03-26 01:31:13 +00:00
refreshFilter ( ) ;
2019-10-30 17:33:58 +00:00
} ) ;
2020-05-13 10:25:08 +00:00
httpGet ( "appdates.csv" ) . then ( csv => {
2020-05-12 21:01:22 +00:00
document . querySelector ( ".sort-nav" ) . classList . remove ( "hidden" ) ;
csv . split ( "\n" ) . forEach ( line => {
2020-05-28 07:20:41 +00:00
let l = line . split ( "," ) ;
2020-05-23 12:28:06 +00:00
appSortInfo [ l [ 0 ] ] = {
created : Date . parse ( l [ 1 ] ) ,
2020-05-13 10:25:08 +00:00
modified : Date . parse ( l [ 2 ] )
} ;
2020-05-12 21:01:22 +00:00
} ) ;
} ) . catch ( err => {
console . log ( "No recent.csv - app sort disabled" ) ;
} ) ;
2019-10-30 17:33:58 +00:00
// =========================================== Top Navigation
2020-02-13 10:12:49 +00:00
function showChangeLog ( appid ) {
2020-05-28 07:20:41 +00:00
let app = appNameToApp ( appid ) ;
2020-02-13 10:12:49 +00:00
function show ( contents ) {
2020-04-14 06:52:04 +00:00
showPrompt ( app . name + " Change Log" , contents , { ok : true } ) . catch ( ( ) => { } ) ;
2020-02-13 10:12:49 +00:00
}
httpGet ( ` apps/ ${ appid } /ChangeLog ` ) .
2020-05-27 15:51:14 +00:00
then ( show ) . catch ( ( ) => show ( "No Change Log available" ) ) ;
2020-02-13 10:12:49 +00:00
}
2020-04-06 15:36:18 +00:00
function showReadme ( appid ) {
2020-05-28 07:20:41 +00:00
let app = appNameToApp ( appid ) ;
let appPath = ` apps/ ${ appid } / ` ;
let markedOptions = { baseUrl : appPath } ;
2020-04-06 15:36:18 +00:00
function show ( contents ) {
2020-04-06 15:49:42 +00:00
if ( ! contents ) return ;
2020-04-07 07:59:24 +00:00
showPrompt ( app . name + " Documentation" , marked ( contents , markedOptions ) , { ok : true } , false ) . catch ( ( ) => { } ) ;
2020-04-06 15:36:18 +00:00
}
2020-04-07 07:59:24 +00:00
httpGet ( appPath + app . readme ) . then ( show ) . catch ( ( ) => show ( "Failed to load README." ) ) ;
2020-04-06 15:36:18 +00:00
}
2020-05-28 13:34:40 +00:00
function getAppDescription ( app ) {
let appPath = ` apps/ ${ app . id } / ` ;
let markedOptions = { baseUrl : appPath } ;
return marked ( app . description , markedOptions ) ;
}
2020-02-28 11:44:25 +00:00
function handleCustomApp ( appTemplate ) {
2020-02-07 17:16:45 +00:00
// Pops up an IFRAME that allows an app to be customised
2020-02-28 11:44:25 +00:00
if ( ! appTemplate . custom ) throw new Error ( "App doesn't have custom HTML" ) ;
2019-11-06 17:25:02 +00:00
return new Promise ( ( resolve , reject ) => {
2020-05-28 07:20:41 +00:00
let modal = htmlElement ( ` <div class="modal active">
2019-11-06 17:25:02 +00:00
< a href = "#close" class = "modal-overlay " aria - label = "Close" > < / a >
< div class = "modal-container" style = "height:100%" >
< div class = "modal-header" >
< a href = "#close" class = "btn btn-clear float-right" aria - label = "Close" > < / a >
2020-02-28 11:44:25 +00:00
< div class = "modal-title h5" > $ { escapeHtml ( appTemplate . name ) } < / d i v >
2019-11-06 17:25:02 +00:00
< / d i v >
< div class = "modal-body" style = "height:100%" >
< div class = "content" style = "height:100%" >
2020-02-28 11:44:25 +00:00
< iframe src = "apps/${appTemplate.id}/${appTemplate.custom}" style = "width:100%;height:100%;border:0px;" >
2019-11-06 17:25:02 +00:00
< / d i v >
< / d i v >
< / d i v >
< / d i v > ` ) ;
document . body . append ( modal ) ;
htmlToArray ( modal . getElementsByTagName ( "a" ) ) . forEach ( button => {
button . addEventListener ( "click" , event => {
event . preventDefault ( ) ;
modal . remove ( ) ;
reject ( "Window closed" ) ;
} ) ;
} ) ;
2020-05-28 07:20:41 +00:00
let iframe = modal . getElementsByTagName ( "iframe" ) [ 0 ] ;
2019-11-06 17:25:02 +00:00
iframe . contentWindow . addEventListener ( "message" , function ( event ) {
2020-05-28 07:20:41 +00:00
let appFiles = event . data ;
let app = JSON . parse ( JSON . stringify ( appTemplate ) ) ; // clone template
2020-05-06 08:55:14 +00:00
// copy extra keys from appFiles
Object . keys ( appFiles ) . forEach ( k => {
if ( k != "storage" ) app [ k ] = appFiles [ k ]
} ) ;
appFiles . storage . forEach ( f => {
app . storage = app . storage . filter ( s => s . name != f . name ) ; // remove existing item
app . storage . push ( f ) ; // add new
} ) ;
2019-11-06 17:25:02 +00:00
console . log ( "Received custom app" , app ) ;
modal . remove ( ) ;
2020-06-04 14:19:37 +00:00
checkDependencies ( app )
2020-06-04 14:48:27 +00:00
. then ( ( ) => Comms . uploadApp ( app ) )
. then ( ( ) => {
Progress . hide ( { sticky : true } ) ;
resolve ( ) ;
} ) . catch ( e => {
Progress . hide ( { sticky : true } ) ;
reject ( e ) ;
} ) ;
2019-11-06 17:25:02 +00:00
} , false ) ;
} ) ;
}
2020-02-07 17:16:45 +00:00
function handleAppInterface ( app ) {
// IFRAME interface window that can be used to get data from the app
if ( ! app . interface ) throw new Error ( "App doesn't have interface HTML" ) ;
return new Promise ( ( resolve , reject ) => {
2020-05-28 07:20:41 +00:00
let modal = htmlElement ( ` <div class="modal active">
2020-02-07 17:16:45 +00:00
< a href = "#close" class = "modal-overlay " aria - label = "Close" > < / a >
< div class = "modal-container" style = "height:100%" >
< div class = "modal-header" >
< a href = "#close" class = "btn btn-clear float-right" aria - label = "Close" > < / a >
< div class = "modal-title h5" > $ { escapeHtml ( app . name ) } < / d i v >
< / d i v >
< div class = "modal-body" style = "height:100%" >
< div class = "content" style = "height:100%" >
< iframe style = "width:100%;height:100%;border:0px;" >
< / d i v >
< / d i v >
< / d i v >
< / d i v > ` ) ;
document . body . append ( modal ) ;
htmlToArray ( modal . getElementsByTagName ( "a" ) ) . forEach ( button => {
button . addEventListener ( "click" , event => {
event . preventDefault ( ) ;
modal . remove ( ) ;
//reject("Window closed");
} ) ;
} ) ;
2020-05-28 07:20:41 +00:00
let iframe = modal . getElementsByTagName ( "iframe" ) [ 0 ] ;
2020-02-10 13:49:36 +00:00
iframe . onload = function ( ) {
2020-05-28 07:20:41 +00:00
let iwin = iframe . contentWindow ;
2020-02-10 13:49:36 +00:00
iwin . addEventListener ( "message" , function ( event ) {
2020-05-28 07:20:41 +00:00
let msg = event . data ;
2020-02-10 13:49:36 +00:00
if ( msg . type == "eval" ) {
Puck . eval ( msg . data , function ( result ) {
iwin . postMessage ( {
type : "evalrsp" ,
data : result ,
id : msg . id
} ) ;
2020-02-07 17:16:45 +00:00
} ) ;
2020-02-10 13:49:36 +00:00
} else if ( msg . type == "write" ) {
Puck . write ( msg . data , function ( result ) {
iwin . postMessage ( {
type : "writersp" ,
data : result ,
id : msg . id
} ) ;
2020-02-07 17:16:45 +00:00
} ) ;
2020-03-31 13:39:59 +00:00
} else if ( msg . type == "readstoragefile" ) {
Comms . readStorageFile ( msg . data /*filename*/ ) . then ( function ( result ) {
iwin . postMessage ( {
type : "readstoragefilersp" ,
data : result ,
id : msg . id
} ) ;
} ) ;
2020-02-10 13:49:36 +00:00
}
} , false ) ;
iwin . postMessage ( { type : "init" } ) ;
} ;
iframe . src = ` apps/ ${ app . id } / ${ app . interface } ` ;
2020-02-07 17:16:45 +00:00
} ) ;
}
2020-04-23 14:47:57 +00:00
function changeAppFavourite ( favourite , app ) {
2020-05-28 07:20:41 +00:00
let favourites = SETTINGS . favourites ;
2020-04-14 06:52:04 +00:00
if ( favourite ) {
2020-04-29 08:20:18 +00:00
SETTINGS . favourites = SETTINGS . favourites . concat ( [ app . id ] ) ;
2020-04-14 06:52:04 +00:00
} else {
if ( [ "boot" , "setting" ] . includes ( app . id ) ) {
showToast ( app . name + ' is required, can\'t remove it' , 'warning' ) ;
} else {
2020-04-29 08:20:18 +00:00
SETTINGS . favourites = SETTINGS . favourites . filter ( e => e != app . id ) ;
2020-04-14 06:52:04 +00:00
}
}
2020-04-29 08:20:18 +00:00
saveSettings ( ) ;
2020-04-14 06:52:04 +00:00
refreshLibrary ( ) ;
}
2019-10-30 17:33:58 +00:00
// =========================================== Top Navigation
function showTab ( tabname ) {
htmlToArray ( document . querySelectorAll ( "#tab-navigate .tab-item" ) ) . forEach ( tab => {
tab . classList . remove ( "active" ) ;
} ) ;
htmlToArray ( document . querySelectorAll ( ".bangle-tab" ) ) . forEach ( tab => {
tab . style . display = "none" ;
} ) ;
document . getElementById ( "tab-" + tabname ) . classList . add ( "active" ) ;
document . getElementById ( tabname ) . style . display = "inherit" ;
}
// =========================================== Library
2019-11-13 17:27:22 +00:00
2020-05-23 12:28:06 +00:00
// Can't use chip.attributes.filterid.value here because Safari/Apple's WebView doesn't handle it
2020-05-28 07:20:41 +00:00
let chips = Array . from ( document . querySelectorAll ( '.filter-nav .chip' ) ) . map ( chip => chip . getAttribute ( "filterid" ) ) ;
let hash = window . location . hash ? window . location . hash . slice ( 1 ) : '' ;
2020-03-26 13:39:49 +00:00
2020-05-28 07:20:41 +00:00
let activeFilter = ~ chips . indexOf ( hash ) ? hash : '' ;
let activeSort = '' ;
let currentSearch = activeFilter ? '' : hash ;
2019-11-13 17:27:22 +00:00
2020-03-26 01:31:13 +00:00
function refreshFilter ( ) {
2020-05-28 07:20:41 +00:00
let filtersContainer = document . querySelector ( "#librarycontainer .filter-nav" ) ;
2020-03-26 13:39:49 +00:00
filtersContainer . querySelector ( '.active' ) . classList . remove ( 'active' ) ;
2020-04-14 06:52:04 +00:00
if ( activeFilter ) filtersContainer . querySelector ( '.chip[filterid="' + activeFilter + '"]' ) . classList . add ( 'active' ) ;
else filtersContainer . querySelector ( '.chip[filterid]' ) . classList . add ( 'active' ) ;
2020-03-26 01:31:13 +00:00
}
2020-05-12 21:01:22 +00:00
function refreshSort ( ) {
2020-05-28 07:20:41 +00:00
let sortContainer = document . querySelector ( "#librarycontainer .sort-nav" ) ;
2020-05-12 21:01:22 +00:00
sortContainer . querySelector ( '.active' ) . classList . remove ( 'active' ) ;
if ( activeSort ) sortContainer . querySelector ( '.chip[sortid="' + activeSort + '"]' ) . classList . add ( 'active' ) ;
else sortContainer . querySelector ( '.chip[sortid]' ) . classList . add ( 'active' ) ;
}
2019-10-30 17:33:58 +00:00
function refreshLibrary ( ) {
2020-05-28 07:20:41 +00:00
let panelbody = document . querySelector ( "#librarycontainer .panel-body" ) ;
2020-06-05 10:22:43 +00:00
let visibleApps = appJSON . slice ( ) ; // clone so we don't mess with the original
2020-05-28 07:20:41 +00:00
let favourites = SETTINGS . favourites ;
2019-11-13 17:27:22 +00:00
if ( activeFilter ) {
2020-04-14 06:52:04 +00:00
if ( activeFilter == "favourites" ) {
visibleApps = visibleApps . filter ( app => app . id && ( favourites . filter ( e => e == app . id ) . length ) ) ;
2020-05-12 21:01:22 +00:00
} else {
2020-04-14 06:52:04 +00:00
visibleApps = visibleApps . filter ( app => app . tags && app . tags . split ( ',' ) . includes ( activeFilter ) ) ;
}
2019-11-13 17:27:22 +00:00
}
if ( currentSearch ) {
visibleApps = visibleApps . filter ( app => app . name . toLowerCase ( ) . includes ( currentSearch ) || app . tags . includes ( currentSearch ) ) ;
}
2020-06-05 10:22:43 +00:00
visibleApps . sort ( appSorter ) ;
2020-05-12 21:01:22 +00:00
if ( activeSort ) {
2020-05-13 10:25:08 +00:00
if ( activeSort == "created" || activeSort == "modified" ) {
visibleApps = visibleApps . sort ( ( a , b ) => appSortInfo [ b . id ] [ activeSort ] - appSortInfo [ a . id ] [ activeSort ] ) ;
2020-05-12 21:01:22 +00:00
} else throw new Error ( "Unknown sort type " + activeSort ) ;
}
2019-11-13 17:27:22 +00:00
panelbody . innerHTML = visibleApps . map ( ( app , idx ) => {
2020-05-28 07:20:41 +00:00
let appInstalled = appsInstalled . find ( a => a . id == app . id ) ;
let version = getVersionInfo ( app , appInstalled ) ;
let versionInfo = version . text ;
2019-12-05 14:48:56 +00:00
if ( versionInfo ) versionInfo = " <small>(" + versionInfo + ")</small>" ;
2020-05-28 07:20:41 +00:00
let readme = ` <a class="c-hand" onclick="showReadme(' ${ app . id } ')">Read more...</a> ` ;
let favourite = favourites . find ( e => e == app . id ) ;
2020-05-11 07:28:23 +00:00
2020-05-28 07:20:41 +00:00
let username = "espruino" ;
let githubMatch = window . location . href . match ( /\/(\w+)\.github\.io/ ) ;
2020-05-11 07:28:23 +00:00
if ( githubMatch ) username = githubMatch [ 1 ] ;
2020-05-28 07:20:41 +00:00
let url = ` https://github.com/ ${ username } /BangleApps/tree/master/apps/ ${ app . id } ` ;
2020-05-11 07:28:23 +00:00
2019-11-06 17:25:02 +00:00
return ` <div class="tile column col-6 col-sm-12 col-xs-12">
2019-10-30 17:33:58 +00:00
< div class = "tile-icon" >
2020-03-16 09:04:13 +00:00
< figure class = "avatar" > < img src = "apps/${app.icon?`${app.id}/${app.icon}`:" unknown . png "}" alt = "${escapeHtml(app.name)}" > < /figure><br/ >
2019-10-30 17:33:58 +00:00
< / d i v >
< div class = "tile-content" >
2020-04-06 16:40:17 +00:00
< p class = "tile-title text-bold" > $ { escapeHtml ( app . name ) } $ { versionInfo } < / p >
2020-05-28 13:34:40 +00:00
< p class = "tile-subtitle" > $ { getAppDescription ( app ) } $ { app . readme ? ` <br/> ${ readme } ` : "" } < / p >
2020-05-11 07:28:23 +00:00
< a href = "${url}" target = "_blank" class = "link-github" > < img src = "img/github-icon-sml.png" alt = "See the code on GitHub" / > < / a >
2019-10-30 17:33:58 +00:00
< / d i v >
2020-04-20 09:07:55 +00:00
< div class = "tile-action" >
2020-04-14 06:52:04 +00:00
< button class = "btn btn-link btn-action btn-lg ${!app.custom?" text - error ":" d - hide "}" appid = "${app.id}" title = "Favorite" > < i class = "icon" > < / i > $ { f a v o u r i t e ? " & # x 2 6 6 5 ; " : " & # x 2 6 6 1 ; " } < / b u t t o n >
2020-02-07 17:16:45 +00:00
< button class = "btn btn-link btn-action btn-lg ${(appInstalled&&app.interface)?" ":" d - hide "}" appid = "${app.id}" title = "Download data from app" > < i class = "icon icon-download" > < / i > < / b u t t o n >
2020-02-07 14:51:31 +00:00
< button class = "btn btn-link btn-action btn-lg ${app.allow_emulator?" ":" d - hide "}" appid = "${app.id}" title = "Try in Emulator" > < i class = "icon icon-share" > < / i > < / b u t t o n >
< button class = "btn btn-link btn-action btn-lg ${version.canUpdate?" ":" d - hide "}" appid = "${app.id}" title = "Update App" > < i class = "icon icon-refresh" > < / i > < / b u t t o n >
2020-02-28 11:44:25 +00:00
< button class = "btn btn-link btn-action btn-lg ${(!appInstalled && !app.custom)?" ":" d - hide "}" appid = "${app.id}" title = "Upload App" > < i class = "icon icon-upload" > < / i > < / b u t t o n >
2020-02-07 14:51:31 +00:00
< button class = "btn btn-link btn-action btn-lg ${appInstalled?" ":" d - hide "}" appid = "${app.id}" title = "Remove App" > < i class = "icon icon-delete" > < / i > < / b u t t o n >
< button class = "btn btn-link btn-action btn-lg ${app.custom?" ":" d - hide "}" appid = "${app.id}" title = "Customise and Upload App" > < i class = "icon icon-menu" > < / i > < / b u t t o n >
2019-10-30 17:33:58 +00:00
< / d i v >
< / d i v >
2019-11-06 17:25:02 +00:00
` ;}).join("");
2019-10-30 17:33:58 +00:00
// set badge up top
2020-05-28 07:20:41 +00:00
let tab = document . querySelector ( "#tab-librarycontainer a" ) ;
2019-10-30 17:33:58 +00:00
tab . classList . add ( "badge" ) ;
2019-11-03 11:13:21 +00:00
tab . setAttribute ( "data-badge" , appJSON . length ) ;
2019-10-30 17:33:58 +00:00
htmlToArray ( panelbody . getElementsByTagName ( "button" ) ) . forEach ( button => {
button . addEventListener ( "click" , event => {
2020-05-28 07:20:41 +00:00
let button = event . currentTarget ;
let icon = button . firstChild ;
let appid = button . getAttribute ( "appid" ) ;
let app = appNameToApp ( appid ) ;
2020-02-07 14:51:31 +00:00
if ( ! app ) throw new Error ( "App " + appid + " not found" ) ;
// check icon to figure out what we should do
2019-12-03 11:45:55 +00:00
if ( icon . classList . contains ( "icon-share" ) ) {
// emulator
2020-05-28 07:20:41 +00:00
let file = app . storage . find ( f => f . name . endsWith ( '.js' ) ) ;
2019-12-03 11:45:55 +00:00
if ( ! file ) {
console . error ( "No entrypoint found for " + appid ) ;
return ;
}
2020-05-28 07:20:41 +00:00
let baseurl = window . location . href ;
2020-03-09 16:45:41 +00:00
baseurl = baseurl . substr ( 0 , baseurl . lastIndexOf ( "/" ) ) ;
2020-05-28 07:20:41 +00:00
let url = baseurl + "/apps/" + app . id + "/" + file . url ;
2019-12-03 11:45:55 +00:00
window . open ( ` https://espruino.com/ide/emulator.html?codeurl= ${ url } &upload ` ) ;
} else if ( icon . classList . contains ( "icon-upload" ) ) {
2020-02-07 14:51:31 +00:00
// upload
2019-11-03 11:13:21 +00:00
icon . classList . remove ( "icon-upload" ) ;
icon . classList . add ( "loading" ) ;
2020-04-14 06:52:04 +00:00
uploadApp ( app ) ;
2019-11-06 17:25:02 +00:00
} else if ( icon . classList . contains ( "icon-menu" ) ) {
2020-02-07 14:51:31 +00:00
// custom HTML update
2020-03-31 14:13:25 +00:00
icon . classList . remove ( "icon-menu" ) ;
icon . classList . add ( "loading" ) ;
customApp ( app ) ;
2020-02-07 14:51:31 +00:00
} else if ( icon . classList . contains ( "icon-delete" ) ) {
// Remove app
2019-11-03 11:13:21 +00:00
icon . classList . remove ( "icon-delete" ) ;
icon . classList . add ( "loading" ) ;
removeApp ( app ) ;
2020-02-07 14:51:31 +00:00
} else if ( icon . classList . contains ( "icon-refresh" ) ) {
// Update app
icon . classList . remove ( "icon-refresh" ) ;
icon . classList . add ( "loading" ) ;
updateApp ( app ) ;
2020-02-07 17:16:45 +00:00
} else if ( icon . classList . contains ( "icon-download" ) ) {
handleAppInterface ( app ) ;
2020-04-14 06:52:04 +00:00
} else if ( button . innerText == String . fromCharCode ( 0x2661 ) ) {
2020-05-27 15:51:14 +00:00
changeAppFavourite ( true , app ) ;
2020-04-14 06:52:04 +00:00
} else if ( button . innerText == String . fromCharCode ( 0x2665 ) ) {
2020-05-27 15:51:14 +00:00
changeAppFavourite ( false , app ) ;
2019-11-03 11:13:21 +00:00
}
2019-10-30 17:33:58 +00:00
} ) ;
} ) ;
}
2020-03-26 01:31:13 +00:00
refreshFilter ( ) ;
2019-10-30 17:33:58 +00:00
refreshLibrary ( ) ;
// =========================================== My Apps
2020-04-06 19:37:27 +00:00
function uploadApp ( app ) {
return getInstalledApps ( ) . then ( ( ) => {
if ( appsInstalled . some ( i => i . id === app . id ) ) {
2020-04-14 06:52:04 +00:00
return updateApp ( app ) ;
2020-04-06 19:37:27 +00:00
}
2020-06-04 14:19:37 +00:00
checkDependencies ( app )
2020-06-04 14:48:27 +00:00
. then ( ( ) => Comms . uploadApp ( app ) )
. then ( ( appJSON ) => {
Progress . hide ( { sticky : true } ) ;
if ( appJSON ) {
appsInstalled . push ( appJSON ) ;
}
showToast ( app . name + ' Uploaded!' , 'success' ) ;
} ) . catch ( err => {
Progress . hide ( { sticky : true } ) ;
showToast ( 'Upload failed, ' + err , 'error' ) ;
} ) . finally ( ( ) => {
refreshMyApps ( ) ;
refreshLibrary ( ) ;
} ) ;
2020-04-06 19:37:27 +00:00
} ) . catch ( err => {
showToast ( "Device connection failed, " + err , "error" ) ;
} ) ;
}
2019-11-03 11:13:21 +00:00
function removeApp ( app ) {
2019-11-06 15:53:38 +00:00
return showPrompt ( "Delete" , "Really remove '" + app . name + "'?" ) . then ( ( ) => {
2020-04-06 19:37:27 +00:00
return getInstalledApps ( ) . then ( ( ) => {
// a = from appid.info, app = from apps.json
2020-04-14 06:52:04 +00:00
return Comms . removeApp ( appsInstalled . find ( a => a . id === app . id ) ) ;
} ) ;
2020-02-07 14:51:31 +00:00
} ) . then ( ( ) => {
appsInstalled = appsInstalled . filter ( a => a . id != app . id ) ;
showToast ( app . name + " removed successfully" , "success" ) ;
refreshMyApps ( ) ;
refreshLibrary ( ) ;
} , err => {
showToast ( app . name + " removal failed, " + err , "error" ) ;
} ) ;
}
2020-03-31 14:13:25 +00:00
function customApp ( app ) {
return handleCustomApp ( app ) . then ( ( appJSON ) => {
if ( appJSON ) appsInstalled . push ( appJSON ) ;
showToast ( app . name + " Uploaded!" , "success" ) ;
refreshMyApps ( ) ;
refreshLibrary ( ) ;
} ) . catch ( err => {
showToast ( "Customise failed, " + err , "error" ) ;
refreshMyApps ( ) ;
refreshLibrary ( ) ;
} ) ;
}
2020-06-04 14:19:37 +00:00
/// check for dependencies the app needs and install them if required
function checkDependencies ( app , uploadOptions ) {
2020-06-04 14:48:27 +00:00
let promise = Promise . resolve ( ) ;
2020-06-04 14:19:37 +00:00
if ( app . dependencies ) {
Object . keys ( app . dependencies ) . forEach ( dependency => {
if ( app . dependencies [ dependency ] != "type" )
throw new Error ( "Only supporting dependencies on app types right now" ) ;
console . log ( ` Searching for dependency on app type ' ${ dependency } ' ` ) ;
2020-06-04 14:48:27 +00:00
let found = appsInstalled . find ( app => app . type == dependency ) ;
2020-06-04 14:19:37 +00:00
if ( found )
console . log ( ` Found dependency in installed app ' ${ found . id } ' ` ) ;
else {
2020-06-10 07:27:26 +00:00
let foundApps = appJSON . filter ( app => app . type == dependency ) ;
2020-06-05 10:22:43 +00:00
if ( ! foundApps . length ) throw new Error ( ` Dependency of ' ${ dependency } ' listed, but nothing satisfies it! ` ) ;
console . log ( ` Apps ${ foundApps . map ( f => ` ' ${ f . id } ' ` ) . join ( "/" ) } implement ' ${ dependency } ' ` ) ;
found = foundApps [ 0 ] ; // choose first app in list
2020-06-04 14:19:37 +00:00
console . log ( ` Dependency not installed. Installing app id ' ${ found . id } ' ` ) ;
2020-06-08 09:56:48 +00:00
promise = promise . then ( ( ) => new Promise ( ( resolve , reject ) => {
2020-06-04 14:19:37 +00:00
console . log ( ` Install dependency ' ${ dependency } ':' ${ found . id } ' ` ) ;
2020-06-05 10:22:43 +00:00
return Comms . uploadApp ( found ) . then ( appJSON => {
if ( appJSON ) appsInstalled . push ( appJSON ) ;
} ) ;
2020-06-04 14:19:37 +00:00
} ) ) ;
}
} ) ;
}
return promise ;
}
2020-02-07 14:51:31 +00:00
function updateApp ( app ) {
2020-03-31 14:13:25 +00:00
if ( app . custom ) return customApp ( app ) ;
2020-04-06 19:37:27 +00:00
return getInstalledApps ( ) . then ( ( ) => {
// a = from appid.info, app = from apps.json
2020-04-14 06:52:04 +00:00
let remove = appsInstalled . find ( a => a . id === app . id ) ;
2020-04-06 19:37:27 +00:00
// no need to remove files which will be overwritten anyway
remove . files = remove . files . split ( ',' )
. filter ( f => f !== app . id + '.info' )
. filter ( f => ! app . storage . some ( s => s . name === f ) )
2020-04-14 06:52:04 +00:00
. join ( ',' ) ;
2020-04-15 19:30:44 +00:00
let data = AppInfo . parseDataString ( remove . data )
2020-04-11 22:26:08 +00:00
if ( 'data' in app ) {
2020-04-15 19:30:44 +00:00
// only remove data files which are no longer declared in new app version
const removeData = ( f ) => ! app . data . some ( d => ( d . name || d . wildcard ) === f )
data . dataFiles = data . dataFiles . filter ( removeData )
data . storageFiles = data . storageFiles . filter ( removeData )
2020-04-11 22:26:08 +00:00
}
2020-04-15 19:30:44 +00:00
remove . data = AppInfo . makeDataString ( data )
2020-04-14 06:52:04 +00:00
return Comms . removeApp ( remove ) ;
2020-04-06 19:37:27 +00:00
} ) . then ( ( ) => {
showToast ( ` Updating ${ app . name } ... ` ) ;
2020-02-07 14:51:31 +00:00
appsInstalled = appsInstalled . filter ( a => a . id != app . id ) ;
2020-06-04 14:19:37 +00:00
return checkDependencies ( app ) ;
} ) . then ( ( ) => Comms . uploadApp ( app )
) . then ( ( appJSON ) => {
2020-02-07 14:51:31 +00:00
if ( appJSON ) appsInstalled . push ( appJSON ) ;
showToast ( app . name + " Updated!" , "success" ) ;
refreshMyApps ( ) ;
refreshLibrary ( ) ;
} , err => {
showToast ( app . name + " update failed, " + err , "error" ) ;
2020-03-31 14:13:25 +00:00
refreshMyApps ( ) ;
refreshLibrary ( ) ;
2019-11-03 11:13:21 +00:00
} ) ;
}
2020-02-07 17:16:45 +00:00
2020-03-31 14:13:25 +00:00
2019-10-30 17:33:58 +00:00
function appNameToApp ( appName ) {
2020-05-28 07:20:41 +00:00
let app = appJSON . find ( app => app . id == appName ) ;
2019-10-30 17:33:58 +00:00
if ( app ) return app ;
2019-11-03 11:13:21 +00:00
/ * I f a p p n o t k n o w n , a d d j u s t o n e f i l e
which is the JSON - so we ' ll remove it from
the menu but may not get rid of all files . * /
return { id : appName ,
2019-10-30 17:33:58 +00:00
name : "Unknown app " + appName ,
2019-11-17 22:39:31 +00:00
icon : "../unknown.png" ,
2019-10-30 17:33:58 +00:00
description : "Unknown app" ,
2020-02-28 11:44:25 +00:00
storage : [ { name : appName + ".info" } ] ,
2019-10-30 17:33:58 +00:00
unknown : true ,
} ;
}
2019-12-24 13:47:02 +00:00
function showLoadingIndicator ( id ) {
2020-05-28 07:20:41 +00:00
let panelbody = document . querySelector ( ` # ${ id } .panel-body ` ) ;
let tab = document . querySelector ( ` #tab- ${ id } a ` ) ;
2019-10-30 17:33:58 +00:00
// set badge up top
tab . classList . add ( "badge" ) ;
tab . setAttribute ( "data-badge" , "" ) ;
// Loading indicator
2019-11-03 11:50:00 +00:00
panelbody . innerHTML = '<div class="tile column col-12"><div class="tile-content" style="min-height:48px;"><div class="loading loading-lg"></div></div></div>' ;
2019-11-03 11:13:21 +00:00
}
2020-04-20 09:07:55 +00:00
function getAppsToUpdate ( ) {
2020-05-28 07:20:41 +00:00
let appsToUpdate = [ ] ;
2020-04-20 09:07:55 +00:00
appsInstalled . forEach ( appInstalled => {
2020-05-28 07:20:41 +00:00
let app = appNameToApp ( appInstalled . id ) ;
2020-04-20 09:07:55 +00:00
if ( app . version != appInstalled . version )
appsToUpdate . push ( app ) ;
} ) ;
return appsToUpdate ;
}
2019-11-03 11:13:21 +00:00
function refreshMyApps ( ) {
2020-05-28 07:20:41 +00:00
let panelbody = document . querySelector ( "#myappscontainer .panel-body" ) ;
2020-02-07 17:16:45 +00:00
panelbody . innerHTML = appsInstalled . map ( appInstalled => {
2020-05-28 07:20:41 +00:00
let app = appNameToApp ( appInstalled . id ) ;
let version = getVersionInfo ( app , appInstalled ) ;
let username = "espruino" ;
let githubMatch = window . location . href . match ( /\/(\w+)\.github\.io/ ) ;
2020-05-27 15:51:14 +00:00
if ( githubMatch ) username = githubMatch [ 1 ] ;
2020-05-28 07:20:41 +00:00
let url = ` https://github.com/ ${ username } /BangleApps/tree/master/apps/ ${ app . id } ` ;
2020-05-27 15:51:14 +00:00
return ` <div class="tile column col-6 col-sm-12 col-xs-12">
2019-11-03 11:13:21 +00:00
< div class = "tile-icon" >
2019-11-17 22:39:31 +00:00
< figure class = "avatar" > < img src = "apps/${app.icon?`${app.id}/${app.icon}`:" unknown . png "}" alt = "${escapeHtml(app.name)}" > < / f i g u r e >
2019-10-30 17:33:58 +00:00
< / d i v >
2019-11-03 11:13:21 +00:00
< div class = "tile-content" >
2020-02-07 14:51:31 +00:00
< p class = "tile-title text-bold" > $ { escapeHtml ( app . name ) } < small > ( $ { version . text } ) < / s m a l l > < / p >
2020-05-28 13:34:40 +00:00
< p class = "tile-subtitle" > $ { getAppDescription ( app ) } < / p >
2020-05-23 08:49:54 +00:00
< a href = "${url}" target = "_blank" class = "link-github" > < img src = "img/github-icon-sml.png" alt = "See the code on GitHub" / > < / a >
2019-11-03 11:13:21 +00:00
< / d i v >
< div class = "tile-action" >
2020-02-07 17:16:45 +00:00
< button class = "btn btn-link btn-action btn-lg ${(appInstalled&&app.interface)?" ":" d - hide "}" appid = "${app.id}" title = "Download data from app" > < i class = "icon icon-download" > < / i > < / b u t t o n >
2020-02-07 14:51:31 +00:00
< button class = "btn btn-link btn-action btn-lg ${version.canUpdate?'':'d-hide'}" appid = "${app.id}" title = "Update App" > < i class = "icon icon-refresh" > < / i > < / b u t t o n >
< button class = "btn btn-link btn-action btn-lg" appid = "${app.id}" title = "Remove App" > < i class = "icon icon-delete" > < / i > < / b u t t o n >
2019-11-03 11:13:21 +00:00
< / d i v >
< / d i v >
2019-12-05 14:48:56 +00:00
` }).join("");
2019-11-03 11:13:21 +00:00
htmlToArray ( panelbody . getElementsByTagName ( "button" ) ) . forEach ( button => {
button . addEventListener ( "click" , event => {
2020-05-28 07:20:41 +00:00
let button = event . currentTarget ;
let icon = button . firstChild ;
let appid = button . getAttribute ( "appid" ) ;
let app = appNameToApp ( appid ) ;
2020-02-07 14:51:31 +00:00
if ( ! app ) throw new Error ( "App " + appid + " not found" ) ;
// check icon to figure out what we should do
if ( icon . classList . contains ( "icon-delete" ) ) removeApp ( app ) ;
if ( icon . classList . contains ( "icon-refresh" ) ) updateApp ( app ) ;
2020-04-14 06:52:04 +00:00
if ( icon . classList . contains ( "icon-download" ) ) handleAppInterface ( app ) ;
2019-10-30 17:33:58 +00:00
} ) ;
} ) ;
2020-05-28 07:20:41 +00:00
let appsToUpdate = getAppsToUpdate ( ) ;
let tab = document . querySelector ( "#tab-myappscontainer a" ) ;
let updateApps = document . querySelector ( "#myappscontainer .updateapps" ) ;
2020-04-20 09:07:55 +00:00
if ( appsToUpdate . length ) {
updateApps . innerHTML = ` Update ${ appsToUpdate . length } apps ` ;
updateApps . classList . remove ( "hidden" ) ;
tab . setAttribute ( "data-badge" , ` ${ appsInstalled . length } ⬆ ${ appsToUpdate . length } ` ) ;
} else {
updateApps . classList . add ( "hidden" ) ;
tab . setAttribute ( "data-badge" , appsInstalled . length ) ;
}
2019-10-30 17:33:58 +00:00
}
2020-04-06 19:37:27 +00:00
let haveInstalledApps = false ;
function getInstalledApps ( refresh ) {
if ( haveInstalledApps && ! refresh ) {
2020-04-14 06:52:04 +00:00
return Promise . resolve ( appsInstalled ) ;
2020-04-06 19:37:27 +00:00
}
2019-12-24 13:47:02 +00:00
showLoadingIndicator ( "myappscontainer" ) ;
// Get apps and files
return Comms . getInstalledApps ( )
. then ( appJSON => {
appsInstalled = appJSON ;
2020-04-06 19:37:27 +00:00
haveInstalledApps = true ;
2019-12-24 13:47:02 +00:00
refreshMyApps ( ) ;
refreshLibrary ( ) ;
} )
2020-02-27 15:52:20 +00:00
. then ( ( ) => handleConnectionChange ( true ) )
2020-04-06 19:37:27 +00:00
. then ( ( ) => appsInstalled )
2020-02-27 15:52:20 +00:00
. catch ( err => {
return Promise . reject ( ) ;
} ) ;
2019-11-03 11:13:21 +00:00
}
2020-04-23 14:47:57 +00:00
/// Removes everything and install the given apps, eg: installMultipleApps(["boot","mclock"], "minimal")
function installMultipleApps ( appIds , promptName ) {
2020-05-28 07:20:41 +00:00
let apps = appIds . map ( appid => appJSON . find ( app => app . id == appid ) ) ;
2020-04-23 14:47:57 +00:00
if ( apps . some ( x => x === undefined ) )
return Promise . reject ( "Not all apps found" ) ;
2020-05-28 07:20:41 +00:00
let appCount = apps . length ;
2020-04-23 14:47:57 +00:00
return showPrompt ( "Install Defaults" , ` Remove everything and install ${ promptName } apps? ` ) . then ( ( ) => {
return Comms . removeAllApps ( ) ;
} ) . then ( ( ) => {
Progress . hide ( { sticky : true } ) ;
appsInstalled = [ ] ;
showToast ( ` Existing apps removed. Installing ${ appCount } apps... ` ) ;
return new Promise ( ( resolve , reject ) => {
function upload ( ) {
2020-05-28 07:20:41 +00:00
let app = apps . shift ( ) ;
2020-04-23 14:47:57 +00:00
if ( app === undefined ) return resolve ( ) ;
Progress . show ( { title : ` ${ app . name } ( ${ appCount - apps . length } / ${ appCount } ) ` , sticky : true } ) ;
2020-06-04 14:19:37 +00:00
checkDependencies ( app , "skip_reset" )
2020-06-04 14:48:27 +00:00
. then ( ( ) => Comms . uploadApp ( app , "skip_reset" ) )
. then ( ( appJSON ) => {
Progress . hide ( { sticky : true } ) ;
if ( appJSON ) appsInstalled . push ( appJSON ) ;
showToast ( ` ( ${ appCount - apps . length } / ${ appCount } ) ${ app . name } Uploaded ` ) ;
upload ( ) ;
} ) . catch ( function ( ) {
Progress . hide ( { sticky : true } ) ;
reject ( ) ;
} ) ;
2020-04-23 14:47:57 +00:00
}
upload ( ) ;
} ) ;
} ) . then ( ( ) => {
return Comms . setTime ( ) ;
} ) . then ( ( ) => {
showToast ( "Apps successfully installed!" , "success" ) ;
return getInstalledApps ( true ) ;
} ) ;
}
2020-05-28 07:20:41 +00:00
let connectMyDeviceBtn = document . getElementById ( "connectmydevice" ) ;
2019-11-12 10:56:31 +00:00
function handleConnectionChange ( connected ) {
2019-11-12 14:41:03 +00:00
connectMyDeviceBtn . textContent = connected ? 'Disconnect' : 'Connect' ;
connectMyDeviceBtn . classList . toggle ( 'is-connected' , connected ) ;
2019-11-12 10:56:31 +00:00
}
2019-12-24 13:47:02 +00:00
htmlToArray ( document . querySelectorAll ( ".btn.refresh" ) ) . map ( button => button . addEventListener ( "click" , ( ) => {
2020-04-06 19:37:27 +00:00
getInstalledApps ( true ) . catch ( err => {
2019-11-12 14:41:03 +00:00
showToast ( "Getting app list failed, " + err , "error" ) ;
} ) ;
2019-12-24 13:47:02 +00:00
} ) ) ;
2020-04-20 09:07:55 +00:00
htmlToArray ( document . querySelectorAll ( ".btn.updateapps" ) ) . map ( button => button . addEventListener ( "click" , ( ) => {
2020-05-28 07:20:41 +00:00
let appsToUpdate = getAppsToUpdate ( ) ;
let count = appsToUpdate . length ;
2020-04-20 09:07:55 +00:00
function updater ( ) {
if ( ! appsToUpdate . length ) return ;
2020-05-28 07:20:41 +00:00
let app = appsToUpdate . pop ( ) ;
2020-04-20 09:07:55 +00:00
return updateApp ( app ) . then ( function ( ) {
return updater ( ) ;
} ) ;
}
updater ( ) . then ( err => {
showToast ( ` Updated ${ count } apps ` , "success" ) ;
} ) . catch ( err => {
showToast ( "Update failed, " + err , "error" ) ;
} ) ;
} ) ) ;
2019-11-12 14:41:03 +00:00
connectMyDeviceBtn . addEventListener ( "click" , ( ) => {
if ( connectMyDeviceBtn . classList . contains ( 'is-connected' ) ) {
Comms . disconnectDevice ( ) ;
} else {
2020-04-06 19:37:27 +00:00
getInstalledApps ( true ) . catch ( err => {
2019-11-12 14:41:03 +00:00
showToast ( "Device connection failed, " + err , "error" ) ;
} ) ;
}
} ) ;
2019-11-12 10:56:31 +00:00
Comms . watchConnectionChange ( handleConnectionChange ) ;
2019-11-07 09:26:46 +00:00
2020-05-28 07:20:41 +00:00
let filtersContainer = document . querySelector ( "#librarycontainer .filter-nav" ) ;
2019-11-13 17:27:22 +00:00
filtersContainer . addEventListener ( 'click' , ( { target } ) => {
if ( target . classList . contains ( 'active' ) ) return ;
2019-11-17 22:39:31 +00:00
2020-03-26 13:39:49 +00:00
activeFilter = target . getAttribute ( 'filterid' ) || '' ;
2020-03-26 01:31:13 +00:00
refreshFilter ( ) ;
2019-11-13 17:27:22 +00:00
refreshLibrary ( ) ;
2020-04-14 06:52:04 +00:00
window . location . hash = activeFilter ;
2019-11-13 17:27:22 +00:00
} ) ;
2020-05-28 07:20:41 +00:00
let librarySearchInput = document . querySelector ( "#searchform input" ) ;
2020-05-12 21:01:22 +00:00
librarySearchInput . value = currentSearch ;
2019-11-17 22:39:31 +00:00
librarySearchInput . addEventListener ( 'input' , evt => {
2019-11-13 17:27:22 +00:00
currentSearch = evt . target . value . toLowerCase ( ) ;
2019-11-17 22:39:31 +00:00
refreshLibrary ( ) ;
2019-11-13 17:27:22 +00:00
} ) ;
2020-05-28 07:20:41 +00:00
let sortContainer = document . querySelector ( "#librarycontainer .sort-nav" ) ;
2020-05-12 21:01:22 +00:00
sortContainer . addEventListener ( 'click' , ( { target } ) => {
if ( target . classList . contains ( 'active' ) ) return ;
activeSort = target . getAttribute ( 'sortid' ) || '' ;
refreshSort ( ) ;
refreshLibrary ( ) ;
window . location . hash = activeFilter ;
} ) ;
2019-11-07 09:26:46 +00:00
// =========================================== About
2020-04-08 07:53:40 +00:00
if ( window . location . host == "banglejs.com" ) {
document . getElementById ( "apploaderlinks" ) . innerHTML =
'This is the official Bangle.js App Loader - you can also try the <a href="https://espruino.github.io/BangleApps/">Development Version</a> for the most recent apps.' ;
} else if ( window . location . host == "espruino.github.io" ) {
document . title += " [Development]" ;
document . getElementById ( "apploaderlinks" ) . innerHTML =
'This is the development Bangle.js App Loader - you can also try the <a href="https://banglejs.com/apps/">Official Version</a> for stable apps.' ;
} else {
document . title += " [Unofficial]" ;
document . getElementById ( "apploaderlinks" ) . innerHTML =
'This is not the official Bangle.js App Loader - you can try the <a href="https://banglejs.com/apps/">Official Version</a> here.' ;
}
2020-04-29 08:20:18 +00:00
// Settings
2020-05-28 07:20:41 +00:00
let SETTINGS _HOOKS = { } ; // stuff to get called when a setting is loaded
2020-04-29 08:20:18 +00:00
/// Load settings and update controls
function loadSettings ( ) {
2020-05-28 07:20:41 +00:00
let j = localStorage . getItem ( "settings" ) ;
2020-04-29 08:20:18 +00:00
if ( typeof j != "string" ) return ;
try {
2020-05-28 07:20:41 +00:00
let s = JSON . parse ( j ) ;
2020-04-29 08:20:18 +00:00
Object . keys ( s ) . forEach ( k => {
SETTINGS [ k ] = s [ k ] ;
if ( SETTINGS _HOOKS [ k ] ) SETTINGS _HOOKS [ k ] ( ) ;
} ) ;
} catch ( e ) {
console . error ( "Invalid settings" ) ;
}
}
/// Save settings
function saveSettings ( ) {
localStorage . setItem ( "settings" , JSON . stringify ( SETTINGS ) ) ;
console . log ( "Changed settings" , SETTINGS ) ;
}
// Link in settings DOM elements
function settingsCheckbox ( id , name ) {
2020-05-28 07:20:41 +00:00
let setting = document . getElementById ( id ) ;
2020-04-29 08:20:18 +00:00
function update ( ) {
setting . checked = SETTINGS [ name ] ;
}
SETTINGS _HOOKS [ name ] = update ;
setting . addEventListener ( 'click' , function ( ) {
SETTINGS [ name ] = setting . checked ;
saveSettings ( ) ;
} ) ;
}
settingsCheckbox ( "settings-pretokenise" , "pretokenise" ) ;
loadSettings ( ) ;
document . getElementById ( "defaultsettings" ) . addEventListener ( "click" , event => {
SETTINGS = JSON . parse ( JSON . stringify ( DEFAULTSETTINGS ) ) ; // clone
saveSettings ( ) ;
loadSettings ( ) ; // update all settings
refreshLibrary ( ) ; // favourites were in settings
} ) ;
2019-11-07 09:26:46 +00:00
document . getElementById ( "settime" ) . addEventListener ( "click" , event => {
Comms . setTime ( ) . then ( ( ) => {
showToast ( "Time set successfully" , "success" ) ;
} , err => {
showToast ( "Error setting time, " + err , "error" ) ;
} ) ;
} ) ;
document . getElementById ( "removeall" ) . addEventListener ( "click" , event => {
showPrompt ( "Remove All" , "Really remove all apps?" ) . then ( ( ) => {
2020-02-04 16:30:31 +00:00
return Comms . removeAllApps ( ) ;
} ) . then ( ( ) => {
2020-04-03 13:27:45 +00:00
Progress . hide ( { sticky : true } ) ;
2020-02-04 16:30:31 +00:00
appsInstalled = [ ] ;
showToast ( "All apps removed" , "success" ) ;
2020-04-06 19:37:27 +00:00
return getInstalledApps ( true ) ;
2020-02-04 16:30:31 +00:00
} ) . catch ( err => {
2020-04-03 13:27:45 +00:00
Progress . hide ( { sticky : true } ) ;
2020-02-04 16:30:31 +00:00
showToast ( "App removal failed, " + err , "error" ) ;
2019-11-07 09:26:46 +00:00
} ) ;
} ) ;
2020-02-04 16:13:06 +00:00
// Install all default apps in one go
document . getElementById ( "installdefault" ) . addEventListener ( "click" , event => {
httpGet ( "defaultapps.json" ) . then ( json => {
2020-04-23 14:47:57 +00:00
return installMultipleApps ( JSON . parse ( json ) , "default" ) ;
2020-02-04 16:13:06 +00:00
} ) . catch ( err => {
2020-04-03 13:27:45 +00:00
Progress . hide ( { sticky : true } ) ;
2020-02-04 16:13:06 +00:00
showToast ( "App Install failed, " + err , "error" ) ;
} ) ;
} ) ;
2020-04-14 06:52:04 +00:00
2020-04-29 08:20:18 +00:00
// Install all favourite apps in one go
2020-04-14 06:52:04 +00:00
document . getElementById ( "installfavourite" ) . addEventListener ( "click" , event => {
2020-05-28 07:20:41 +00:00
let favApps = SETTINGS . favourites ;
2020-04-23 14:47:57 +00:00
installMultipleApps ( favApps , "favourite" ) . catch ( err => {
2020-04-14 06:52:04 +00:00
Progress . hide ( { sticky : true } ) ;
showToast ( "App Install failed, " + err , "error" ) ;
} ) ;
} ) ;