ability to depend on a specific app ID

Layout can display images in buttons
iOS and Android integration apps
pull/878/head
Gordon Williams 2021-11-04 17:16:02 +00:00
parent 47ba763a9d
commit 935d409f4c
18 changed files with 403 additions and 98 deletions

View File

@ -230,6 +230,7 @@ and which gives information about the app for the Launcher.
"tags": "", // comma separated tag list for searching "tags": "", // comma separated tag list for searching
"supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2 "supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2
"dependencies" : { "notify":"type" } // optional, app 'types' we depend on "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' // 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 "readme": "README.md", // if supplied, a link to a markdown-style text file
// that contains more information about this app (usage, etc) // that contains more information about this app (usage, etc)

View File

@ -32,17 +32,50 @@
{ {
"id": "messages", "id": "messages",
"name": "Messages", "name": "Messages",
"version": "0.01", "version": "0.02",
"description": "App to display notifications from iOS and Gadgetbridge (BETA)", "description": "App to display notifications from iOS and Gadgetbridge",
"icon": "app.png", "icon": "app.png",
"type": "app",
"tags": "tool,system", "tags": "tool,system",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{"name":"messages.app.js","url":"app.js"}, {"name":"messages.app.js","url":"app.js"},
{"name":"messages.img","url":"app-icon.js","evaluate":true}, {"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 "sortorder": -9
}, },
@ -216,7 +249,7 @@
"id": "gbridge", "id": "gbridge",
"name": "Gadgetbridge", "name": "Gadgetbridge",
"version": "0.24", "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", "icon": "app.png",
"type": "widget", "type": "widget",
"tags": "tool,system,android,widget", "tags": "tool,system,android,widget",

1
apps/android/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App!

1
apps/android/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4MA///xF9FstggwFDuEOAoc//gFJv/+AoZHBAgUB8/nwAFCBYIFCgYFB4AFHABdjCIPGAoPzAoPPAvpHFMpYFFPosAnk8NgYFdjEYfMo="))

2
apps/android/app.js Normal file
View File

@ -0,0 +1,2 @@
// Config app not implemented yet
load("messages.app.js");

BIN
apps/android/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

55
apps/android/boot.js Normal file
View File

@ -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 });
}
})();

1
apps/ios/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App!

1
apps/ios/app-icon.js Normal file
View File

@ -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"))

2
apps/ios/app.js Normal file
View File

@ -0,0 +1,2 @@
// Config app not implemented yet
load("messages.app.js");

BIN
apps/ios/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

129
apps/ios/boot.js Normal file
View File

@ -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
});
*/

2
apps/messages/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: New App!
0.02: Add 'messages' library

View File

