You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
470 lines
16 KiB
470 lines
16 KiB
/* MESSAGES is a list of: |
|
{id:int, |
|
src, |
|
title, |
|
subject, |
|
body, |
|
sender, |
|
tel:string, |
|
new:true // not read yet |
|
} |
|
*/ |
|
|
|
/* For example for maps: |
|
|
|
// a message |
|
require("messages").pushMessage({"t":"add","id":1575479849,"src":"Clock","title":"Timer","body":"test"}) |
|
// maps |
|
require("messages").pushMessage({"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="}); |
|
// call |
|
require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bob","body":"12421312",positive:true,negative:true}) |
|
*/ |
|
var Layout = require("Layout"); |
|
var settings = require('Storage').readJSON("messages.settings.json", true) || {}; |
|
var fontSmall = "6x8"; |
|
var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2"; |
|
var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; |
|
var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; |
|
var active; // active screen |
|
var openMusic = false; // go back to music screen after we handle something else? |
|
// hack for 2v10 firmware's lack of ':size' font handling |
|
try { |
|
g.setFont("6x8:2"); |
|
} catch (e) { |
|
g._setFont = g.setFont; |
|
g.setFont = function(f,s) { |
|
if (f.includes(":")) { |
|
f = f.split(":"); |
|
return g._setFont(f[0],f[1]); |
|
} |
|
return g._setFont(f,s); |
|
}; |
|
} |
|
|
|
/** this is a timeout if the app has started and is showing a single message |
|
but the user hasn't seen it (eg no user input) - in which case |
|
we should start a timeout for settings.unreadTimeout to return |
|
to the clock. */ |
|
var unreadTimeout; |
|
/// List of all our messages |
|
var MESSAGES = require("messages").getMessages(); |
|
if (Bangle.MESSAGES) { |
|
// fast loading messages |
|
Bangle.MESSAGES.forEach(m => require("messages").apply(m, MESSAGES)); |
|
delete Bangle.MESSAGES; |
|
} |
|
|
|
var onMessagesModified = function(type,msg) { |
|
if (msg.handled) return; |
|
msg.handled = true; |
|
require("messages").apply(msg, MESSAGES); |
|
// TODO: if new, show this new one |
|
if (msg && msg.id!=="music" && msg.new && active!="map" && |
|
!((require('Storage').readJSON('setting.json', 1) || {}).quiet)) { |
|
require("messages").buzz(msg.src); |
|
} |
|
if (msg && msg.id=="music") { |
|
if (msg.state && msg.state!="play") openMusic = false; // no longer playing music to go back to |
|
if (active!="music") return; // don't open music over other screens |
|
} |
|
showMessage(msg&&msg.id); |
|
}; |
|
Bangle.on("message", onMessagesModified); |
|
|
|
function saveMessages() { |
|
require("messages").write(MESSAGES); |
|
} |
|
E.on("kill", saveMessages); |
|
|
|
function showMapMessage(msg) { |
|
active = "map"; |
|
var m, distance, street, target, eta; |
|
m=msg.title.match(/(.*) - (.*)/); |
|
if (m) { |
|
distance = m[1]; |
|
street = m[2]; |
|
} else street=msg.title; |
|
m=msg.body.match(/(.*) - (.*)/); |
|
if (m) { |
|
target = m[1]; |
|
eta = m[2]; |
|
} else target=msg.body; |
|
layout = new Layout({ type:"v", c: [ |
|
{type:"txt", font:fontMedium, label:target, bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2 }, |
|
{type:"h", bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, c: [ |
|
{type:"txt", font:"6x8", label:"Towards" }, |
|
{type:"txt", font:fontLarge, 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:fontLarge, label:distance||"" } |
|
]}, |
|
]}, |
|
{type:"txt", font:"6x8:2", label:eta } |
|
]}); |
|
g.reset().clearRect(Bangle.appRect); |
|
layout.render(); |
|
function back() { // mark as not new and return to menu |
|
msg.new = false; |
|
layout = undefined; |
|
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:0}); |
|
} |
|
Bangle.setUI({mode:"updown", back: back}, back); // any input takes us back |
|
} |
|
|
|
let updateLabelsInterval; |
|
|
|
function showMusicMessage(msg) { |
|
active = "music"; |
|
// defaults, so e.g. msg.xyz.length doesn't error. `msg` should contain up to date info |
|
msg = Object.assign({artist: "", album: "", track: "Music"}, msg); |
|
openMusic = msg.state=="play"; |
|
var trackScrollOffset = 0; |
|
var artistScrollOffset = 0; |
|
var albumScrollOffset = 0; |
|
var trackName = ''; |
|
var artistName = ''; |
|
var albumName = ''; |
|
|
|
function fmtTime(s) { |
|
var m = Math.floor(s/60); |
|
s = (parseInt(s%60)).toString().padStart(2,0); |
|
return m+":"+s; |
|
} |
|
function reduceStringAndPad(text, offset, maxLen) { |
|
var sliceLength = offset + maxLen > text.length ? text.length - offset : maxLen; |
|
return text.substr(offset, sliceLength).padEnd(maxLen, " "); |
|
} |
|
function back() { |
|
clearInterval(updateLabelsInterval); |
|
updateLabelsInterval = undefined; |
|
openMusic = false; |
|
var wasNew = msg.new; |
|
msg.new = false; |
|
layout = undefined; |
|
if (wasNew) checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:0,openMusic:0}); |
|
else checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); |
|
} |
|
function updateLabels() { |
|
trackName = reduceStringAndPad(msg.track, trackScrollOffset, 13); |
|
artistName = reduceStringAndPad(msg.artist, artistScrollOffset, 21); |
|
albumName = reduceStringAndPad(msg.album, albumScrollOffset, 21); |
|
|
|
trackScrollOffset++; |
|
artistScrollOffset++; |
|
albumScrollOffset++; |
|
|
|
if ((trackScrollOffset + 13) > msg.track.length) trackScrollOffset = 0; |
|
if ((artistScrollOffset + 21) > msg.artist.length) artistScrollOffset = 0; |
|
if ((albumScrollOffset + 21) > msg.album.length) albumScrollOffset = 0; |
|
} |
|
updateLabels(); |
|
|
|
layout = new Layout({ type:"v", c: [ |
|
{type:"h", fillx:1, bgCol:g.theme.bg2, col: g.theme.fg2, c: [ |
|
{ type:"v", fillx:1, c: [ |
|
{ type:"txt", font:fontMedium, bgCol:g.theme.bg2, label:artistName, pad:2, id:"artist" }, |
|
{ type:"txt", font:fontMedium, bgCol:g.theme.bg2, label:albumName, pad:2, id:"album" } |
|
]} |
|
]}, |
|
{type:"txt", font:fontLarge, bgCol:g.theme.bg, label:trackName, fillx:1, filly:1, pad:2, id:"track" }, |
|
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):"--:--" } |
|
]}, { back : back }); |
|
g.reset().clearRect(Bangle.appRect); |
|
layout.render(); |
|
|
|
updateLabelsInterval = setInterval(function() { |
|
updateLabels(); |
|
layout.artist.label = artistName; |
|
layout.album.label = albumName; |
|
layout.track.label = trackName; |
|
layout.render(); |
|
}, 400); |
|
} |
|
|
|
function showMessageScroller(msg) { |
|
active = "scroller"; |
|
var bodyFont = fontBig; |
|
g.setFont(bodyFont); |
|
var lines = []; |
|
if (msg.title) lines = g.wrapString(msg.title, g.getWidth()-10) |
|
var titleCnt = lines.length; |
|
if (titleCnt) lines.push(""); // add blank line after title |
|
lines = lines.concat(g.wrapString(msg.body, g.getWidth()-10),["",/*LANG*/"< Back"]); |
|
E.showScroller({ |
|
h : g.getFontHeight(), // height of each menu item in pixels |
|
c : lines.length, // number of menu items |
|
// a function to draw a menu item |
|
draw : function(idx, r) { |
|
// FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12 |
|
g.setBgColor(idx<titleCnt ? g.theme.bg2 : g.theme.bg). |
|
setColor(idx<titleCnt ? g.theme.fg2 : g.theme.fg). |
|
clearRect(r.x,r.y,r.x+r.w, r.y+r.h); |
|
g.setFont(bodyFont).drawString(lines[idx], r.x, r.y); |
|
}, select : function(idx) { |
|
if (idx>=lines.length-2) |
|
showMessage(msg.id); |
|
}, |
|
back : () => showMessage(msg.id) |
|
}); |
|
} |
|
|
|
function showMessageSettings(msg) { |
|
active = "settings"; |
|
E.showMenu({"":{"title":/*LANG*/"Message"}, |
|
"< Back" : () => showMessage(msg.id), |
|
/*LANG*/"View Message" : () => { |
|
showMessageScroller(msg); |
|
}, |
|
/*LANG*/"Delete" : () => { |
|
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id); |
|
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); |
|
}, |
|
/*LANG*/"Mark Unread" : () => { |
|
msg.new = true; |
|
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); |
|
}, |
|
/*LANG*/"Mark all read" : () => { |
|
MESSAGES.forEach(msg => msg.new = false); |
|
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); |
|
}, |
|
/*LANG*/"Delete all messages" : () => { |
|
E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Delete All Messages"}).then(isYes => { |
|
if (isYes) { |
|
MESSAGES = []; |
|
} |
|
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); |
|
}); |
|
}, |
|
}); |
|
} |
|
|
|
function showMessage(msgid) { |
|
var msg = MESSAGES.find(m=>m.id==msgid); |
|
if (updateLabelsInterval) { |
|
clearInterval(updateLabelsInterval); |
|
updateLabelsInterval=undefined; |
|
} |
|
if (!msg) return checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic}); // go home if no message found |
|
if (msg.id=="music") { |
|
cancelReloadTimeout(); // don't auto-reload to clock now |
|
return showMusicMessage(msg); |
|
} |
|
if (msg.src=="Maps") { |
|
cancelReloadTimeout(); // don't auto-reload to clock now |
|
return showMapMessage(msg); |
|
} |
|
active = "message"; |
|
// Normal text message display |
|
var title=msg.title, titleFont = fontLarge, lines; |
|
var body=msg.body, bodyFont = fontLarge; |
|
// If no body, use the title text instead... |
|
if (body===undefined) { |
|
body = title; |
|
title = undefined; |
|
} |
|
if (title) { |
|
var w = g.getWidth()-48; |
|
if (g.setFont(titleFont).stringWidth(title) > w) { |
|
titleFont = fontBig; |
|
if (settings.fontSize!=1 && g.setFont(titleFont).stringWidth(title) > w) |
|
titleFont = fontMedium; |
|
} |
|
if (g.setFont(titleFont).stringWidth(title) > w) { |
|
lines = g.wrapString(title, w); |
|
title = (lines.length>2) ? lines.slice(0,2).join("\n")+"..." : lines.join("\n"); |
|
} |
|
} |
|
// If body of message is only two lines long w/ large font, use large font. |
|
|
|
if (body) { |
|
var w = g.getWidth()-10; |
|
if (g.setFont(bodyFont).stringWidth(body) > w * 2) { |
|
bodyFont = fontBig; |
|
if (settings.fontSize!=1 && g.setFont(bodyFont).stringWidth(body) > w * 3) |
|
bodyFont = fontMedium; |
|
} |
|
if (g.setFont(bodyFont).stringWidth(body) > w) { |
|
lines = g.setFont(bodyFont).wrapString(msg.body, w); |
|
var maxLines = Math.floor((g.getHeight()-110) / g.getFontHeight()); |
|
body = (lines.length>maxLines) ? lines.slice(0,maxLines).join("\n")+"..." : lines.join("\n"); |
|
} |
|
} |
|
function goBack() { |
|
layout = undefined; |
|
msg.new = false; // read mail |
|
cancelReloadTimeout(); // don't auto-reload to clock now |
|
checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic}); |
|
} |
|
var buttons = [ |
|
]; |
|
if (msg.positive) { |
|
buttons.push({type:"btn", src:atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA=="), cb:()=>{ |
|
msg.new = false; |
|
cancelReloadTimeout(); // don't auto-reload to clock now |
|
Bangle.messageResponse(msg,true); |
|
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic}); |
|
}}); |
|
} |
|
if (msg.negative) { |
|
if (buttons.length) buttons.push({width:32}); // nasty hack... |
|
buttons.push({type:"btn", src:atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA="), cb:()=>{ |
|
msg.new = false; |
|
cancelReloadTimeout(); // don't auto-reload to clock now |
|
Bangle.messageResponse(msg,false); |
|
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic}); |
|
}}); |
|
} |
|
|
|
|
|
layout = new Layout({ type:"v", c: [ |
|
{type:"h", fillx:1, bgCol:g.theme.bg2, col: g.theme.fg2, c: [ |
|
{ type:"v", fillx:1, c: [ |
|
{type:"txt", font:fontSmall, label:msg.src||/*LANG*/"Message", bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2, halign:1 }, |
|
title?{type:"txt", font:titleFont, label:title, bgCol:g.theme.bg2, col: g.theme.fg2, fillx:1, pad:2 }:{}, |
|
]}, |
|
{ type:"btn", |
|
src:require("messageicons").getImage(msg), |
|
col:require("messageicons").getColor(msg, {settings:settings, default:g.theme.fg2}), |
|
pad: 3, cb:()=>{ |
|
cancelReloadTimeout(); // don't auto-reload to clock now |
|
showMessageSettings(msg); |
|
} |
|
}, |
|
]}, |
|
{type:"txt", font:bodyFont, label:body, fillx:1, filly:1, pad:2, cb:()=>{ |
|
// allow tapping to show a larger version |
|
showMessageScroller(msg); |
|
} }, |
|
{type:"h",fillx:1, c: buttons} |
|
]},{back:goBack}); |
|
g.reset().clearRect(Bangle.appRect); |
|
layout.render(); |
|
} |
|
|
|
|
|
/* options = { |
|
clockIfNoMsg : bool |
|
clockIfAllRead : bool |
|
showMsgIfUnread : bool |
|
openMusic : bool // open music if it's playing |
|
} |
|
*/ |
|
function checkMessages(options) { |
|
options=options||{}; |
|
|
|
var goBack = false; |
|
while (MESSAGES.length && MESSAGES[0].title == "Timer") { |
|
MESSAGES.pop(0); |
|
goBack = true; |
|
} |
|
|
|
if (goBack) { |
|
return load(); |
|
} |
|
|
|
// If no messages, just show 'no messages' and return |
|
if (!MESSAGES.length) { |
|
if (!options.clockIfNoMsg) return E.showPrompt(/*LANG*/"No Messages",{ |
|
title:/*LANG*/"Messages", |
|
img:require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")), |
|
buttons : {/*LANG*/"Ok":1} |
|
}).then(() => { load() }); |
|
return load(); |
|
} |
|
|
|
// we have >0 messages |
|
var newMessages = MESSAGES.filter(m=>m.new&&m.id!="music"); |
|
// If we have a new message, show it |
|
if (options.showMsgIfUnread && newMessages.length) { |
|
delete newMessages[0].show; // stop us getting stuck here if we're called a second time |
|
showMessage(newMessages[0].id); |
|
// buzz after showMessage, so being busy during layout doesn't affect the buzz pattern |
|
if (global.BUZZ_ON_NEW_MESSAGE) { |
|
// this is set if we entered the messages app by loading `messagegui.new.js` |
|
// ... but only buzz the first time we view a new message |
|
global.BUZZ_ON_NEW_MESSAGE = false; |
|
// messages.buzz respects quiet mode - no need to check here |
|
require("messages").buzz(newMessages[0].src); |
|
} |
|
return; |
|
} |
|
// no new messages: show playing music? Only if we have playing music, or state=="show" (set by messagesmusic) |
|
if (options.openMusic && MESSAGES.some(m=>m.id=="music" && ((m.track && m.state=="play") || m.state=="show"))) |
|
return showMessage('music'); |
|
// no new messages - go to clock? |
|
if (options.clockIfAllRead && newMessages.length==0) |
|
return load(); |
|
active = "main"; |
|
// Otherwise show a menu |
|
E.showScroller({ |
|
h : 48, |
|
c : Math.max(MESSAGES.length,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11) |
|
draw : function(idx, r) {"ram" |
|
var msg = MESSAGES[idx]; |
|
if (msg && msg.new) g.setBgColor(g.theme.bgH).setColor(g.theme.fgH); |
|
else g.setBgColor(g.theme.bg).setColor(g.theme.fg); |
|
g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h); |
|
if (!msg) return; |
|
var x = r.x+2, title = msg.title, body = msg.body; |
|
var img = require("messageicons").getImage(msg); |
|
if (msg.id=="music") { |
|
title = msg.artist || /*LANG*/"Music"; |
|
body = msg.track; |
|
} |
|
if (img) { |
|
var fg = g.getColor(), |
|
col = require("messageicons").getColor(msg, {settings:settings, default:fg}); |
|
g.setColor(col).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering |
|
.setColor(fg); // only color the icon |
|
x += 50; |
|
} |
|
var m = msg.title+"\n"+msg.body, longBody=false; |
|
if (title) g.setFontAlign(-1,-1).setFont(fontBig).drawString(title, x,r.y+2); |
|
if (body) { |
|
g.setFontAlign(-1,-1).setFont("6x8"); |
|
var l = g.wrapString(body, r.w-(x+14)); |
|
if (l.length>3) { |
|
l = l.slice(0,3); |
|
l[l.length-1]+="..."; |
|
} |
|
longBody = l.length>2; |
|
g.drawString(l.join("\n"), x+10,r.y+20); |
|
} |
|
if (!longBody && msg.src) g.setFontAlign(1,1).setFont("6x8").drawString(msg.src, r.x+r.w-2, r.y+r.h-2); |
|
g.setColor("#888").fillRect(r.x,r.y+r.h-1,r.x+r.w-1,r.y+r.h-1); // dividing line between items |
|
}, |
|
select : idx => showMessage(MESSAGES[idx].id), |
|
back : () => load() |
|
}); |
|
} |
|
|
|
|
|
function cancelReloadTimeout() { |
|
if (!unreadTimeout) return; |
|
clearTimeout(unreadTimeout); |
|
unreadTimeout = undefined; |
|
} |
|
|
|
g.clear(); |
|
|
|
Bangle.loadWidgets(); |
|
require("messages").toggleWidget(false); |
|
Bangle.drawWidgets(); |
|
|
|
setTimeout(() => { |
|
if (!isFinite(settings.unreadTimeout)) settings.unreadTimeout=60; |
|
if (settings.unreadTimeout) |
|
unreadTimeout = setTimeout(load, settings.unreadTimeout*1000); |
|
// only openMusic on launch if music is new, or state=="show" (set by messagesmusic) |
|
var musicMsg = MESSAGES.find(m => m.id === "music"); |
|
checkMessages({ |
|
clockIfNoMsg: 0, clockIfAllRead: 0, showMsgIfUnread: 1, |
|
openMusic: ((musicMsg&&musicMsg.new) && settings.openMusic) || (musicMsg&&musicMsg.state=="show") }); |
|
}, 10); // if checkMessages wants to 'load', do that
|
|
|