diff --git a/js/index.js b/js/index.js index 259ee0d..c5681b5 100644 --- a/js/index.js +++ b/js/index.js @@ -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 */ @@ -501,7 +502,9 @@ 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]); @@ -509,11 +512,13 @@ function changeAppFavourite(favourite, app) { 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 => { @@ -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; @@ -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")}"`; @@ -585,12 +603,13 @@ function getAppHTML(app, appInstalled, forInterface) { let githubLink = Const.APP_SOURCECODE_URL ? `See the code on GitHub` : ""; let getAppFavouritesHTML = cnt => { - if (!cnt) return ""; - let txt = (cnt > 999) ? Math.round(cnt/1000)+"k" : cnt; - return `${txt}`; + // 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 `${txt}`; }; - let html = `
+ let html = `
${escapeHtml(app.name)}
@@ -601,8 +620,9 @@ function getAppHTML(app, appInstalled, forInterface) { Copy link to app
`; + html += `
`; if (forInterface=="library") html += ` - + @@ -610,11 +630,11 @@ function getAppHTML(app, appInstalled, forInterface) { `; if (forInterface=="myapps") html += ` - + `; - html += "
"; + html += "
"; if (forInterface=="library") { let screenshots = (app.screenshots || []).filter(s=>s.url); if (screenshots.length) @@ -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"); @@ -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"); @@ -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); } }); }); @@ -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"); @@ -1120,9 +1151,20 @@ 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); } }); }); @@ -1130,7 +1172,8 @@ function refreshMyApps() { 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}`);