@ -12,15 +12,10 @@
/* For example for maps: /* For example for maps:
GB({"t":"notify","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"}) // a message
GB({"t":"notify","id":2,"src":"Hangouts","title":"Gordon","body":"Hello world quite a lot of text here..."}) {"t":"add","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"}
GB({"t":"notify","id":3,"src":"Messages","title":"Ted","body":"Bed time."}) // maps
GB({"t":"notify","id":4,"src":"Messages","title":"Kailo","body":"Mmm... food"}) {"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="}
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"})
*/ */
@ -37,6 +32,21 @@ function saveMessages() {
require("Storage").writeJSON("messages.json",MESSAGES) 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) { function showMapMessage(msg) {
var m; var m;
var distance, street, target, eta; var distance, street, target, eta;
@ -50,48 +60,98 @@ function showMapMessage(msg) {
target = m[1]; target = m[1];
eta = m[2]; eta = m[2];
} else target=msg.body; } else target=msg.body;
layout = new Layout({ layout = new Layout({ type:"v", c: [
type:"v", c: [ {type:"txt", font:"6x15", label:target, bgCol:"#0f0", fillx:1, pad:2 },
{type:"txt", font:"6x15", label:target, bgCol:"#0f0", fillx:1, pad:2 }, {type:"h", bgCol:"#0f0", fillx:1, c: [
{type:"h", bgCol:"#0f0", fillx:1, c: [ {type:"txt", font:"6x8", label:"Towards" },
{type:"txt", font:"6x8", label:"Towards" }, {type:"txt", font:"6x15:2", label:street }
{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:"txt", font:"6x8:2", label:eta }
{type:"v", fillx:1, c: [ ]});
{type:"txt", font:"6x15:2", label:distance||"" } g.clearRect(Bangle.appRect);
]},
]},
{type:"txt", font:"6x8:2", label:eta }
]
});
g.clearRect(0,24,g.getWidth()-1,g.getHeight()-1);
layout.render(); layout.render();
Bangle.setUI("updown",function() { Bangle.setUI("updown",function() {
// any input to mark as not new and return to menu // any input to mark as not new and return to menu
msg.new = false; msg.new = false;
saveMessages(); saveMessages();
layout = undefined;
checkMessages(); 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) { function showMessage(msgid) {
var msg = MESSAGES.find(m=>m.id==msgid); var msg = MESSAGES.find(m=>m.id==msgid);
if (!msg) return checkMessages(); // go home if no message found if (!msg) return checkMessages(); // go home if no message found
if (msg.src=="Maps") return showMapMessage(msg); if (msg.src=="Maps") return showMapMessage(msg);
var m = msg.title+"\n"+msg.body; if (msg.id=="music") return showMusicMessage(msg);
E.showPrompt(m,{title:"Message", buttons : {"Read":"read", "Back":"back"}}).then(chosen => { // Normal text message display
if (chosen=="read") { var title=msg.title, titleFont = "6x15:2";
// any input to mark as not new and return to menu if (title) {
msg.new = false; var w = g.getWidth()-40;
saveMessages(); if (g.setFont(titleFont).stringWidth(title) > w)
checkMessages(); titleFont = "6x15";
} else { if (g.setFont(titleFont).stringWidth(title) > w)
checkMessages(true); 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) { function checkMessages(forceShowMenu) {
@ -112,24 +172,35 @@ function checkMessages(forceShowMenu) {
// Otherwise show a menu // Otherwise show a menu
E.showScroller({ E.showScroller({
h : 48, h : 48,
c : MESSAGES.length, c : MESSAGES.length+1,
draw : function(idx, r) {"ram" draw : function(idx, r) {"ram"
var msg = MESSAGES[idx-1]; var msg = MESSAGES[idx-1];
if (msg && msg.new) g.setBgColor("#4F4"); if (msg && msg.new) g.setBgColor("#4F4");
else g.setBgColor((idx&1) ? "#CFC" : "#9F9"); 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); 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; 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.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 (title) g.setFontAlign(-1,-1).setFont("12x20").drawString(title, x,r.y+2);
if (msg.body) { if (body) {
g.setFontAlign(-1,-1).setFont("6x8"); 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) { if (l.length>3) {
l = l.slice(0,3); l = l.slice(0,3);
l[l.length-1]+="..."; 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 => { select : idx => {

View File

@ -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();
};
})()

View File

@ -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);
}

2
core

@ -1 +1 @@
Subproject commit 5ef454a1acce54f6420015b519a7ecf461f9bc37 Subproject commit 59f80bb52a38da12cb272f9106cb3951b49dab2e

View File

@ -29,7 +29,9 @@ layoutObject has:
* `undefined` - blank, can be used for padding * `undefined` - blank, can be used for padding
* `"txt"` - a text label, with value `label` and `r` for text rotation. 'font' is required * `"txt"` - a text label, with value `label` and `r` for text rotation. 'font' is required
* `"btn"` - a button, with value `label` and callback `cb` * `"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 * `"custom"` - a custom block where `render(layoutObj)` is called to render
* `"h"` - Horizontal layout, `c` is an array of more `layoutObject` * `"h"` - Horizontal layout, `c` is an array of more `layoutObject`
* `"v"` - Veritical 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; this.lazy = options.lazy || false;
var btnList; var btnList;
Bangle.setUI(); // remove all existing input handlers
if (process.env.HWVERSION!=2) { if (process.env.HWVERSION!=2) {
// no touchscreen, find any buttons in 'layout' // no touchscreen, find any buttons in 'layout'
btnList = []; btnList = [];
@ -157,6 +160,7 @@ function Layout(layout, options) {
} }
} }
if (process.env.HWVERSION==2) { if (process.env.HWVERSION==2) {
// Handler for touch events // Handler for touch events
function touchHandler(l,e) { 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) { 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)); 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){ }, "btn":function(l){
var x = l.x+(0|l.pad); var x = l.x+(0|l.pad), y = l.y+(0|l.pad),
var y = l.y+(0|l.pad); w = l.w-(l.pad<<1), h = l.h-(l.pad<<1);
var w = l.w-(l.pad<<1);
var h = l.h-(l.pad<<1);
var poly = [ var poly = [
x,y+4, x,y+4,
x+4,y, x+4,y,
@ -259,10 +261,12 @@ Layout.prototype.render = function (l) {
x+4,y+h-1, x+4,y+h-1,
x,y+h-5, x,y+h-5,
x,y+4 x,y+4
]; ], bg = l.selected?g.theme.bgH:g.theme.bg2;
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); 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){ }, "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){ }, "custom":function(l){
l.render(l); l.render(l);
},"h":function(l) { l.c.forEach(render); }, },"h":function(l) { l.c.forEach(render); },
@ -363,12 +367,13 @@ Layout.prototype.update = function() {
l._w = m.width; l._h = m.height; l._w = m.width; l._h = m.height;
} }
}, "btn": function(l) { }, "btn": function(l) {
l._h = 32; var m = l.src?g.imageMetrics("function"==typeof l.src?l.src():l.src):g.setFont("6x8",2).stringMetrics(l.label);
l._w = 20 + l.label.length*12; l._h = 16 + m.height;
l._w = 20 + m.width;
}, "img": function(l) { }, "img": function(l) {
var m = g.imageMetrics("function"==typeof l.src?l.src():l.src); // get width and height out of image 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; l._w = m.width*s;
l._h = m.height; l._h = m.height*s;
}, "": function(l) { }, "": function(l) {
// size should already be set up in width/height // size should already be set up in width/height
l._w = 0; l._w = 0;