diff --git a/js/index.js b/js/index.js index 259ee0d..00f2b66 100644 --- a/js/index.js +++ b/js/index.js @@ -1,3 +1,4 @@ +//FAV WITH PILL AND ANIM let appJSON = []; // List of apps and info from apps.json let appSortInfo = {}; // list of data to sort by, from appdates.csv { created, modified } let appCounts = {}; @@ -501,7 +502,7 @@ 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]); @@ -510,10 +511,11 @@ function changeAppFavourite(favourite, app) { 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 +545,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 +584,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,9 +600,10 @@ 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/1000)+"k" : n; + return `${txt}`; }; let html = `
@@ -601,8 +617,9 @@ function getAppHTML(app, appInstalled, forInterface) { Copy link to app
`; + html += `
`; if (forInterface=="library") html += ` - + @@ -610,11 +627,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 +806,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 +817,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 +858,21 @@ 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/1000)+"k" : cnt; + let countEl = button.querySelector('.fav-count'); + if (countEl) countEl.textContent = String(txt); + const ANIM_MS = 500; + // ensure animation class is removed after the duration so it can be re-triggered + setTimeout(() => { + try { if (icon) icon.classList.remove("favoriteAnim"); } catch (e) {} + }, ANIM_MS); } }); }); @@ -1109,7 +1138,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,10 +1149,6 @@ function refreshMyApps() { handleAppInterface(app).catch( err => { if (err != "") showToast("Failed, "+err, "error"); }); - if (icon.classList.contains("icon-favourite")) { - let favourite = SETTINGS.favourites.find(e => e == app.id); - changeAppFavourite(!favourite, app); - } }); }); let nonCustomAppsToUpdate = getAppsToUpdate({excludeCustomApps:true});