add favourite functionality

* library select/unselect apps/widgets
* as library  section
* as upload
* deny unselect  for boot and setting
pull/292/head
MaBecker 2020-04-14 08:52:04 +02:00
parent 1689973abc
commit 0fa4b44990
5 changed files with 113 additions and 23 deletions

View File

@ -7,3 +7,4 @@ Changed for individual apps are listed in `apps/appname/ChangeLog`
* Added optional `README.md` file for apps
* Remove 2v04 version warning, add links in About to official/developer versions
* Fix issue removing an app that was just installed (Fix #253)
* Add `Favourite` functionality

View File

@ -96,6 +96,7 @@
<label class="chip" filterid="widget">Widgets</label>
<label class="chip" filterid="bluetooth">Bluetooth</label>
<label class="chip" filterid="outdoors">Outdoors</label>
<label class="chip" filterid="favourites">Favourites</label>
</div>
<div class="panel">
<div class="panel-header">
@ -134,7 +135,8 @@
<h3>Utilities</h3>
<p><button class="btn" id="settime">Set Bangle.js Time</button>
<button class="btn" id="removeall">Remove all Apps</button>
<button class="btn" id="installdefault">Install default apps</button></p>
<button class="btn" id="installdefault">Install default apps</button>
<button class="btn" id="installfavourite">Install favourite apps</button></p>
</div>
</div>

View File

@ -1,6 +1,8 @@
var appJSON = []; // List of apps and info from apps.json
var appsInstalled = []; // list of app JSON
var files = []; // list of files on Bangle
var favourites = []; // list of user favourite app
const FAVOURITE = "favouriteapps.json";
httpGet("apps.json").then(apps=>{
try {
@ -18,7 +20,7 @@ httpGet("apps.json").then(apps=>{
function showChangeLog(appid) {
var app = appNameToApp(appid);
function show(contents) {
showPrompt(app.name+" Change Log",contents,{ok:true}).catch(()=>{});;
showPrompt(app.name+" Change Log",contents,{ok:true}).catch(()=>{});
}
httpGet(`apps/${appid}/ChangeLog`).
then(show).catch(()=>show("No Change Log available"));
@ -142,6 +144,20 @@ function handleAppInterface(app) {
});
}
function handleAppFavourite(favourite, app){
if (favourite) {
favourites = favourites.concat([app.id]);
} else {
if ([ "boot","setting"].includes(app.id)) {
showToast(app.name + ' is required, can\'t remove it' , 'warning');
}else {
favourites = favourites.filter(e => e != app.id);
}
}
localStorage.setItem("favouriteapps.json", JSON.stringify(favourites));
refreshLibrary();
}
// =========================================== Top Navigation
function showTab(tabname) {
htmlToArray(document.querySelectorAll("#tab-navigate .tab-item")).forEach(tab => {
@ -156,7 +172,7 @@ function showTab(tabname) {
// =========================================== Library
var chips = Array.from(document.querySelectorAll('.chip')).map(chip => chip.attributes.filterid.value)
var chips = Array.from(document.querySelectorAll('.chip')).map(chip => chip.attributes.filterid.value);
var hash = window.location.hash ? window.location.hash.slice(1) : '';
var activeFilter = !!~chips.indexOf(hash) ? hash : '';
@ -165,27 +181,34 @@ var currentSearch = '';
function refreshFilter(){
var filtersContainer = document.querySelector("#librarycontainer .filter-nav");
filtersContainer.querySelector('.active').classList.remove('active');
if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active')
else filtersContainer.querySelector('.chip[filterid]').classList.add('active')
if(activeFilter) filtersContainer.querySelector('.chip[filterid="'+activeFilter+'"]').classList.add('active');
else filtersContainer.querySelector('.chip[filterid]').classList.add('active');
}
function refreshLibrary() {
var panelbody = document.querySelector("#librarycontainer .panel-body");
var visibleApps = appJSON;
if (activeFilter) {
visibleApps = visibleApps.filter(app => app.tags && app.tags.split(',').includes(activeFilter));
if ( activeFilter == "favourites" ) {
visibleApps = visibleApps.filter(app => app.id && (favourites.filter( e => e == app.id).length));
}else{
visibleApps = visibleApps.filter(app => app.tags && app.tags.split(',').includes(activeFilter));
}
}
if (currentSearch) {
visibleApps = visibleApps.filter(app => app.name.toLowerCase().includes(currentSearch) || app.tags.includes(currentSearch));
}
favourites = (localStorage.getItem(FAVOURITE)) === null ? JSON.parse('["boot","launch","setting"]') : JSON.parse(localStorage.getItem("favouriteapps.json"));
panelbody.innerHTML = visibleApps.map((app,idx) => {
var appInstalled = appsInstalled.find(a=>a.id==app.id);
var version = getVersionInfo(app, appInstalled);
var versionInfo = version.text;
if (versionInfo) versionInfo = " <small>("+versionInfo+")</small>";
var readme = `<a href="#" onclick="showReadme('${app.id}')">Read more...</a>`;
var favourite = favourites.find(e => e == app.id);
return `<div class="tile column col-6 col-sm-12 col-xs-12">
<div class="tile-icon">
<figure class="avatar"><img src="apps/${app.icon?`${app.id}/${app.icon}`:"unknown.png"}" alt="${escapeHtml(app.name)}"></figure><br/>
@ -195,7 +218,8 @@ function refreshLibrary() {
<p class="tile-subtitle">${escapeHtml(app.description)}${app.readme?`<br/>${readme}`:""}</p>
<a href="https://github.com/espruino/BangleApps/tree/master/apps/${app.id}" target="_blank" class="link-github"><img src="img/github-icon-sml.png" alt="See the code on GitHub"/></a>
</div>
<div class="tile-action">
<div class="tile-action">
<button class="btn btn-link btn-action btn-lg ${!app.custom?"text-error":"d-hide"}" appid="${app.id}" title="Favorite"><i class="icon"></i>${favourite?"&#x2665;":"&#x2661;"}</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-download"></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-share"></i></button>
<button class="btn btn-link btn-action btn-lg ${version.canUpdate?"":"d-hide"}" appid="${app.id}" title="Update App"><i class="icon icon-refresh"></i></button>
@ -232,7 +256,7 @@ function refreshLibrary() {
// upload
icon.classList.remove("icon-upload");
icon.classList.add("loading");
uploadApp(app)
uploadApp(app);
} else if (icon.classList.contains("icon-menu")) {
// custom HTML update
icon.classList.remove("icon-menu");
@ -250,6 +274,10 @@ function refreshLibrary() {
updateApp(app);
} else if (icon.classList.contains("icon-download")) {
handleAppInterface(app);
} else if ( button.innerText == String.fromCharCode(0x2661)) {
handleAppFavourite(true, app);
} else if ( button.innerText == String.fromCharCode(0x2665) ) {
handleAppFavourite(false, app);
}
});
});
@ -262,17 +290,17 @@ refreshLibrary();
function uploadApp(app) {
return getInstalledApps().then(()=>{
if (appsInstalled.some(i => i.id === app.id)) {
return updateApp(app)
return updateApp(app);
}
Comms.uploadApp(app).then((appJSON) => {
Progress.hide({ sticky: true })
Progress.hide({ sticky: true });
if (appJSON) {
appsInstalled.push(appJSON)
appsInstalled.push(appJSON);
}
showToast(app.name + ' Uploaded!', 'success')
showToast(app.name + ' Uploaded!', 'success');
}).catch(err => {
Progress.hide({ sticky: true })
showToast('Upload failed, ' + err, 'error')
Progress.hide({ sticky: true });
showToast('Upload failed, ' + err, 'error');
}).finally(()=>{
refreshMyApps();
refreshLibrary();
@ -286,8 +314,8 @@ function removeApp(app) {
return showPrompt("Delete","Really remove '"+app.name+"'?").then(() => {
return getInstalledApps().then(()=>{
// a = from appid.info, app = from apps.json
return Comms.removeApp(appsInstalled.find(a => a.id === app.id))
})
return Comms.removeApp(appsInstalled.find(a => a.id === app.id));
});
}).then(()=>{
appsInstalled = appsInstalled.filter(a=>a.id!=app.id);
showToast(app.name+" removed successfully","success");
@ -315,13 +343,13 @@ function updateApp(app) {
if (app.custom) return customApp(app);
return getInstalledApps().then(() => {
// a = from appid.info, app = from apps.json
let remove = appsInstalled.find(a => a.id === app.id)
let remove = appsInstalled.find(a => a.id === app.id);
// no need to remove files which will be overwritten anyway
remove.files = remove.files.split(',')
.filter(f => f !== app.id + '.info')
.filter(f => !app.storage.some(s => s.name === f))
.join(',')
return Comms.removeApp(remove)
.join(',');
return Comms.removeApp(remove);
}).then(()=>{
showToast(`Updating ${app.name}...`);
appsInstalled = appsInstalled.filter(a=>a.id!=app.id);
@ -397,7 +425,7 @@ return `<div class="tile column col-6 col-sm-12 col-xs-12">
// check icon to figure out what we should do
if (icon.classList.contains("icon-delete")) removeApp(app);
if (icon.classList.contains("icon-refresh")) updateApp(app);
if (icon.classList.contains("icon-download")) handleAppInterface(app)
if (icon.classList.contains("icon-download")) handleAppInterface(app);
});
});
}
@ -405,7 +433,7 @@ return `<div class="tile column col-6 col-sm-12 col-xs-12">
let haveInstalledApps = false;
function getInstalledApps(refresh) {
if (haveInstalledApps && !refresh) {
return Promise.resolve(appsInstalled)
return Promise.resolve(appsInstalled);
}
showLoadingIndicator("myappscontainer");
// Get apps and files
@ -453,7 +481,7 @@ filtersContainer.addEventListener('click', ({ target }) => {
activeFilter = target.getAttribute('filterid') || '';
refreshFilter();
refreshLibrary();
window.location.hash = activeFilter
window.location.hash = activeFilter;
});
var librarySearchInput = document.querySelector("#searchform input");
@ -526,7 +554,7 @@ document.getElementById("installdefault").addEventListener("click",event=>{
upload();
}).catch(function() {
Progress.hide({sticky:true});
reject()
reject();
});
}
upload();
@ -541,3 +569,48 @@ document.getElementById("installdefault").addEventListener("click",event=>{
showToast("App Install failed, "+err,"error");
});
});
// Install all favoutrie apps in one go
document.getElementById("installfavourite").addEventListener("click",event=>{
var defaultApps, appCount;
asyncLocalStorage.getItem(FAVOURITE).then(json=>{
defaultApps = JSON.parse(json);
defaultApps = defaultApps.map( appid => appJSON.find(app=>app.id==appid) );
if (defaultApps.some(x=>x===undefined))
throw "Not all apps found";
appCount = defaultApps.length;
return showPrompt("Install Defaults","Remove everything and install favourite apps?");
}).then(() => {
return Comms.removeAllApps();
}).then(()=>{
Progress.hide({sticky:true});
appsInstalled = [];
showToast(`Existing apps removed. Installing ${appCount} apps...`);
return new Promise((resolve,reject) => {
function upload() {
var app = defaultApps.shift();
if (app===undefined) return resolve();
Progress.show({title:`${app.name} (${appCount-defaultApps.length}/${appCount})`,sticky:true});
Comms.uploadApp(app,"skip_reset").then((appJSON) => {
Progress.hide({sticky:true});
if (appJSON) appsInstalled.push(appJSON);
showToast(`(${appCount-defaultApps.length}/${appCount}) ${app.name} Uploaded`);
upload();
}).catch(function() {
Progress.hide({sticky:true});
reject();
});
}
upload();
});
}).then(()=>{
return Comms.setTime();
}).then(()=>{
showToast("Favourites apps successfully installed!","success");
return getInstalledApps(true);
}).catch(err=>{
Progress.hide({sticky:true});
showToast("App Install failed, "+err,"error");
});
});

View File

@ -86,6 +86,7 @@ function showToast(message, type) {
var style = "toast-primary";
if (type=="success") style = "toast-success";
else if (type=="error") style = "toast-error";
else if (type=="warning") style = "toast-warning";
else if (type!==undefined) console.log("showToast: unknown toast "+type);
var toastcontainer = document.getElementById("toastcontainer");
var msgDiv = htmlElement(`<div class="toast ${style}"></div>`);

View File

@ -67,3 +67,16 @@ function getVersionInfo(appListing, appInstalled) {
canUpdate : canUpdate
}
}
const asyncLocalStorage = {
setItem: function (key, value) {
return Promise.resolve().then(function () {
localStorage.setItem(key, value);
});
},
getItem: function (key) {
return Promise.resolve().then(function () {
return localStorage.getItem(key);
});
}
};