Skip to content
Merged
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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like an exact copy of what's on line 866 - is that right? If so maybe you could pull it out into a function to remove code duplication?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just consolidated it into one handleAppFavorites function. For consistency, would it be preferable if I make 'favorite' 'favourite' to match the existing spelling of things, or does it not matter that much?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I've just done it anyway, it will allow for easier debugging and more consistency in the future...

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