BangleApps/index.js

586 lines
22 KiB
JavaScript
Raw Normal View History

2019-11-03 11:13:21 +00:00
var appJSON = []; // List of apps and info from apps.json
2019-12-05 14:48:56 +00:00
var appsInstalled = []; // list of app JSON
var files = []; // list of files on Bangle
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-11-03 11:13:21 +00:00
appJSON.sort(appSorter);
2019-10-30 17:33:58 +00:00
refreshLibrary();
});
// Status
// =========================================== Top Navigation
2019-11-03 11:13:21 +00:00
function showToast(message, type) {
2019-10-30 17:33:58 +00:00
// toast-primary, toast-success, toast-warning or toast-error
2019-11-06 17:25:02 +00:00
var style = "toast-primary";
if (type=="success") style = "toast-success";
else if (type=="error") style = "toast-error";
else if (type!==undefined) console.log("showToast: unknown toast "+type);
2019-10-30 17:33:58 +00:00
var toastcontainer = document.getElementById("toastcontainer");
2019-11-06 17:25:02 +00:00
var msgDiv = htmlElement(`<div class="toast ${style}"></div>`);
2019-10-30 17:33:58 +00:00
msgDiv.innerHTML = message;
toastcontainer.append(msgDiv);
setTimeout(function() {
msgDiv.remove();
}, 5000);
}
var progressToast; // the DOM element
var progressSticky; // showProgress(,,"sticky") don't remove until hideProgress("sticky")
var progressInterval; // the interval used if showProgress(..., "animate")
var progressPercent; // the current progress percentage
function showProgress(text, percent, sticky) {
if (sticky=="sticky")
progressSticky = true;
2019-12-05 11:48:56 +00:00
if (!progressToast) {
if (progressInterval) {
clearInterval(progressInterval);
progressInterval = undefined;
}
if (percent == "animate") {
progressInterval = setInterval(function() {
progressPercent += 2;
if (progressPercent>100) progressPercent=0;
showProgress(undefined, progressPercent);
}, 100);
percent = 0;
}
progressPercent = percent;
2019-12-05 11:48:56 +00:00
var toastcontainer = document.getElementById("toastcontainer");
progressToast = htmlElement(`<div class="toast">
${text ? `<div>${text}</div>`:``}
2019-12-05 11:48:56 +00:00
<div class="bar bar-sm">
<div class="bar-item" id="progressToast" role="progressbar" style="width:${percent}%;" aria-valuenow="${percent}" aria-valuemin="0" aria-valuemax="100"></div>
</div>
</div>`);
toastcontainer.append(progressToast);
} else {
var pt=document.getElementById("progressToast");
pt.setAttribute("aria-valuenow",percent);
pt.style.width = percent+"%";
}
}
function hideProgress(sticky) {
if (progressSticky && sticky!="sticky")
return;
progressSticky = false;
if (progressInterval) {
clearInterval(progressInterval);
progressInterval = undefined;
}
if (progressToast) progressToast.remove();
progressToast = undefined;
}
Puck.writeProgress = function(charsSent, charsTotal) {
if (charsSent===undefined) {
hideProgress();
return;
}
var percent = Math.round(charsSent*100/charsTotal);
showProgress(undefined, percent);
}
2020-02-13 10:12:49 +00:00
function showPrompt(title, text, buttons) {
if (!buttons) buttons={yes:1,no:1};
2019-10-30 17:33:58 +00:00
return new Promise((resolve,reject) => {
var modal = htmlElement(`<div class="modal active">
<!--<a href="#close" class="modal-overlay" aria-label="Close"></a>-->
<div class="modal-container">
<div class="modal-header">
<a href="#close" class="btn btn-clear float-right" aria-label="Close"></a>
<div class="modal-title h5">${escapeHtml(title)}</div>
</div>
<div class="modal-body">
<div class="content">
2020-02-13 10:12:49 +00:00
${escapeHtml(text).replace(/\n/g,'<br/>')}
2019-10-30 17:33:58 +00:00
</div>
</div>
<div class="modal-footer">
<div class="modal-footer">
2020-02-13 10:12:49 +00:00
${buttons.yes?'<button class="btn btn-primary" isyes="1">Yes</button>':''}
${buttons.no?'<button class="btn" isyes="0">No</button>':''}
${buttons.ok?'<button class="btn" isyes="1">Ok</button>':''}
2019-10-30 17:33:58 +00:00
</div>
</div>
</div>
</div>`);
document.body.append(modal);
2020-02-13 10:12:49 +00:00
modal.querySelector("a[href='#close']").addEventListener("click",event => {
event.preventDefault();
reject("User cancelled");
2020-02-13 10:12:49 +00:00
modal.remove();
});
2019-10-30 17:33:58 +00:00
htmlToArray(modal.getElementsByTagName("button")).forEach(button => {
button.addEventListener("click",event => {
2019-11-06 17:25:02 +00:00
event.preventDefault();
2019-11-03 11:13:21 +00:00
var isYes = event.target.getAttribute("isyes")=="1";
2019-10-30 17:33:58 +00:00
if (isYes) resolve();
else reject("User cancelled");
2019-10-30 17:33:58 +00:00
modal.remove();
});
});
});
}
2020-02-13 10:12:49 +00:00
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"));
}
2019-11-06 17:25:02 +00:00
function handleCustomApp(app) {
// Pops up an IFRAME that allows an app to be customised
if (!app.custom) throw new Error("App doesn't have custom HTML");
2019-11-06 17:25:02 +00:00
return new Promise((resolve,reject) => {
var modal = htmlElement(`<div class="modal active">
<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)}</div>
</div>
<div class="modal-body" style="height:100%">
<div class="content" style="height:100%">
<iframe src="apps/${app.id}/${app.custom}" style="width:100%;height:100%;border:0px;">
2019-11-06 17:25:02 +00:00
</div>
</div>
</div>
</div>`);
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 app = event.data;
console.log("Received custom app", app);
modal.remove();
showProgress(`Uploading ${app.name}`,undefined,"sticky");
Comms.uploadApp(app).then(()=>{
hideProgress("sticky");
resolve();
}).catch(e => {
hideProgress("sticky");
reject(e);
});
2019-11-06 17:25:02 +00:00
}, 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(`<div class="modal active">
<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)}</div>
</div>
<div class="modal-body" style="height:100%">
<div class="content" style="height:100%">
<iframe style="width:100%;height:100%;border:0px;">
</div>
</div>
</div>
</div>`);
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
});
});
}
}, false);
iwin.postMessage({type:"init"});
};
iframe.src = `apps/${app.id}/${app.interface}`;
});
}
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
var activeFilter = '';
var currentSearch = '';
2019-10-30 17:33:58 +00:00
function refreshLibrary() {
var panelbody = document.querySelector("#librarycontainer .panel-body");
var visibleApps = appJSON;
if (activeFilter) {
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));
}
panelbody.innerHTML = visibleApps.map((app,idx) => {
var appInstalled = appsInstalled.find(a=>a.id==app.id);
var version = getVersionInfo(app, appInstalled);
var versionInfo = version.text;
2019-12-05 14:48:56 +00:00
if (versionInfo) versionInfo = " <small>("+versionInfo+")</small>";
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">
<figure class="avatar"><img src="apps/${app.icon?`${app.id}/${app.icon}`:"unknown.png"}" alt="${escapeHtml(app.name)}"></figure>
2019-10-30 17:33:58 +00:00
</div>
<div class="tile-content">
2019-12-05 14:48:56 +00:00
<p class="tile-title text-bold">${escapeHtml(app.name)} ${versionInfo}</p>
2019-10-30 17:33:58 +00:00
<p class="tile-subtitle">${escapeHtml(app.description)}</p>
</div>
<div class="tile-action">
<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></button>
<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></button>
<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></button>
<button class="btn btn-link btn-action btn-lg ${!appInstalled?"":"d-hide"}" appid="${app.id}" title="Upload App"><i class="icon icon-upload"></i></button>
<button class="btn btn-link btn-action btn-lg ${appInstalled?"":"d-hide"}" appid="${app.id}" title="Remove App"><i class="icon icon-delete"></i></button>
<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></button>
2019-10-30 17:33:58 +00:00
</div>
</div>
2019-11-06 17:25:02 +00:00
`;}).join("");
2019-10-30 17:33:58 +00:00
// set badge up top
var tab = document.querySelector("#tab-librarycontainer a");
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 => {
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
2019-12-03 11:45:55 +00:00
if (icon.classList.contains("icon-share")) {
// emulator
var file = app.storage.find(f=>f.name[0]=='-');
if (!file) {
console.error("No entrypoint found for "+appid);
return;
}
var baseurl = window.location.href;
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
2019-11-03 11:13:21 +00:00
icon.classList.remove("icon-upload");
icon.classList.add("loading");
showProgress(`Uploading ${app.name}`,undefined,"sticky");
2019-12-05 14:48:56 +00:00
Comms.uploadApp(app).then((appJSON) => {
hideProgress("sticky");
2019-12-05 14:48:56 +00:00
if (appJSON) appsInstalled.push(appJSON);
2019-11-03 11:13:21 +00:00
showToast(app.name+" Uploaded!", "success");
icon.classList.remove("loading");
icon.classList.add("icon-delete");
refreshMyApps();
refreshLibrary();
2019-11-03 11:13:21 +00:00
}).catch(err => {
hideProgress("sticky");
2019-11-03 11:13:21 +00:00
showToast("Upload failed, "+err, "error");
icon.classList.remove("loading");
icon.classList.add("icon-upload");
});
2019-11-06 17:25:02 +00:00
} else if (icon.classList.contains("icon-menu")) {
// custom HTML update
2019-11-06 17:25:02 +00:00
if (app.custom) {
icon.classList.remove("icon-menu");
icon.classList.add("loading");
2019-12-05 14:48:56 +00:00
handleCustomApp(app).then((appJSON) => {
if (appJSON) appsInstalled.push(appJSON);
2019-11-06 17:25:02 +00:00
showToast(app.name+" Uploaded!", "success");
icon.classList.remove("loading");
icon.classList.add("icon-delete");
refreshMyApps();
refreshLibrary();
2019-11-06 17:25:02 +00:00
}).catch(err => {
showToast("Customise failed, "+err, "error");
icon.classList.remove("loading");
icon.classList.add("icon-menu");
});
}
} 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);
} 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);
2019-11-03 11:13:21 +00:00
}
2019-10-30 17:33:58 +00:00
});
});
}
refreshLibrary();
// =========================================== My Apps
2019-11-03 11:13:21 +00:00
function removeApp(app) {
return showPrompt("Delete","Really remove '"+app.name+"'?").then(() => {
return Comms.removeApp(app);
}).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 updateApp(app) {
showProgress(`Upgrading ${app.name}`,undefined,"sticky");
Comms.removeApp(app).then(()=>{
showToast(app.name+" removed successfully. Updating...",);
appsInstalled = appsInstalled.filter(a=>a.id!=app.id);
return Comms.uploadApp(app);
}).then((appJSON) => {
hideProgress("sticky");
if (appJSON) appsInstalled.push(appJSON);
showToast(app.name+" Updated!", "success");
refreshMyApps();
refreshLibrary();
}, err=>{
hideProgress("sticky");
showToast(app.name+" update failed, "+err,"error");
2019-11-03 11:13:21 +00:00
});
}
2019-10-30 17:33:58 +00:00
function appNameToApp(appName) {
2019-11-03 11:13:21 +00:00
var 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
/* 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,
2019-10-30 17:33:58 +00:00
name: "Unknown app "+appName,
icon: "../unknown.png",
2019-10-30 17:33:58 +00:00
description: "Unknown app",
2019-11-03 11:13:21 +00:00
storage: [ {name:"+"+appName}],
2019-10-30 17:33:58 +00:00
unknown: true,
};
}
function showLoadingIndicator(id) {
var panelbody = document.querySelector(`#${id} .panel-body`);
var 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
}
function refreshMyApps() {
var panelbody = document.querySelector("#myappscontainer .panel-body");
var tab = document.querySelector("#tab-myappscontainer a");
tab.setAttribute("data-badge", appsInstalled.length);
panelbody.innerHTML = appsInstalled.map(appInstalled => {
var app = appNameToApp(appInstalled.id);
var version = getVersionInfo(app, appInstalled);
2019-12-05 14:48:56 +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">
<figure class="avatar"><img src="apps/${app.icon?`${app.id}/${app.icon}`:"unknown.png"}" alt="${escapeHtml(app.name)}"></figure>
2019-10-30 17:33:58 +00:00
</div>
2019-11-03 11:13:21 +00:00
<div class="tile-content">
<p class="tile-title text-bold">${escapeHtml(app.name)} <small>(${version.text})</small></p>
2019-11-03 11:13:21 +00:00
<p class="tile-subtitle">${escapeHtml(app.description)}</p>
</div>
<div class="tile-action">
<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></button>
<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></button>
<button class="btn btn-link btn-action btn-lg" appid="${app.id}" title="Remove App"><i class="icon icon-delete"></i></button>
2019-11-03 11:13:21 +00:00
</div>
</div>
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 => {
var button = event.currentTarget;
var icon = button.firstChild;
var appid = button.getAttribute("appid");
2019-11-03 11:13:21 +00:00
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-delete")) removeApp(app);
if (icon.classList.contains("icon-refresh")) updateApp(app);
if (icon.classList.contains("icon-download")) handleAppInterface(app)
2019-10-30 17:33:58 +00:00
});
});
}
2019-11-03 11:13:21 +00:00
function getInstalledApps() {
showLoadingIndicator("myappscontainer");
showProgress(`Getting app list...`,undefined,"sticky");
// Get apps and files
return Comms.getInstalledApps()
.then(appJSON => {
hideProgress("sticky");
appsInstalled = appJSON;
refreshMyApps();
refreshLibrary();
})
.then(() => handleConnectionChange(true))
.catch(err=>{
hideProgress("sticky");
return Promise.reject();
});
2019-11-03 11:13:21 +00:00
}
var connectMyDeviceBtn = document.getElementById("connectmydevice");
function handleConnectionChange(connected) {
2019-11-12 14:41:03 +00:00
connectMyDeviceBtn.textContent = connected ? 'Disconnect' : 'Connect';
connectMyDeviceBtn.classList.toggle('is-connected', connected);
}
htmlToArray(document.querySelectorAll(".btn.refresh")).map(button => button.addEventListener("click", () => {
2019-11-12 14:41:03 +00:00
getInstalledApps().catch(err => {
showToast("Getting app list failed, "+err,"error");
});
}));
2019-11-12 14:41:03 +00:00
connectMyDeviceBtn.addEventListener("click", () => {
if (connectMyDeviceBtn.classList.contains('is-connected')) {
Comms.disconnectDevice();
} else {
getInstalledApps().catch(err => {
showToast("Device connection failed, "+err,"error");
});
}
});
Comms.watchConnectionChange(handleConnectionChange);
var filtersContainer = document.querySelector("#librarycontainer .filter-nav");
filtersContainer.addEventListener('click', ({ target }) => {
if (!target.hasAttribute('filterid')) return;
if (target.classList.contains('active')) return;
activeFilter = target.getAttribute('filterid');
filtersContainer.querySelector('.active').classList.remove('active');
target.classList.add('active');
refreshLibrary();
});
var librarySearchInput = document.querySelector("#searchform input");
librarySearchInput.addEventListener('input', evt => {
currentSearch = evt.target.value.toLowerCase();
refreshLibrary();
});
// =========================================== About
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(() => {
showProgress("Removing all apps","animate", "sticky");
2020-02-04 16:30:31 +00:00
return Comms.removeAllApps();
}).then(()=>{
hideProgress("sticky");
2020-02-04 16:30:31 +00:00
appsInstalled = [];
showToast("All apps removed","success");
return getInstalledApps();
}).catch(err=>{
hideProgress("sticky");
2020-02-04 16:30:31 +00:00
showToast("App removal failed, "+err,"error");
});
});
// Install all default apps in one go
document.getElementById("installdefault").addEventListener("click",event=>{
2020-02-04 16:30:31 +00:00
var defaultApps, appCount;
httpGet("defaultapps.json").then(json=>{
defaultApps = JSON.parse(json);
defaultApps = defaultApps.map( appid => appJSON.find(app=>app.id==appid) );
if (defaultApps.some(x=>x===undefined))
throw "Not all apps found";
2020-02-04 16:30:31 +00:00
appCount = defaultApps.length;
return showPrompt("Install Defaults","Remove everything and install default apps?");
}).then(() => {
showProgress("Removing all apps","animate", "sticky");
return Comms.removeAllApps();
}).then(()=>{
hideProgress("sticky");
2020-02-04 16:30:31 +00:00
appsInstalled = [];
showToast(`Existing apps removed. Installing ${appCount} apps...`);
return new Promise((resolve,reject) => {
function upload() {
var app = defaultApps.shift();
if (app===undefined) return resolve();
showProgress(`${app.name} (${appCount-defaultApps.length}/${appCount})`,undefined,"sticky");
Comms.uploadApp(app).then((appJSON) => {
hideProgress("sticky");
if (appJSON) appsInstalled.push(appJSON);
2020-02-04 16:30:31 +00:00
showToast(`(${appCount-defaultApps.length}/${appCount}) ${app.name} Uploaded`);
upload();
}).catch(function() {
hideProgress("sticky");
reject()
});
}
upload();
});
}).then(()=>{
return Comms.setTime();
}).then(()=>{
showToast("Default apps successfully installed!","success");
2020-02-04 16:30:31 +00:00
return getInstalledApps();
}).catch(err=>{
showToast("App Install failed, "+err,"error");
});
});