Skip to content
107 changes: 75 additions & 32 deletions js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ let device = {
}
};*/
let LANGUAGE = undefined;
const favAnimMS = 500;

/** Ensure we run transfers one after the other rather that potentially having them overlap if the user clicks around
https://github.com/espruino/EspruinoAppLoaderCore/issues/67 */
Expand Down Expand Up @@ -501,19 +502,23 @@ function handleAppInterface(app) {
});
}

function changeAppFavourite(favourite, app) {
function changeAppFavourite(favourite, app,refresh=true) {


if (favourite) {
SETTINGS.appsFavoritedThisSession.push({"id":app.id,"favs":appSortInfo[app.id]&&appSortInfo[app.id].favourites?appSortInfo[app.id].favourites:0});
SETTINGS.favourites = SETTINGS.favourites.concat([app.id]);
} else {
SETTINGS.appsFavoritedThisSession = SETTINGS.appsFavoritedThisSession.filter(obj => obj.id !== app.id);
SETTINGS.favourites = SETTINGS.favourites.filter(e => e != app.id);
}

saveSettings();
refreshLibrary();
refreshMyApps();
if(refresh) {
refreshLibrary();
refreshMyApps();
}
}

// =========================================== Top Navigation
function showTab(tabname) {
htmlToArray(document.querySelectorAll("#tab-navigate .tab-item")).forEach(tab => {
Expand Down Expand Up @@ -543,6 +548,29 @@ librarySearchInput.addEventListener('input', evt => {

// =========================================== App Info




function getAppFavorites(app){
let info = appSortInfo[app.id] || {};
// start with whatever number we have in the database (may be undefined -> treat as 0)
let appFavourites = (typeof info.favourites === 'number') ? info.favourites : 0;
let favsThisSession = SETTINGS.appsFavoritedThisSession.find(obj => obj.id === app.id);
if (favsThisSession) {
// If the database count changed since we recorded the session-favourite, it means
// the server/db has been updated and our optimistic session entry is stale.
if (typeof info.favourites === 'number' && info.favourites !== favsThisSession.favs) {
// remove stale session entry
SETTINGS.appsFavoritedThisSession = SETTINGS.appsFavoritedThisSession.filter(obj => obj.id !== app.id);
} else {
// otherwise include our optimistic +1 so the UI updates immediately
appFavourites += 1;
}
}
return appFavourites;
}


function getAppHTML(app, appInstalled, forInterface) {
let version = getVersionInfo(app, appInstalled);
let versionInfo = version.text;
Expand All @@ -559,21 +587,11 @@ function getAppHTML(app, appInstalled, forInterface) {
infoTxt.push(`${info.installs} reported installs (${percentText})`);
}
if (info.favourites) {
let favsThisSession = SETTINGS.appsFavoritedThisSession.find(obj => obj.id === app.id);
let percent=(info.favourites / info.installs * 100).toFixed(0);
appFavourites = getAppFavorites(app);
let percent=(appFavourites / info.installs * 100).toFixed(0);
let percentText=percent>100?"More than 100% of installs":percent+"% of installs";
if(!info.installs||info.installs<1) {infoTxt.push(`${info.favourites} users favourited`)}
else {infoTxt.push(`${info.favourites} users favourited (${percentText})`)}
appFavourites = info.favourites;
if(favsThisSession){
if(info.favourites!=favsThisSession.favs){
//database has been updated, remove app from favsThisSession
SETTINGS.appsFavoritedThisSession = SETTINGS.appsFavoritedThisSession.filter(obj => obj.id !== app.id);
}
else{
appFavourites += 1; //add one to give the illusion of immediate database changes
}
}
if(!info.installs||info.installs<1) {infoTxt.push(`${appFavourites} users favourited`);}
else {infoTxt.push(`${appFavourites} users favourited (${percentText})`);}
}
if (infoTxt.length)
versionTitle = `title="${infoTxt.join("\n")}"`;
Expand All @@ -585,12 +603,13 @@ function getAppHTML(app, appInstalled, forInterface) {
let githubLink = Const.APP_SOURCECODE_URL ?
`<a href="${Const.APP_SOURCECODE_URL}/${app.id}" target="_blank" class="link-github"><img src="core/img/github-icon-sml.png" alt="See the code on GitHub"/></a>` : "";
let getAppFavouritesHTML = cnt => {
if (!cnt) return "";
let txt = (cnt > 999) ? Math.round(cnt/1000)+"k" : cnt;
return `<span>${txt}</span>`;
// Always show a count (0 if none) and format large numbers with 'k'
let n = (cnt && typeof cnt === 'number') ? cnt : 0;
let txt = (n > 999) ? Math.round(n/100)/10+"k" : n;
return `<span class="fav-count" style="margin-left:-1em;margin-right:0.5em">${txt}</span>`;
};

let html = `<div class="tile column col-6 col-sm-12 col-xs-12 app-tile">
let html = `<div class="tile column col-6 col-sm-12 col-xs-12 app-tile ${version.canUpdate?'updateTile':''}">
<div class="tile-icon">
<figure class="avatar"><img src="apps/${app.icon?`${app.id}/${app.icon}`:"unknown.png"}" alt="${escapeHtml(app.name)}"></figure>
</div>
Expand All @@ -601,20 +620,21 @@ function getAppHTML(app, appInstalled, forInterface) {
<a href="${appurl}" class="link-copy-url" appid="${app.id}" title="Copy link to app" style="position:absolute;top: 56px;left: -24px;"><img src="core/img/copy-icon.png" alt="Copy link to app"/></a>
</div>
<div class="tile-action">`;
html += `<div class="pill-container">`;
if (forInterface=="library") html += `
<button class="btn btn-link btn-action btn-lg btn-favourite" appid="${app.id}" title="Favourite"><i class="icon icon-favourite${favourite?" icon-favourite-active":""}">${getAppFavouritesHTML(appFavourites)}</i></button>
<button class="btn btn-link btn-action btn-lg btn-favourite" appid="${app.id}" title="Favourite">${getAppFavouritesHTML(appFavourites)}<i class="icon icon-favourite${favourite?" icon-favourite-active":""}"></i></button>
<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-interface"></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-emulator"></i></button>
<button class="btn btn-link btn-action btn-lg ${(SETTINGS.alwaysAllowUpdate && appInstalled) || 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 && !app.custom)?"":"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>`;
if (forInterface=="myapps") html += `
<button class="btn btn-link btn-action btn-lg btn-favourite" appid="${app.id}" title="Favourite"><i class="icon icon-favourite${favourite?" icon-favourite-active":""}">${getAppFavouritesHTML(appFavourites)}</i></button>
<button class="btn btn-link btn-action btn-lg btn-favourite" appid="${app.id}" title="Favourite">${getAppFavouritesHTML(appFavourites)}<i class="icon icon-favourite${favourite?" icon-favourite-active":""}"></i></button>
<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-interface"></i></button>
<button class="btn btn-link btn-action btn-lg ${(SETTINGS.alwaysAllowUpdate && appInstalled) || 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>`;
html += "</div>";
html += "</div></div>";
if (forInterface=="library") {
let screenshots = (app.screenshots || []).filter(s=>s.url);
if (screenshots.length)
Expand Down Expand Up @@ -789,7 +809,6 @@ function refreshLibrary(options) {
visibleApps = visibleApps.slice(0, Const.MAX_APPS_SHOWN-1);
}


panelbody.innerHTML = visibleApps.map((app,idx) => {
let appInstalled = device.appsInstalled.find(a=>a.id==app.id);
return getAppHTML(app, appInstalled, "library");
Expand All @@ -801,7 +820,7 @@ function refreshLibrary(options) {
htmlToArray(panelbody.getElementsByTagName("button")).forEach(button => {
button.addEventListener("click",event => {
let button = event.currentTarget;
let icon = button.firstChild;
let icon = (button.querySelector && (button.querySelector('i.icon'))) || button.firstElementChild || button.firstChild;
let appid = button.getAttribute("appid");
let app = appNameToApp(appid);
if (!app) throw new Error("App "+appid+" not found");
Expand Down Expand Up @@ -842,8 +861,20 @@ function refreshLibrary(options) {
if (err != "") showToast("Failed, "+err, "error");
});
} else if ( button.classList.contains("btn-favourite")) {
// clicked: animate and toggle favourite state immediately for instant feedback
let favourite = SETTINGS.favourites.find(e => e == app.id);
changeAppFavourite(!favourite, app);
changeAppFavourite(!favourite, app,false);
if (icon) icon.classList.toggle("icon-favourite-active", !favourite);
if (icon) icon.classList.add("favoriteAnim");
// update visible count optimistically (always update, even if 0)
let cnt = getAppFavorites(app);
let txt = (cnt > 999) ? Math.round(cnt/100)/10+"k" : cnt;
let countEl = button.querySelector('.fav-count');
if (countEl) countEl.textContent = String(txt);
// ensure animation class is removed after the duration so it can be re-triggered
setTimeout(() => {
try { if (icon) icon.classList.remove("favoriteAnim"); } catch (e) { console.error(e); }
}, favAnimMS);
}
});
});
Expand Down Expand Up @@ -1109,7 +1140,7 @@ function refreshMyApps() {
htmlToArray(panelbody.getElementsByTagName("button")).forEach(button => {
button.addEventListener("click",event => {
let button = event.currentTarget;
let icon = button.firstChild;
let icon = (button.querySelector && (button.querySelector('i.icon'))) || button.firstElementChild || button.firstChild;
let appid = button.getAttribute("appid");
let app = appNameToApp(appid);
if (!app) throw new Error("App "+appid+" not found");
Expand All @@ -1120,17 +1151,29 @@ function refreshMyApps() {
handleAppInterface(app).catch( err => {
if (err != "") showToast("Failed, "+err, "error");
});
if (icon.classList.contains("icon-favourite")) {
// handle favourites on My Apps page (button has class btn-favourite)
if (button.classList && button.classList.contains("btn-favourite")) {
let favourite = SETTINGS.favourites.find(e => e == app.id);
changeAppFavourite(!favourite, app);
changeAppFavourite(!favourite, app, false);
if (icon) icon.classList.toggle("icon-favourite-active", !favourite);
if (icon) icon.classList.add("favoriteAnim");
// update visible count optimistically (always update, even if 0)
let cnt = getAppFavorites(app);
let txt = (cnt > 999) ? Math.round(cnt/100)/10+"k" : cnt;
let countEl = button.querySelector('.fav-count');
if (countEl) countEl.textContent = String(txt);
setTimeout(() => {
try { if (icon) icon.classList.remove("favoriteAnim"); } catch (e) {}
}, favAnimMS);
}
});
});
let nonCustomAppsToUpdate = getAppsToUpdate({excludeCustomApps:true});
let tab = document.querySelector("#tab-myappscontainer a");
let updateApps = document.querySelector("#myappscontainer .updateapps");
if (nonCustomAppsToUpdate.length) {
updateApps.innerHTML = `Update ${nonCustomAppsToUpdate.length} apps`;

updateApps.innerHTML = `Update ${nonCustomAppsToUpdate.length} ${nonCustomAppsToUpdate.length>1?"apps":"app"}`;
updateApps.classList.remove("hidden");
updateApps.classList.remove("disabled");
tab.setAttribute("data-badge", `${device.appsInstalled.length} ⬆${nonCustomAppsToUpdate.length}`);
Expand Down