var appJSON = []; // List of apps and info from apps.json var appsInstalled = []; // list of app JSON var files = []; // list of files on Bangle var favourites = []; // list of user favourite app const FAVOURITE = "favouriteapps.json"; httpGet("apps.json").then(apps=>{ try { appJSON = JSON.parse(apps); } catch(e) { console.log(e); showToast("App List Corrupted","error"); } appJSON.sort(appSorter); refreshLibrary(); refreshFilter(); }); // =========================================== Top Navigation function showChangeLog(appid) { var app = appNameToApp(appid); function show(contents) { showPrompt(app.name+" Change Log",contents,{ok:true}).catch(()=>{}); } httpGet(`apps/${appid}/ChangeLog`). then(show).catch(()=>show("No Change Log available")); } function showReadme(appid) { var app = appNameToApp(appid); var appPath = `apps/${appid}/`; var markedOptions = { baseUrl : appPath }; function show(contents) { if (!contents) return; showPrompt(app.name + " Documentation", marked(contents, markedOptions), {ok: true}, false).catch(() => {}); } httpGet(appPath+app.readme).then(show).catch(()=>show("Failed to load README.")); } function handleCustomApp(appTemplate) { // Pops up an IFRAME that allows an app to be customised if (!appTemplate.custom) throw new Error("App doesn't have custom HTML"); return new Promise((resolve,reject) => { var modal = htmlElement(`
`); document.body.append(modal); htmlToArray(modal.getElementsByTagName("a")).forEach(button => { button.addEventListener("click",event => { event.preventDefault(); modal.remove(); reject("Window closed"); }); }); var iframe = modal.getElementsByTagName("iframe")[0]; iframe.contentWindow.addEventListener("message", function(event) { var appFiles = event.data; var app = {}; Object.keys(appTemplate).forEach(k => app[k] = appTemplate[k]); Object.keys(appFiles).forEach(k => app[k] = appFiles[k]); console.log("Received custom app", app); modal.remove(); Comms.uploadApp(app).then(()=>{ Progress.hide({sticky:true}); resolve(); }).catch(e => { Progress.hide({sticky:true}); reject(e); }); }, false); }); } 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) => { var modal = htmlElement(``); document.body.append(modal); htmlToArray(modal.getElementsByTagName("a")).forEach(button => { button.addEventListener("click",event => { event.preventDefault(); modal.remove(); //reject("Window closed"); }); }); var iframe = modal.getElementsByTagName("iframe")[0]; iframe.onload = function() { var iwin = iframe.contentWindow; iwin.addEventListener("message", function(event) { var msg = event.data; if (msg.type=="eval") { Puck.eval(msg.data, function(result) { iwin.postMessage({ type : "evalrsp", data : result, id : msg.id }); }); } else if (msg.type=="write") { Puck.write(msg.data, function(result) { iwin.postMessage({ type : "writersp", data : result, id : msg.id }); }); } else if (msg.type=="readstoragefile") { Comms.readStorageFile(msg.data/*filename*/).then(function(result) { iwin.postMessage({ type : "readstoragefilersp", data : result, id : msg.id }); }); } }, false); iwin.postMessage({type:"init"}); }; iframe.src = `apps/${app.id}/${app.interface}`; }); } function handleAppFavourite(favourite, app){ if (favourite) { favourites = favourites.concat([app.id]); } else { if ([ "boot","setting"].includes(app.id)) { showToast(app.name + ' is required, can\'t remove it' , 'warning'); }else { favourites = favourites.filter(e => e != app.id); } } localStorage.setItem("favouriteapps.json", JSON.stringify(favourites)); refreshLibrary(); } // =========================================== 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 var chips = Array.from(document.querySelectorAll('.chip')).map(chip => chip.attributes.filterid.value); var hash = window.location.hash ? window.location.hash.slice(1) : ''; var activeFilter = !!~chips.indexOf(hash) ? hash : ''; var currentSearch = ''; function refreshFilter(){ var filtersContainer = document.querySelector("#librarycontainer .filter-nav"); filtersContainer.querySelector('.active').classList.remove('active'); if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active'); else filtersContainer.querySelector('.chip[filterid]').classList.add('active'); } function refreshLibrary() { var panelbody = document.querySelector("#librarycontainer .panel-body"); var visibleApps = appJSON; if (activeFilter) { if ( activeFilter == "favourites" ) { visibleApps = visibleApps.filter(app => app.id && (favourites.filter( e => e == app.id).length)); }else{ visibleApps = visibleApps.filter(app => app.tags && app.tags.split(',').includes(activeFilter)); } } if (currentSearch) { visibleApps = visibleApps.filter(app => app.name.toLowerCase().includes(currentSearch) || app.tags.includes(currentSearch)); } favourites = (localStorage.getItem(FAVOURITE)) === null ? JSON.parse('["boot","launch","setting"]') : JSON.parse(localStorage.getItem("favouriteapps.json")); panelbody.innerHTML = visibleApps.map((app,idx) => { var appInstalled = appsInstalled.find(a=>a.id==app.id); var version = getVersionInfo(app, appInstalled); var versionInfo = version.text; if (versionInfo) versionInfo = " ("+versionInfo+")"; var readme = `Read more...`; var favourite = favourites.find(e => e == app.id); return ` `;}).join(""); // set badge up top var tab = document.querySelector("#tab-librarycontainer a"); tab.classList.add("badge"); tab.setAttribute("data-badge", appJSON.length); htmlToArray(panelbody.getElementsByTagName("button")).forEach(button => { button.addEventListener("click",event => { var button = event.currentTarget; var icon = button.firstChild; var appid = button.getAttribute("appid"); var app = appNameToApp(appid); if (!app) throw new Error("App "+appid+" not found"); // check icon to figure out what we should do if (icon.classList.contains("icon-share")) { // emulator var file = app.storage.find(f=>f.name.endsWith('.js')); if (!file) { console.error("No entrypoint found for "+appid); return; } var baseurl = window.location.href; baseurl = baseurl.substr(0,baseurl.lastIndexOf("/")); var url = baseurl+"/apps/"+app.id+"/"+file.url; window.open(`https://espruino.com/ide/emulator.html?codeurl=${url}&upload`); } else if (icon.classList.contains("icon-upload")) { // upload icon.classList.remove("icon-upload"); icon.classList.add("loading"); uploadApp(app); } else if (icon.classList.contains("icon-menu")) { // custom HTML update icon.classList.remove("icon-menu"); icon.classList.add("loading"); customApp(app); } else if (icon.classList.contains("icon-delete")) { // Remove app icon.classList.remove("icon-delete"); icon.classList.add("loading"); removeApp(app); } else if (icon.classList.contains("icon-refresh")) { // Update app icon.classList.remove("icon-refresh"); icon.classList.add("loading"); updateApp(app); } else if (icon.classList.contains("icon-download")) { handleAppInterface(app); } else if ( button.innerText == String.fromCharCode(0x2661)) { handleAppFavourite(true, app); } else if ( button.innerText == String.fromCharCode(0x2665) ) { handleAppFavourite(false, app); } }); }); } refreshFilter(); refreshLibrary(); // =========================================== My Apps function uploadApp(app) { return getInstalledApps().then(()=>{ if (appsInstalled.some(i => i.id === app.id)) { return updateApp(app); } 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(); }); }).catch(err => { showToast("Device connection failed, "+err,"error"); }); } function removeApp(app) { return showPrompt("Delete","Really remove '"+app.name+"'?").then(() => { return getInstalledApps().then(()=>{ // a = from appid.info, app = from apps.json return Comms.removeApp(appsInstalled.find(a => a.id === app.id)); }); }).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"); }); } 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(); }); } function updateApp(app) { if (app.custom) return customApp(app); return getInstalledApps().then(() => { // a = from appid.info, app = from apps.json let remove = appsInstalled.find(a => a.id === app.id); // 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)) .join(','); let data = AppInfo.parseDataString(remove.data) if ('data' in app) { // 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) } remove.data = AppInfo.makeDataString(data) return Comms.removeApp(remove); }).then(()=>{ showToast(`Updating ${app.name}...`); appsInstalled = appsInstalled.filter(a=>a.id!=app.id); return Comms.uploadApp(app); }).then((appJSON) => { if (appJSON) appsInstalled.push(appJSON); showToast(app.name+" Updated!", "success"); refreshMyApps(); refreshLibrary(); }, err=>{ showToast(app.name+" update failed, "+err,"error"); refreshMyApps(); refreshLibrary(); }); } function appNameToApp(appName) { var app = appJSON.find(app=>app.id==appName); if (app) return app; /* If app not known, add just one file which is the JSON - so we'll remove it from the menu but may not get rid of all files. */ return { id: appName, name: "Unknown app "+appName, icon: "../unknown.png", description: "Unknown app", storage: [ {name:appName+".info"}], unknown: true, }; } function showLoadingIndicator(id) { var panelbody = document.querySelector(`#${id} .panel-body`); var tab = document.querySelector(`#tab-${id} a`); // set badge up top tab.classList.add("badge"); tab.setAttribute("data-badge", ""); // Loading indicator panelbody.innerHTML = '${escapeHtml(app.name)} (${version.text})
${escapeHtml(app.description)}