@@ -8,7 +8,7 @@ const DEFAULTSETTINGS = {
88 settime : false , // Always update time when we connect
99 favourites : [ "launch" ] ,
1010 language : "" ,
11- appsFavoritedThisSession : [ ] , // list of apps favourited before database was updated
11+ appsfavouritedThisSession : [ ] , // list of apps favourited before database was updated
1212 bleCompat : false , // 20 byte MTU BLE Compatibility mode
1313 sendUsageStats : true , // send usage stats to banglejs.com
1414 alwaysAllowUpdate : false , // Always show "reinstall app" button regardless of the version
@@ -501,19 +501,23 @@ function handleAppInterface(app) {
501501 } ) ;
502502}
503503
504- function changeAppFavourite ( favourite , app ) {
504+ function changeAppFavourite ( favourite , app , refresh = true ) {
505+
506+
505507 if ( favourite ) {
506- SETTINGS . appsFavoritedThisSession . push ( { "id" :app . id , "favs" :appSortInfo [ app . id ] && appSortInfo [ app . id ] . favourites ?appSortInfo [ app . id ] . favourites :0 } ) ;
508+ SETTINGS . appsfavouritedThisSession . push ( { "id" :app . id , "favs" :appSortInfo [ app . id ] && appSortInfo [ app . id ] . favourites ?appSortInfo [ app . id ] . favourites :0 } ) ;
507509 SETTINGS . favourites = SETTINGS . favourites . concat ( [ app . id ] ) ;
508510 } else {
509- SETTINGS . appsFavoritedThisSession = SETTINGS . appsFavoritedThisSession . filter ( obj => obj . id !== app . id ) ;
511+ SETTINGS . appsfavouritedThisSession = SETTINGS . appsfavouritedThisSession . filter ( obj => obj . id !== app . id ) ;
510512 SETTINGS . favourites = SETTINGS . favourites . filter ( e => e != app . id ) ;
511513 }
514+
512515 saveSettings ( ) ;
513- refreshLibrary ( ) ;
514- refreshMyApps ( ) ;
516+ if ( refresh ) {
517+ refreshLibrary ( ) ;
518+ refreshMyApps ( ) ;
519+ }
515520}
516-
517521// =========================================== Top Navigation
518522function showTab ( tabname ) {
519523 htmlToArray ( document . querySelectorAll ( "#tab-navigate .tab-item" ) ) . forEach ( tab => {
@@ -543,6 +547,29 @@ librarySearchInput.addEventListener('input', evt => {
543547
544548// =========================================== App Info
545549
550+
551+
552+
553+ function getAppfavourites ( app ) {
554+ let info = appSortInfo [ app . id ] || { } ;
555+ // start with whatever number we have in the database (may be undefined -> treat as 0)
556+ let appFavourites = ( typeof info . favourites === 'number' ) ? info . favourites : 0 ;
557+ let favsThisSession = SETTINGS . appsfavouritedThisSession . find ( obj => obj . id === app . id ) ;
558+ if ( favsThisSession ) {
559+ // If the database count changed since we recorded the session-favourite, it means
560+ // the server/db has been updated and our optimistic session entry is stale.
561+ if ( typeof info . favourites === 'number' && info . favourites !== favsThisSession . favs ) {
562+ // remove stale session entry
563+ SETTINGS . appsfavouritedThisSession = SETTINGS . appsfavouritedThisSession . filter ( obj => obj . id !== app . id ) ;
564+ } else {
565+ // otherwise include our optimistic +1 so the UI updates immediately
566+ appFavourites += 1 ;
567+ }
568+ }
569+ return appFavourites ;
570+ }
571+
572+
546573function getAppHTML ( app , appInstalled , forInterface ) {
547574 let version = getVersionInfo ( app , appInstalled ) ;
548575 let versionInfo = version . text ;
@@ -559,21 +586,11 @@ function getAppHTML(app, appInstalled, forInterface) {
559586 infoTxt . push ( `${ info . installs } reported installs (${ percentText } )` ) ;
560587 }
561588 if ( info . favourites ) {
562- let favsThisSession = SETTINGS . appsFavoritedThisSession . find ( obj => obj . id === app . id ) ;
563- let percent = ( info . favourites / info . installs * 100 ) . toFixed ( 0 ) ;
589+ appFavourites = getAppfavourites ( app ) ;
590+ let percent = ( appFavourites / info . installs * 100 ) . toFixed ( 0 ) ;
564591 let percentText = percent > 100 ?"More than 100% of installs" :percent + "% of installs" ;
565- if ( ! info . installs || info . installs < 1 ) { infoTxt . push ( `${ info . favourites } users favourited` ) }
566- else { infoTxt . push ( `${ info . favourites } users favourited (${ percentText } )` ) }
567- appFavourites = info . favourites ;
568- if ( favsThisSession ) {
569- if ( info . favourites != favsThisSession . favs ) {
570- //database has been updated, remove app from favsThisSession
571- SETTINGS . appsFavoritedThisSession = SETTINGS . appsFavoritedThisSession . filter ( obj => obj . id !== app . id ) ;
572- }
573- else {
574- appFavourites += 1 ; //add one to give the illusion of immediate database changes
575- }
576- }
592+ if ( ! info . installs || info . installs < 1 ) { infoTxt . push ( `${ appFavourites } users favourited` ) ; }
593+ else { infoTxt . push ( `${ appFavourites } users favourited (${ percentText } )` ) ; }
577594 }
578595 if ( infoTxt . length )
579596 versionTitle = `title="${ infoTxt . join ( "\n" ) } "` ;
@@ -585,12 +602,13 @@ function getAppHTML(app, appInstalled, forInterface) {
585602 let githubLink = Const . APP_SOURCECODE_URL ?
586603 `<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>` : "" ;
587604 let getAppFavouritesHTML = cnt => {
588- if ( ! cnt ) return "" ;
589- let txt = ( cnt > 999 ) ? Math . round ( cnt / 1000 ) + "k" : cnt ;
590- return `<span>${ txt } </span>` ;
605+ // Always show a count (0 if none) and format large numbers with 'k'
606+ let n = ( cnt && typeof cnt === 'number' ) ? cnt : 0 ;
607+ let txt = ( n > 999 ) ? Math . round ( n / 100 ) / 10 + "k" : n ;
608+ return `<span class="fav-count" style="margin-left:-1em;margin-right:0.5em">${ txt } </span>` ;
591609 } ;
592610
593- let html = `<div class="tile column col-6 col-sm-12 col-xs-12 app-tile">
611+ let html = `<div class="tile column col-6 col-sm-12 col-xs-12 app-tile ${ version . canUpdate ? 'updateTile' : '' } ">
594612 <div class="tile-icon">
595613 <figure class="avatar"><img src="apps/${ app . icon ?`${ app . id } /${ app . icon } ` :"unknown.png" } " alt="${ escapeHtml ( app . name ) } "></figure>
596614 </div>
@@ -601,20 +619,21 @@ function getAppHTML(app, appInstalled, forInterface) {
601619 <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>
602620 </div>
603621 <div class="tile-action">` ;
622+ html += `<div class="pill-container">` ;
604623 if ( forInterface == "library" ) html += `
605- <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>
624+ <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>
606625 <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>
607626 <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>
608627 <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>
609628 <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>
610629 <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>
611630 <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>` ;
612631 if ( forInterface == "myapps" ) html += `
613- <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>
632+ <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>
614633 <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>
615634 <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>
616635 <button class="btn btn-link btn-action btn-lg" appid="${ app . id } " title="Remove App"><i class="icon icon-delete"></i></button>` ;
617- html += "</div>" ;
636+ html += "</div></div> " ;
618637 if ( forInterface == "library" ) {
619638 let screenshots = ( app . screenshots || [ ] ) . filter ( s => s . url ) ;
620639 if ( screenshots . length )
@@ -649,6 +668,23 @@ function refreshSort(){
649668 if ( activeSort ) sortContainer . querySelector ( '.chip[sortid="' + activeSort + '"]' ) . classList . add ( 'active' ) ;
650669 else sortContainer . querySelector ( '.chip[sortid]' ) . classList . add ( 'active' ) ;
651670}
671+ function handlefavouriteClick ( icon , app , button ) {
672+ const favAnimMS = 500 ; // duration of favourite animation in ms
673+ // clicked: animate and toggle favourite state immediately for instant feedback
674+ let favourite = SETTINGS . favourites . find ( e => e == app . id ) ;
675+ changeAppFavourite ( ! favourite , app , false ) ;
676+ if ( icon ) icon . classList . toggle ( "icon-favourite-active" , ! favourite ) ;
677+ if ( icon ) icon . classList . add ( "favoriteAnim" ) ;
678+ // update visible count optimistically (always update, even if 0)
679+ let cnt = getAppfavourites ( app ) ;
680+ let txt = ( cnt > 999 ) ? Math . round ( cnt / 100 ) / 10 + "k" : cnt ;
681+ let countEl = button . querySelector ( '.fav-count' ) ;
682+ if ( countEl ) countEl . textContent = String ( txt ) ;
683+ // ensure animation class is removed after the duration so it can be re-triggered
684+ setTimeout ( ( ) => {
685+ try { if ( icon ) icon . classList . remove ( "favoriteAnim" ) ; } catch ( e ) { console . error ( e ) ; }
686+ } , favAnimMS ) ;
687+ }
652688// Refill the library with apps
653689function refreshLibrary ( options ) {
654690 options = options || { } ;
@@ -789,7 +825,6 @@ function refreshLibrary(options) {
789825 visibleApps = visibleApps . slice ( 0 , Const . MAX_APPS_SHOWN - 1 ) ;
790826 }
791827
792-
793828 panelbody . innerHTML = visibleApps . map ( ( app , idx ) => {
794829 let appInstalled = device . appsInstalled . find ( a => a . id == app . id ) ;
795830 return getAppHTML ( app , appInstalled , "library" ) ;
@@ -801,7 +836,7 @@ function refreshLibrary(options) {
801836 htmlToArray ( panelbody . getElementsByTagName ( "button" ) ) . forEach ( button => {
802837 button . addEventListener ( "click" , event => {
803838 let button = event . currentTarget ;
804- let icon = button . firstChild ;
839+ let icon = ( button . querySelector && ( button . querySelector ( 'i.icon' ) ) ) || button . firstElementChild || button . firstChild ;
805840 let appid = button . getAttribute ( "appid" ) ;
806841 let app = appNameToApp ( appid ) ;
807842 if ( ! app ) throw new Error ( "App " + appid + " not found" ) ;
@@ -842,8 +877,7 @@ function refreshLibrary(options) {
842877 if ( err != "" ) showToast ( "Failed, " + err , "error" ) ;
843878 } ) ;
844879 } else if ( button . classList . contains ( "btn-favourite" ) ) {
845- let favourite = SETTINGS . favourites . find ( e => e == app . id ) ;
846- changeAppFavourite ( ! favourite , app ) ;
880+ handlefavouriteClick ( icon , app , button ) ;
847881 }
848882 } ) ;
849883 } ) ;
@@ -1109,7 +1143,7 @@ function refreshMyApps() {
11091143 htmlToArray ( panelbody . getElementsByTagName ( "button" ) ) . forEach ( button => {
11101144 button . addEventListener ( "click" , event => {
11111145 let button = event . currentTarget ;
1112- let icon = button . firstChild ;
1146+ let icon = ( button . querySelector && ( button . querySelector ( 'i.icon' ) ) ) || button . firstElementChild || button . firstChild ;
11131147 let appid = button . getAttribute ( "appid" ) ;
11141148 let app = appNameToApp ( appid ) ;
11151149 if ( ! app ) throw new Error ( "App " + appid + " not found" ) ;
@@ -1120,17 +1154,18 @@ function refreshMyApps() {
11201154 handleAppInterface ( app ) . catch ( err => {
11211155 if ( err != "" ) showToast ( "Failed, " + err , "error" ) ;
11221156 } ) ;
1123- if ( icon . classList . contains ( "icon -favourite" ) ) {
1124- let favourite = SETTINGS . favourites . find ( e => e == app . id ) ;
1125- changeAppFavourite ( ! favourite , app ) ;
1157+ // handle favourites on My Apps page (button has class btn -favourite)
1158+ if ( button . classList && button . classList . contains ( "btn-favourite" ) ) {
1159+ handlefavouriteClick ( icon , app , button ) ;
11261160 }
11271161 } ) ;
11281162 } ) ;
11291163 let nonCustomAppsToUpdate = getAppsToUpdate ( { excludeCustomApps :true } ) ;
11301164 let tab = document . querySelector ( "#tab-myappscontainer a" ) ;
11311165 let updateApps = document . querySelector ( "#myappscontainer .updateapps" ) ;
11321166 if ( nonCustomAppsToUpdate . length ) {
1133- updateApps . innerHTML = `Update ${ nonCustomAppsToUpdate . length } apps` ;
1167+
1168+ updateApps . innerHTML = `Update ${ nonCustomAppsToUpdate . length } ${ nonCustomAppsToUpdate . length > 1 ?"apps" :"app" } ` ;
11341169 updateApps . classList . remove ( "hidden" ) ;
11351170 updateApps . classList . remove ( "disabled" ) ;
11361171 tab . setAttribute ( "data-badge" , `${ device . appsInstalled . length } ⬆${ nonCustomAppsToUpdate . length } ` ) ;
@@ -1364,7 +1399,7 @@ function loadSettings() {
13641399 console . error ( "Invalid settings" ) ;
13651400 }
13661401 // upgrade old settings
1367- if ( ! SETTINGS . appsFavoritedThisSession ) SETTINGS . appsFavoritedThisSession = [ ] ;
1402+ if ( ! SETTINGS . appsfavouritedThisSession ) SETTINGS . appsfavouritedThisSession = [ ] ;
13681403}
13691404/// Save settings
13701405function saveSettings ( ) {
0 commit comments