BangleApps/index.js

435 lines
15 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);
}
2019-12-05 11:48:56 +00:00
var progressToast;
Puck.writeProgress = function(charsSent, charsTotal) {
if (charsSent===undefined) {
if (progressToast) progressToast.remove();
progressToast = undefined;
return;
}
var percent = Math.round(charsSent*100/charsTotal);
if (!progressToast) {
var toastcontainer = document.getElementById("toastcontainer");
progressToast = htmlElement(`<div class="toast">
<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+"%";
}
}
2019-10-30 17:33:58 +00:00
function showPrompt(title, text) {
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">
${escapeHtml(text)}
</div>
</div>
<div class="modal-footer">
<div class="modal-footer">
<button class="btn btn-primary" isyes="1">Yes</button>
<button class="btn" isyes="0">No</button>
</div>
</div>
</div>
</div>`);
document.body.append(modal);
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();
modal.remove();
});
});
});
}
2019-11-06 17:25:02 +00:00
function handleCustomApp(app) {
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();
Comms.uploadApp(app).then(resolve,reject);
}, false);
});
}
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) => {
2019-11-06 17:25:02 +00:00
var icon = "icon-upload";
2019-12-05 14:48:56 +00:00
var versionInfo = app.version || "";
2019-11-06 17:25:02 +00:00
if (app.custom)
icon = "icon-menu";
2019-12-05 14:48:56 +00:00
if (appsInstalled.find(a=>a.id==app.id)) {
2019-11-06 17:25:02 +00:00
icon = "icon-delete";
2019-12-05 14:48:56 +00:00
versionInfo+=" installed";
}
2019-12-03 11:45:55 +00:00
var buttons = "";
2019-12-05 14:48:56 +00:00
if (versionInfo) versionInfo = " <small>("+versionInfo+")</small>";
2019-12-03 11:45:55 +00:00
if (app.allow_emulator)
buttons += `<button class="btn btn-link btn-action btn-lg" title="Try in Emulator"><i class="icon icon-share" appid="${app.id}"></i></button>`;
buttons += `<button class="btn btn-link btn-action btn-lg"><i class="icon ${icon}" appid="${app.id}"></i></button>`;
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">
2019-12-03 11:45:55 +00:00
${buttons}
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 icon = event.target;
var appid = icon.getAttribute("appid");
2019-11-03 11:13:21 +00:00
var app = appJSON.find(app=>app.id==appid);
2019-10-30 17:33:58 +00:00
if (!app) return;
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")) {
2019-11-03 11:13:21 +00:00
icon.classList.remove("icon-upload");
icon.classList.add("loading");
2019-12-05 14:48:56 +00:00
Comms.uploadApp(app).then((appJSON) => {
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();
}).catch(err => {
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")) {
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();
}).catch(err => {
showToast("Customise failed, "+err, "error");
icon.classList.remove("loading");
icon.classList.add("icon-menu");
});
}
2019-11-03 11:13:21 +00:00
} else {
icon.classList.remove("icon-delete");
icon.classList.add("loading");
removeApp(app);
}
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(() => {
2019-11-03 11:13:21 +00:00
Comms.removeApp(app).then(()=>{
2019-12-05 14:48:56 +00:00
appsInstalled = appsInstalled.filter(a=>a.id!=app.id);
2019-11-03 11:13:21 +00:00
showToast(app.name+" removed successfully","success");
refreshMyApps();
refreshLibrary();
}, err=>{
showToast(app.name+" removal failed, "+err,"error");
});
});
}
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);
2019-12-05 14:48:56 +00:00
panelbody.innerHTML = appsInstalled.map(appJSON => {
var app = appNameToApp(appJSON.id);
var version = "";
if (!appJSON.version) {
version = "Unknown version";
if (app.version) version += ", latest "+app.version;
} else {
version = appJSON.version;
if (app.version == appJSON.version) version += ", up to date";
else if (app.version) version += ", latest "+app.version;
}
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">
2019-12-05 14:48:56 +00:00
<p class="tile-title text-bold">${escapeHtml(app.name)} <small>(${version})</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"><i class="icon icon-delete" appid="${app.id}"></i></button>
</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 icon = event.target;
var appid = icon.getAttribute("appid");
var app = appNameToApp(appid);
removeApp(app);
2019-10-30 17:33:58 +00:00
});
});
}
2019-11-03 11:13:21 +00:00
function getInstalledApps() {
showLoadingIndicator("myappscontainer");
showLoadingIndicator("myfscontainer");
// Get apps and files
return Comms.getInstalledApps()
.then(appJSON => {
appsInstalled = appJSON;
refreshMyApps();
refreshLibrary();
})
.then(Comms.listFiles)
.then(list => {
files = list;
refreshMyFS();
})
.then(() => handleConnectionChange(true));
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();
});
// =========================================== My Files
function refreshMyFS() {
var panelbody = document.querySelector("#myfscontainer .panel-body");
var tab = document.querySelector("#tab-myfscontainer a");
tab.setAttribute("data-badge", files.length);
panelbody.innerHTML = `
<thead>
<tr>
<th>Name</th>
<th>Type</th>
</tr>
</thead>
<tbody>${
files.map(file =>
`<tr data-name="${file}"><td>${escapeHtml(file)}</td><td>${fileType(file).name}</td></li>`
).join("")}
</tbody>`;
htmlToArray(panelbody.getElementsByTagName("tr")).forEach(row => {
row.addEventListener("click",event => {
var name = event.target.closest('tr').dataset.name;
const type = fileType(name);
Comms.readFile(name).then(content => content.length && saveAs(new Blob([content], type), name));
});
});
}
function fileType(file) {
switch (file[0]) {
case "+": return { name: "App descriptor", type: "application/json;charset=utf-8" };
case "*": return { name: "App icon", type: "text/plain;charset=utf-8" };
case "-": return { name: "App code", type: "application/javascript;charset=utf-8" };
case "=": return { name: "Boot-time code", type: "application/javascript;charset=utf-8" };
default: return { name: "Plain", type: "text/plain;charset=utf-8" };
}
}
// =========================================== 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(() => {
Comms.removeAllApps().then(()=>{
appsInstalled = [];
showToast("All apps removed","success");
refreshMyApps();
refreshLibrary();
}, err=>{
showToast("App removal failed, "+err,"error");
});
});
});