diff --git a/README.md b/README.md index 21f5dbff9..527cb1188 100644 --- a/README.md +++ b/README.md @@ -230,6 +230,7 @@ and which gives information about the app for the Launcher. "tags": "", // comma separated tag list for searching "supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2 "dependencies" : { "notify":"type" } // optional, app 'types' we depend on + "dependencies" : { "messages":"app" } // optional, depend on a specific app ID // for instance this will use notify/notifyfs is they exist, or will pull in 'notify' "readme": "README.md", // if supplied, a link to a markdown-style text file // that contains more information about this app (usage, etc) diff --git a/apps.json b/apps.json index 35fe0b49a..156b6e5b0 100644 --- a/apps.json +++ b/apps.json @@ -32,17 +32,50 @@ { "id": "messages", "name": "Messages", - "version": "0.01", - "description": "App to display notifications from iOS and Gadgetbridge (BETA)", + "version": "0.02", + "description": "App to display notifications from iOS and Gadgetbridge", "icon": "app.png", + "type": "app", "tags": "tool,system", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ {"name":"messages.app.js","url":"app.js"}, {"name":"messages.img","url":"app-icon.js","evaluate":true}, - {"name":"messages.boot.js","url":"boot.js"}, - {"name":"messages.wid.js","url":"widget.js"} + {"name":"messages.wid.js","url":"widget.js"}, + {"name":"messages","url":"lib.js"} + ], + "sortorder": -9 + }, + { + "id": "android", + "name": "Android Integration", + "version": "0.01", + "description": "(BETA) App to display notifications from Gadgetbridge on Android. This will eventually replace the Gadgetbridge widget.", + "icon": "app.png", + "tags": "tool,system,messages,notifications", + "dependencies": {"messages":"app"}, + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"android.app.js","url":"app.js"}, + {"name":"android.img","url":"app-icon.js","evaluate":true}, + {"name":"android.boot.js","url":"boot.js"} + ], + "sortorder": -9 + }, + { + "id": "ios", + "name": "iOS Integration", + "version": "0.01", + "description": "(BETA) App to display notifications from iOS devices", + "icon": "app.png", + "tags": "tool,system,ios,apple,messages,notifications", + "dependencies": {"messages":"app"}, + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"ios.app.js","url":"app.js"}, + {"name":"ios.img","url":"app-icon.js","evaluate":true}, + {"name":"ios.boot.js","url":"boot.js"} ], "sortorder": -9 }, @@ -216,7 +249,7 @@ "id": "gbridge", "name": "Gadgetbridge", "version": "0.24", - "description": "The default notification handler for Gadgetbridge notifications from Android", + "description": "The default notification handler for Gadgetbridge notifications from Android. This will eventually be replaced by the 'Android' app.", "icon": "app.png", "type": "widget", "tags": "tool,system,android,widget", diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/android/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/android/app-icon.js b/apps/android/app-icon.js new file mode 100644 index 000000000..9253ec839 --- /dev/null +++ b/apps/android/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4MA///xF9FstggwFDuEOAoc//gFJv/+AoZHBAgUB8/nwAFCBYIFCgYFB4AFHABdjCIPGAoPzAoPPAvpHFMpYFFPosAnk8NgYFdjEYfMo=")) diff --git a/apps/android/app.js b/apps/android/app.js new file mode 100644 index 000000000..5c5c7ddaf --- /dev/null +++ b/apps/android/app.js @@ -0,0 +1,2 @@ +// Config app not implemented yet +load("messages.app.js"); diff --git a/apps/android/app.png b/apps/android/app.png new file mode 100644 index 000000000..65150f08d Binary files /dev/null and b/apps/android/app.png differ diff --git a/apps/android/boot.js b/apps/android/boot.js new file mode 100644 index 000000000..dd19f9500 --- /dev/null +++ b/apps/android/boot.js @@ -0,0 +1,55 @@ +(function() { + function gbSend(message) { + Bluetooth.println(""); + Bluetooth.println(JSON.stringify(message)); + } + + var _GB = global.GB; + global.GB = (event) => { + // feed a copy to other handlers if there were any + if (_GB) setTimeout(_GB,0,Object.assign({},event)); + + /* TODO: Call handling, fitness */ + var HANDLERS = { + // {t:"notify",id:int, src,title,subject,body,sender,tel:string} add + "notify" : function() { event.t="add";require("messages").pushMessage(event); }, + // {t:"notify~",id:int, title:string} // modified + "notify~" : function() { event.t="modify";require("messages").pushMessage(event); }, + // {t:"notify-",id:int} // remove + "notify-" : function() { event.t="remove";require("messages").pushMessage(event); }, + // {t:"find", n:bool} // find my phone + "find" : function() { + if (Bangle.findDeviceInterval) { + clearInterval(Bangle.findDeviceInterval); + delete Bangle.findDeviceInterval; + } + if (event.n) // Ignore quiet mode: we always want to find our watch + Bangle.findDeviceInterval = setInterval(_=>Bangle.buzz(),1000); + }, + // {t:"musicstate", state:"play/pause",position,shuffle,repeat} + "musicstate" : function() { + require("messages").pushMessage({t:"modify",id:"music",title:"Music",state:event.state}); + }, + // {t:"musicinfo", artist,album,track,dur,c(track count),n(track num} + "musicinfo" : function() { + require("messages").pushMessage(Object.assign(event, {t:"modify",id:"music",title:"Music"})); + } + }; + var h = HANDLERS[event.t]; + if (h) h(); else console.log("GB Unknown",event); + }; + + // Battery monitor + function sendBattery() { gbSend({ t: "status", bat: E.getBattery() }); } + NRF.on("connect", () => setTimeout(sendBattery, 2000)); + setInterval(sendBattery, 10*60*1000); + // Health tracking + Bangle.on('health', health=>{ + gbSend({ t: "act", stp: health.steps, hrm: health.bpm }); + }); + // Music control + Bangle.musicControl = cmd => { + // play/pause/next/previous/volumeup/volumedown + gbSend({ t: "music", m:cmd }); + } +})(); diff --git a/apps/ios/ChangeLog b/apps/ios/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/ios/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/ios/app-icon.js b/apps/ios/app-icon.js new file mode 100644 index 000000000..b74048750 --- /dev/null +++ b/apps/ios/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwZC/AGEB/4AGwARHv4RH/wQGj4QHAAP4CIoQJAAIRWg4RL8ARVn4RL/gR/CJv9BIP934DFEZH+v/0AgMv+wRK+YCBz/7C4PfCJOfAQO//JHMCIX3/d/CJ//t4RJF4JlCCIP/koRKEYh+DCIxlBCIQADCJQgCn4DCCJSbBHIIDBXYQRI/+Sp4DB7ZsCfdQRzg4RL8ARVgARLCAgRSj4QJ/ARFgF/CA/+CA0AgIRHwARHAH4AnA")) diff --git a/apps/ios/app.js b/apps/ios/app.js new file mode 100644 index 000000000..5c5c7ddaf --- /dev/null +++ b/apps/ios/app.js @@ -0,0 +1,2 @@ +// Config app not implemented yet +load("messages.app.js"); diff --git a/apps/ios/app.png b/apps/ios/app.png new file mode 100644 index 000000000..79aa78f3a Binary files /dev/null and b/apps/ios/app.png differ diff --git a/apps/ios/boot.js b/apps/ios/boot.js new file mode 100644 index 000000000..69e5e2a26 --- /dev/null +++ b/apps/ios/boot.js @@ -0,0 +1,129 @@ +bleServiceOptions.ancs = true; +Bangle.ancsMessageQueue = []; + +/* Handle ANCS events coming in, and fire off 'notify' events +when we actually have all the information we need */ +E.on('ANCS',msg=>{ + /* eg: + { + event:"add", + uid:42, + category:4, + categoryCnt:42, + silent:true, + important:false, + preExisting:true, + positive:false, + negative:true + } */ + + //console.log("ANCS",msg.event,msg.id); + // don't need info for remove events - pass these on + if (msg.event=="remove") + return E.emit("notify", msg); + + // not a remove - we need to get the message info first + function ancsHandler() { + var msg = Bangle.ancsMessageQueue[0]; + NRF.ancsGetNotificationInfo( msg.uid ).then( info => { + E.emit("notify", Object.assign(msg, info)); + Bangle.ancsMessageQueue.shift(); + if (Bangle.ancsMessageQueue.length) + ancsHandler(); + }); + } + Bangle.ancsMessageQueue.push(msg); + // if this is the first item in the queue, kick off ancsHandler, + // otherwise ancsHandler will handle the rest + if (Bangle.ancsMessageQueue.length==1) + ancsHandler(); +}); + +// Handle ANCS events with all the data +E.on('notify',msg=>{ +/* Info from ANCS event plus + "uid" : int, + "appId" : string, + "title" : string, + "subtitle" : string, + "message" : string, + "messageSize" : string, + "date" : string, + "posAction" : string, + "negAction" : string, + "name" : string, +*/ + var appNames = { + "com.netflix.Netflix" : "Netflix", + "com.google.ios.youtube" : "YouTube", + "com.google.hangouts" : "Hangouts" + // could also use NRF.ancsGetAppInfo(msg.appId) here + }; + var unicodeRemap = { + '2019':"'" + }; + var replacer = ""; //(n)=>print('Unknown unicode '+n.toString(16)); + if (appNames[msg.appId]) msg.a + require("messages").pushMessage({ + t : msg.event, + id : msg.uid, + src : appNames[msg.appId] || msg.appId, + title : msg.title&&E.decodeUTF8(msg.title, unicodeRemap, replacer), + subject : msg.subtitle&&E.decodeUTF8(msg.subtitle, unicodeRemap, replacer), + body : msg.message&&E.decodeUTF8(msg.message, unicodeRemap, replacer) + }); + // TODO: posaction/negaction? +}); + +// Apple media service +E.on('AMS',a=>{ + function push(m) { + var msg = { t : "modify", id : "music", title:"Music" }; + if (a.id=="artist") msg.artist = m; + else if (a.id=="album") msg.artist = m; + else if (a.id=="title") msg.tracl = m; + else return; // duration? need to reformat + require("messages").pushMessage(msg); + } + if (a.truncated) NRF.amsGetMusicInfo(a.id).then(push) + else push(a.value); +}); + +// Music control +Bangle.musicControl = cmd => { + // play, pause, playpause, next, prev, volup, voldown, repeat, shuffle, skipforward, skipback, like, dislike, bookmark + NRF.amsCommand(cmd); +} + +/* +// For testing... + +NRF.ancsGetNotificationInfo = function(uid) { + print("ancsGetNotificationInfo",uid); + return Promise.resolve({ + "uid" : uid, + "appId" : "Hangouts", + "title" : "Hello", + "subtitle" : "There", + "message" : "Lots and lots of text", + "messageSize" : 100, + "date" : "...", + "posAction" : "ok", + "negAction" : "cancel", + "name" : "Fred", + }); +}; + +E.emit("ANCS", { + event:"add", + uid:42, + category:4, + categoryCnt:42, + silent:true, + important:false, + preExisting:true, + positive:false, + negative:true +}); + +*/ diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog new file mode 100644 index 000000000..bbeb8b717 --- /dev/null +++ b/apps/messages/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Add 'messages' library diff --git a/apps/messages/app.js b/apps/messages/app.js index 801434498..2fb0c613b 100644 --- a/apps/messages/app.js +++ b/apps/messages/app.js @@ -12,15 +12,10 @@ /* For example for maps: -GB({"t":"notify","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"}) -GB({"t":"notify","id":2,"src":"Hangouts","title":"Gordon","body":"Hello world quite a lot of text here..."}) -GB({"t":"notify","id":3,"src":"Messages","title":"Ted","body":"Bed time."}) -GB({"t":"notify","id":4,"src":"Messages","title":"Kailo","body":"Mmm... food"}) -GB({"t":"notify-","id":1}) - -GB({"t":"notify","id":1,"src":"Maps","title":"0 yd - High St","body":"Campton - 11:48 ETA","img":"Y2MBAA....AAAAAAAAAAAAAA="}) -GB({"t":"notify~","id":1,"body":"Campton - 11:54 ETA"}) -GB({"t":"notify~","id":1,"title":"High St"}) +// a message +{"t":"add","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"} +// maps +{"t":"add","id":1,"src":"Maps","title":"0 yd - High St","body":"Campton - 11:48 ETA","img":"GhqBAAAMAAAHgAAD8AAB/gAA/8AAf/gAP/8AH//gD/98B//Pg/4B8f8Afv+PP//n3/f5//j+f/wfn/4D5/8Aef+AD//AAf/gAD/wAAf4AAD8AAAeAAADAAA="} */ @@ -37,6 +32,21 @@ function saveMessages() { require("Storage").writeJSON("messages.json",MESSAGES) } +function getBackImage() { + return atob("FhYBAAAAEAAAwAAHAAA//wH//wf//g///BwB+DAB4EAHwAAPAAA8AADwAAPAAB4AAHgAB+AH/wA/+AD/wAH8AA=="); +} +function getMessageImage(msg) { + if (msg.img) return atob(msg.img); + var s = (msg.src||"").toLowerCase(); + if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA=="); + if (s=="hangouts") return atob("FBaBAAH4AH/gD/8B//g//8P//H5n58Y+fGPnxj5+d+fmfj//4//8H//B//gH/4A/8AA+AAHAABgAAAA="); + if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA=="); + if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A="); + if (msg.id=="back") return getBackImage(); + return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A=="); +} + + function showMapMessage(msg) { var m; var distance, street, target, eta; @@ -50,48 +60,98 @@ function showMapMessage(msg) { target = m[1]; eta = m[2]; } else target=msg.body; - layout = new Layout({ - type:"v", c: [ - {type:"txt", font:"6x15", label:target, bgCol:"#0f0", fillx:1, pad:2 }, - {type:"h", bgCol:"#0f0", fillx:1, c: [ - {type:"txt", font:"6x8", label:"Towards" }, - {type:"txt", font:"6x15:2", label:street } + layout = new Layout({ type:"v", c: [ + {type:"txt", font:"6x15", label:target, bgCol:"#0f0", fillx:1, pad:2 }, + {type:"h", bgCol:"#0f0", fillx:1, c: [ + {type:"txt", font:"6x8", label:"Towards" }, + {type:"txt", font:"6x15:2", label:street } + ]}, + {type:"h",fillx:1, filly:1, c: [ + msg.img?{type:"img",src:atob(msg.img), scale:2}:{}, + {type:"v", fillx:1, c: [ + {type:"txt", font:"6x15:2", label:distance||"" } ]}, - {type:"h",fillx:1, filly:1, c: [ - {type:"img",src:atob(msg.img)}, - {type:"v", fillx:1, c: [ - {type:"txt", font:"6x15:2", label:distance||"" } - ]}, - ]}, - - {type:"txt", font:"6x8:2", label:eta } - ] - }); - g.clearRect(0,24,g.getWidth()-1,g.getHeight()-1); + ]}, + {type:"txt", font:"6x8:2", label:eta } + ]}); + g.clearRect(Bangle.appRect); layout.render(); Bangle.setUI("updown",function() { // any input to mark as not new and return to menu msg.new = false; saveMessages(); + layout = undefined; checkMessages(); }); } +function showMusicMessage(msg) { + function fmtTime(s) { + var m = Math.floor(s/60); + s = (s%60).toString().padStart(2,0); + return m+":"+s; + } + + function back() { + msg.new = false; + saveMessages(); + layout = undefined; + checkMessages(); + } + layout = new Layout({ type:"v", c: [ + {type:"h", fillx:1, bgCol:"#0f0", c: [ + { type:"btn", src:getBackImage, cb:back }, + { type:"v", fillx:1, c: [ + { type:"txt", font:"6x15:2", label:msg.artist, pad:2 }, + { type:"txt", font:"6x15", label:msg.album, pad:2 } + ]} + ]}, + {type:"txt", font:"6x15:2", label:msg.track, fillx:1, filly:1, pad:2 }, + Bangle.musicControl?{type:"h",fillx:1, c: [ + {type:"btn", pad:8, label:"\0"+atob("FhgBwAADwAAPwAA/wAD/gAP/gA//gD//gP//g///j///P//////////P//4//+D//gP/4A/+AD/gAP8AA/AADwAAMAAA"), cb:()=>Bangle.musicControl("play")}, // play + {type:"btn", pad:8, label:"\0"+atob("EhaBAHgHvwP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP/wP3gHg"), cb:()=>Bangle.musicControl("pause")}, // pause + {type:"btn", pad:8, label:"\0"+atob("EhKBAMAB+AB/gB/wB/8B/+B//B//x//5//5//x//B/+B/8B/wB/gB+AB8ABw"), cb:()=>Bangle.musicControl("next")}, // next + ]}:{}, + {type:"txt", font:"6x8:2", label:msg.dur?fmtTime(msg.dur):"--:--" } + ]}); + g.clearRect(Bangle.appRect); + layout.render(); +} + function showMessage(msgid) { var msg = MESSAGES.find(m=>m.id==msgid); if (!msg) return checkMessages(); // go home if no message found if (msg.src=="Maps") return showMapMessage(msg); - var m = msg.title+"\n"+msg.body; - E.showPrompt(m,{title:"Message", buttons : {"Read":"read", "Back":"back"}}).then(chosen => { - if (chosen=="read") { - // any input to mark as not new and return to menu - msg.new = false; - saveMessages(); - checkMessages(); - } else { - checkMessages(true); - } - }); + if (msg.id=="music") return showMusicMessage(msg); + // Normal text message display + var title=msg.title, titleFont = "6x15:2"; + if (title) { + var w = g.getWidth()-40; + if (g.setFont(titleFont).stringWidth(title) > w) + titleFont = "6x15"; + if (g.setFont(titleFont).stringWidth(title) > w) + title = g.wrapString(title, w).join("\n"); + } + layout = new Layout({ type:"v", c: [ + {type:"h", fillx:1, bgCol:"#0f0", c: [ + { type:"img", src:getMessageImage(msg), pad:2 }, + { type:"v", fillx:1, c: [ + {type:"txt", font:"6x15", label:msg.src||"Message", bgCol:"#0f0", fillx:1, pad:2 }, + title?{type:"txt", font:titleFont, label:title, bgCol:"#0f0", fillx:1, pad:2 }:{}, + ]}, + ]}, + {type:"txt", font:"6x15", label:msg.body||"", wrap:true, fillx:1, filly:1, pad:2 }, + {type:"h",fillx:1, c: [ + {type:"btn", src:getBackImage(), cb:()=>checkMessages(true)}, // back + msg.new?{type:"btn", src:atob("HRiBAD///8D///wj///Fj//8bj//x3z//Hvx/8/fx/j+/x+Ad/B4AL8Rh+HxwH+PHwf+cf5/+x/n/PH/P8cf+cx5/84HwAB4fgAD5/AAD/8AAD/wAAD/AAAD8A=="), cb:()=>{ + msg.new = false; // read mail + saveMessages(); + checkMessages(); + }}:{} + ]} + ]}); + g.clearRect(Bangle.appRect); + layout.render(); } function checkMessages(forceShowMenu) { @@ -112,24 +172,35 @@ function checkMessages(forceShowMenu) { // Otherwise show a menu E.showScroller({ h : 48, - c : MESSAGES.length, + c : MESSAGES.length+1, draw : function(idx, r) {"ram" var msg = MESSAGES[idx-1]; if (msg && msg.new) g.setBgColor("#4F4"); else g.setBgColor((idx&1) ? "#CFC" : "#9F9"); g.clearRect(r.x,r.y,r.x+r.w-1,r.y+r.h-1).setColor(g.theme.fg); - if (idx==0) msg = {title:"< Back"}; + if (idx==0) msg = {id:"back", title:"< Back"}; + if (!msg) return; + var x = r.x+2, title = msg.title, body = msg.body; + var img = getMessageImage(msg); + if (msg.id=="music") { + title = msg.artist || "Music"; + body = msg.track; + } + if (img) { + g.drawImage(img, x+24, r.y+24, {rotate:0}); // force centering + x += 50; + } var m = msg.title+"\n"+msg.body; if (msg.src) g.setFontAlign(1,-1).drawString(msg.src, r.x+r.w-2, r.t+2); - if (msg.title) g.setFontAlign(-1,-1).setFont("12x20").drawString(msg.title, r.x+2,r.y+2); - if (msg.body) { + if (title) g.setFontAlign(-1,-1).setFont("12x20").drawString(title, x,r.y+2); + if (body) { g.setFontAlign(-1,-1).setFont("6x8"); - var l = g.wrapString(msg.body, r.w-14); + var l = g.wrapString(body, r.w-14); if (l.length>3) { l = l.slice(0,3); l[l.length-1]+="..."; } - g.drawString(l.join("\n"), r.x+12,r.y+20); + g.drawString(l.join("\n"), x+10,r.y+20); } }, select : idx => { diff --git a/apps/messages/boot.js b/apps/messages/boot.js deleted file mode 100644 index dce3979da..000000000 --- a/apps/messages/boot.js +++ /dev/null @@ -1,36 +0,0 @@ -(function() { - var _GB = global.GB; - global.GB = (event) => { - if (_GB) setTimeout(_GB,0,event); - // call handling? - if (!event.t.startsWith("notify")) return; - /* event is: - {t:"notify",id:int, src,title,subject,body,sender,tel:string} - {t:"notify~",id:int, title:string} // modified - {t:"notify-",id:int} // remove - */ - var messages, inApp = "undefined"!=typeof MESSAGES; - if (inApp) - messages = MESSAGES; // we're in an app that has already loaded messages - else // no app - load messages - messages = require("Storage").readJSON("messages.json",1)||[]; - // now modify/delete as appropriate - var mIdx = messages.findIndex(m=>m.id==event.id); - if (event.t=="notify-") { - if (mIdx>=0) messages.splice(mIdx, 1); // remove item - mIdx=-1; - } else { // add/modify - if (event.t=="notify") event.new=true; // new message - if (mIdx<0) mIdx=messages.push(event)-1; - else Object.assign(messages[mIdx], event); - } - require("Storage").writeJSON("messages.json",messages); - if (inApp) return onMessagesModified(mIdx<0 ? {id:event.id} : messages[mIdx]); - // ok, saved now - we only care if it's new - if (event.t!="notify") return; - // if we're in a clock, go straight to messages app - if (Bangle.CLOCK) return load("messages.app.js"); - if (!global.WIDGETS || !WIDGETS.messages) return Bangle.buzz(); // no widgets - just buzz to let someone know - WIDGETS.messages.newMessage(); - }; -})() diff --git a/apps/messages/lib.js b/apps/messages/lib.js index e69de29bb..f3ea242e5 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -0,0 +1,37 @@ +exports.pushMessage = function(event) { + /* event is: + {t:"add",id:int, src,title,subject,body,sender,tel, important:bool} // add new + {t:"add",id:int, id:"music", state, artist, track, etc} // add new + {t:"remove-",id:int} // remove + {t:"modify",id:int, title:string} // modified + */ + var messages, inApp = "undefined"!=typeof MESSAGES; + if (inApp) + messages = MESSAGES; // we're in an app that has already loaded messages + else // no app - load messages + messages = require("Storage").readJSON("messages.json",1)||[]; + // now modify/delete as appropriate + var mIdx = messages.findIndex(m=>m.id==event.id); + if (event.t=="remove") { + if (mIdx>=0) messages.splice(mIdx, 1); // remove item + mIdx=-1; + } else { // add/modify + if (event.t=="add") event.new=true; // new message + if (mIdx<0) mIdx=messages.push(event)-1; + else Object.assign(messages[mIdx], event); + } + require("Storage").writeJSON("messages.json",messages); + // if in app, process immediately + if (inApp) return onMessagesModified(mIdx<0 ? {id:event.id} : messages[mIdx]); + // ok, saved now - we only care if it's new + if (event.t!="add") return; + // otherwise load after a delay, to ensure we have all the messages + if (exports.messageTimeout) clearTimeout(exports.messageTimeout); + exports.messageTimeout = setTimeout(function() { + exports.messageTimeout = undefined; + // if we're in a clock or it's important, go straight to messages app + if (Bangle.CLOCK || event.important) return load("messages.app.js"); + if (!global.WIDGETS || !WIDGETS.messages) return Bangle.buzz(); // no widgets - just buzz to let someone know + WIDGETS.messages.newMessage(); + }, 500); +} diff --git a/core b/core index 5ef454a1a..59f80bb52 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 5ef454a1acce54f6420015b519a7ecf461f9bc37 +Subproject commit 59f80bb52a38da12cb272f9106cb3951b49dab2e diff --git a/modules/Layout.js b/modules/Layout.js index 8a5b0a0a5..c7d44ab9b 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -29,7 +29,9 @@ layoutObject has: * `undefined` - blank, can be used for padding * `"txt"` - a text label, with value `label` and `r` for text rotation. 'font' is required * `"btn"` - a button, with value `label` and callback `cb` - * `"img"` - an image where `src` is an image, or a function which is called to return an image to draw + optional `src` specifies an image (like img) in which case label is ignored + * `"img"` - an image where `src` is an image, or a function which is called to return an image to draw. + optional `scale` specifies if image should be scaled up or not * `"custom"` - a custom block where `render(layoutObj)` is called to render * `"h"` - Horizontal layout, `c` is an array of more `layoutObject` * `"v"` - Veritical layout, `c` is an array of more `layoutObject` @@ -85,6 +87,7 @@ function Layout(layout, options) { this.lazy = options.lazy || false; var btnList; + Bangle.setUI(); // remove all existing input handlers if (process.env.HWVERSION!=2) { // no touchscreen, find any buttons in 'layout' btnList = []; @@ -157,6 +160,7 @@ function Layout(layout, options) { } } if (process.env.HWVERSION==2) { + // Handler for touch events function touchHandler(l,e) { if (l.type=="btn" && l.cb && e.x>=l.x && e.y>=l.y && e.x<=l.x+l.w && e.y<=l.y+l.h) { @@ -245,10 +249,8 @@ Layout.prototype.render = function (l) { g.setFont(l.font,l.fsz).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1)); } }, "btn":function(l){ - var x = l.x+(0|l.pad); - var y = l.y+(0|l.pad); - var w = l.w-(l.pad<<1); - var h = l.h-(l.pad<<1); + var x = l.x+(0|l.pad), y = l.y+(0|l.pad), + w = l.w-(l.pad<<1), h = l.h-(l.pad<<1); var poly = [ x,y+4, x+4,y, @@ -259,10 +261,12 @@ Layout.prototype.render = function (l) { x+4,y+h-1, x,y+h-5, x,y+4 - ]; - g.setColor(l.selected?g.theme.bgH:g.theme.bg2).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg2).drawPoly(poly).setFont("6x8",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2); + ], bg = l.selected?g.theme.bgH:g.theme.bg2; + g.setColor(bg).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg2).drawPoly(poly); + if (l.src) g.setBgColor(bg).drawImage("function"==typeof l.src?l.src():l.src, l.x + 10 + (0|l.pad), l.y + 8 + (0|l.pad)); + else g.setFont("6x8",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2); }, "img":function(l){ - g.drawImage("function"==typeof l.src?l.src():l.src, l.x + (0|l.pad), l.y + (0|l.pad)); + g.drawImage("function"==typeof l.src?l.src():l.src, l.x + (0|l.pad), l.y + (0|l.pad), l.scale?{scale:l.scale}:undefined); }, "custom":function(l){ l.render(l); },"h":function(l) { l.c.forEach(render); }, @@ -363,12 +367,13 @@ Layout.prototype.update = function() { l._w = m.width; l._h = m.height; } }, "btn": function(l) { - l._h = 32; - l._w = 20 + l.label.length*12; + var m = l.src?g.imageMetrics("function"==typeof l.src?l.src():l.src):g.setFont("6x8",2).stringMetrics(l.label); + l._h = 16 + m.height; + l._w = 20 + m.width; }, "img": function(l) { - var m = g.imageMetrics("function"==typeof l.src?l.src():l.src); // get width and height out of image - l._w = m.width; - l._h = m.height; + var m = g.imageMetrics("function"==typeof l.src?l.src():l.src), s=l.scale||1; // get width and height out of image + l._w = m.width*s; + l._h = m.height*s; }, "": function(l) { // size should already be set up in width/height l._w = 0;