mirror of https://github.com/espruino/BangleApps
1208 lines
43 KiB
JavaScript
1208 lines
43 KiB
JavaScript
/* 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
|
|
{"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="}
|
|
// call
|
|
{"t":"add","id":"call","src":"Phone","name":"Bob","number":"12421312",positive:true,negative:true}
|
|
*/
|
|
{
|
|
const B2 = process.env.HWVERSION>1, // Bangle.js 2?
|
|
RIGHT = 1, LEFT = -1, // swipe directions
|
|
UP = -1, DOWN = 1; // updown directions
|
|
const Layout = require("Layout");
|
|
|
|
const settings = () => require("messagegui").settings();
|
|
const fontTiny = "6x8"; // fixed size, don't use this for important things
|
|
let fontNormal;
|
|
// setFont() is also called after we close the settings screen
|
|
const setFont = function() {
|
|
const fontSize = settings().fontSize;
|
|
if (fontSize===0) // small
|
|
fontNormal = g.getFonts().includes("6x15") ? "6x15" : "6x8:2";
|
|
else if (fontSize===2) // large
|
|
fontNormal = g.getFonts().includes("6x15") ? "6x15:2" : "6x8:4";
|
|
else // medium
|
|
fontNormal = g.getFonts().includes("12x20") ? "12x20" : "6x8:3";
|
|
};
|
|
setFont();
|
|
|
|
let active, back; // active screen, last active screen
|
|
|
|
/// List of all our messages
|
|
let MESSAGES;
|
|
const saveMessages = function() {
|
|
const noSave = ["alarm", "call", "music"]; // assume these are outdated once we close the app
|
|
noSave.forEach(id => remove({id: id}));
|
|
require("messages").write(MESSAGES
|
|
.filter(m => m.id && !noSave.includes(m.id))
|
|
.map(m => {
|
|
delete m.show;
|
|
return m;
|
|
})
|
|
);
|
|
};
|
|
const uiRemove = function() {
|
|
if (musicTimeout) clearTimeout(musicTimeout);
|
|
layout = undefined;
|
|
Bangle.removeListener("message", onMessage);
|
|
saveMessages();
|
|
clearUnreadStuff();
|
|
delete Bangle.appRect;
|
|
};
|
|
const quitApp = () => load(); // TODO: revert to Bangle.showClock after fixing memory leaks
|
|
try {
|
|
MESSAGES = require("messages").getMessages();
|
|
// Apply fast loaded messages
|
|
(Bangle.MESSAGES || []).forEach(m => require("messages").apply(m, MESSAGES));
|
|
delete Bangle.MESSAGES;
|
|
// Write them back to storage when we're done
|
|
E.on("kill", saveMessages);
|
|
} catch(e) {
|
|
g.reset().clear();
|
|
E.showPrompt(/*LANG*/"Message file corrupt, erase all messages?", {title:/*LANG*/"Delete All Messages"}).then(isYes => {
|
|
// We are troubleshooting, so do a clean "load" in both cases (instead of Bangle.load)
|
|
if (isYes) { // OK: erase message file and reload this app
|
|
require("messages").clearAll();
|
|
load("messagelist.app.js");
|
|
} else {
|
|
load(); // well, this app won't work... let's go back to the clock
|
|
}
|
|
});
|
|
}
|
|
|
|
const setUI = function(options, cb) {
|
|
options = Object.assign({remove: () => uiRemove()}, options);
|
|
Bangle.setUI(options, cb);
|
|
Bangle.on("message", onMessage);
|
|
};
|
|
|
|
const remove = function(msg) {
|
|
if (msg.id==="call") call = undefined;
|
|
else if (msg.id==="map") map = undefined;
|
|
else if (msg.id==="alarm") alarm = undefined;
|
|
else if (msg.id==="music") music = undefined;
|
|
else MESSAGES = MESSAGES.filter(m => m.id!==msg.id);
|
|
};
|
|
const buzz = function(msg) {
|
|
return require("messages").buzz(msg.src);
|
|
};
|
|
const show = function(msg) {
|
|
delete msg.show; // don't show this again
|
|
if (msg.id==="call") showCall(msg);
|
|
else if (msg.id==="map") showMap(msg);
|
|
else if (msg.id==="alarm") showAlarm(msg);
|
|
else if (msg.id==="music") showMusic(msg);
|
|
else showMessage(msg);
|
|
};
|
|
|
|
const onMessage = function(type, msg) {
|
|
if (msg.handled) return;
|
|
msg.handled = true;
|
|
switch(type) {
|
|
case "call":
|
|
return onCall(msg);
|
|
case "music":
|
|
return onMusic(msg);
|
|
case "map":
|
|
return onMap(msg);
|
|
case "alarm":
|
|
return onAlarm(msg);
|
|
case "text":
|
|
return onText(msg);
|
|
case "clearAll":
|
|
MESSAGES = [];
|
|
if (["messages", "menu"].includes(active)) showMenu();
|
|
break;
|
|
default:
|
|
E.showAlert(/*LANG*/"Unknown message type:"+"\n"+type).then(goBack);
|
|
}
|
|
};
|
|
Bangle.on("message", onMessage);
|
|
|
|
const onCall = function(msg) {
|
|
if (msg.t==="remove") {
|
|
call = undefined;
|
|
return exitScreen("call");
|
|
}
|
|
// incoming call: show it
|
|
call = msg;
|
|
buzz(call);
|
|
showCall();
|
|
};
|
|
const onAlarm = function(msg) {
|
|
if (msg.t==="remove") {
|
|
alarm = undefined;
|
|
return exitScreen("alarm");
|
|
}
|
|
alarm = msg;
|
|
buzz(alarm);
|
|
showAlarm();
|
|
};
|
|
let musicTimeout;
|
|
const onMusic = function(msg) {
|
|
const hadMusic = !!music;
|
|
if (musicTimeout) clearTimeout(musicTimeout);
|
|
musicTimeout = undefined;
|
|
if (msg.t==="remove") {
|
|
music = undefined;
|
|
if (active==="main" && hadMusic) return showMain(); // refresh menu: remove "Music" entry (if not always visible)
|
|
else return exitScreen("music");
|
|
}
|
|
|
|
music = Object.assign({}, music, msg);
|
|
|
|
// auto-close after being paused
|
|
if (music.state!=="play") musicTimeout = setTimeout(function() {
|
|
musicTimeout = undefined;
|
|
if (active==="music" && (!music || music.state!=="play")) quitApp();
|
|
}, 60*1000); // paused for 1 minute
|
|
// auto-close after "playing" way beyond song duration (because "stop" messages don't seem to exist)
|
|
else musicTimeout = setTimeout(function() {
|
|
musicTimeout = undefined;
|
|
if (active==="music" && (!music || music.state==="play")) quitApp();
|
|
}, 2*Math.max(music.dur || 0, 5*60)*1000); // playing: assume ended after twice song duration, or at least 10 minutes
|
|
|
|
if (active==="music") showMusic(); // update music screen
|
|
else if (active==="main" && !hadMusic) {
|
|
if (settings().openMusic && music.state==="play" && music.track) showMusic();
|
|
else showMain(); // refresh menu: add "Music" entry
|
|
}
|
|
};
|
|
const onMap = function(msg) {
|
|
const hadMap = !!map;
|
|
if (msg.t==="remove") {
|
|
map = undefined;
|
|
if (back==="map") back = undefined;
|
|
if (active==="main" && hadMap) return showMain(); // refresh menu: remove "Map" entry
|
|
else return exitScreen("map");
|
|
}
|
|
map = msg;
|
|
if (["map", "music"].includes(active)) showMap(); // update map screen, or switch away from music (not other screens)
|
|
else if (active==="main" && !hadMap) showMain(); // refresh menu: add "Map" entry
|
|
};
|
|
const onText = function(msg) {
|
|
require("messages").apply(msg, MESSAGES);
|
|
const mIdx = MESSAGES.findIndex(m => m.id===msg.id);
|
|
if (!MESSAGES[mIdx]) if (back==="messages") back = undefined;
|
|
if (active==="main") showMain(); // update message count
|
|
if (MESSAGES.length===0) exitScreen("messages"); // removed last message
|
|
else if (active==="messages") showMessage(messageNum);
|
|
if (msg.new) buzz(msg);
|
|
if (active!=="call") {// don't switch away from incoming call
|
|
if (active!=="messages" || messageNum===mIdx) showMessage(mIdx);
|
|
}
|
|
if (active==="messages") drawFooter(); // update footer with new number of messages
|
|
};
|
|
|
|
const getImage = function(msg, def) {
|
|
// app icons, provided by `messages` app
|
|
return require("messageicons").getImage(msg);
|
|
};
|
|
const getImageColor = function(msg, def) {
|
|
// app colors, provided by `messages` app
|
|
return require("messageicons").getColor(msg, {default: def});
|
|
};
|
|
const getIcon = function(icon) {
|
|
return require("messagegui").getIcon(icon);
|
|
};
|
|
const getIconColor = function(icon) {
|
|
return require("messagegui").getColor(icon);
|
|
};
|
|
|
|
/*
|
|
* icons should be 24x24px with 1bpp colors and transparancy
|
|
*/
|
|
const getMessageImage = function(msg) {
|
|
if (msg.img) return atob(msg.img);
|
|
if (msg.id==="music") return getIcon("Music");
|
|
if (msg.id==="back") return getIcon("Back");
|
|
const s = (msg.src || "").toLowerCase();
|
|
|
|
return getImage(s, "notification");
|
|
};
|
|
|
|
const showMap = function() {
|
|
setActive("map");
|
|
delete map.new;
|
|
let m, distance, street, target, eta;
|
|
m = map.title.match(/(.*) - (.*)/);
|
|
if (m) {
|
|
distance = m[1];
|
|
street = m[2];
|
|
} else {
|
|
street = map.title;
|
|
}
|
|
m = map.body.match(/(.*) - (.*)/);
|
|
if (m) {
|
|
target = m[1];
|
|
eta = m[2];
|
|
} else {
|
|
target = map.body;
|
|
}
|
|
let layout = new Layout({
|
|
type: "v", c: [
|
|
{type: "txt", font: fontNormal, 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: fontNormal, label: street},
|
|
]
|
|
},
|
|
{
|
|
type: "h", fillx: 1, filly: 1, c: [
|
|
map.img ? {type: "img", src: () => atob(map.img), scale: 2} : {},
|
|
{
|
|
type: "v", fillx: 1, c: [
|
|
{type: "txt", font: fontNormal, label: distance || ""},
|
|
]
|
|
},
|
|
]
|
|
},
|
|
{type: "txt", font: "6x8:2", label: eta}
|
|
]
|
|
});
|
|
layout.render();
|
|
// go back on any input
|
|
setUI({
|
|
mode: "custom",
|
|
back: goBack,
|
|
btn: b => {
|
|
if (B2 || b===2) goBack();
|
|
},
|
|
swipe: dir => {
|
|
if (dir===RIGHT) showMain();
|
|
},
|
|
});
|
|
};
|
|
|
|
const toggleMusic = function() {
|
|
const mc = cmd => {
|
|
if (Bangle.musicControl) Bangle.musicControl(cmd);
|
|
};
|
|
if (!music) {
|
|
music = {state: "play"};
|
|
mc("play");
|
|
} else if (music.state==="play") {
|
|
music.state = "pause";
|
|
mc("pause");
|
|
} else {
|
|
music.state = "play";
|
|
mc("play");
|
|
}
|
|
if (layout && layout.musicIcon) {
|
|
// musicIcon/musicToggle .src returns icon based on current music.state
|
|
layout.update(layout.musicIcon);
|
|
if (layout.musicToggle) layout.update(layout.musicToggle);
|
|
layout.render();
|
|
}
|
|
};
|
|
|
|
const doMusic = function(action) {
|
|
if (!Bangle.musicControl) return;
|
|
Bangle.buzz(50);
|
|
if (action==="toggle") toggleMusic();
|
|
else Bangle.musicControl(action);
|
|
};
|
|
const showMusic = function() {
|
|
if (active!==music) setActive("music");
|
|
if (!music) music = {track: "<unknown>", artist: "<unknown>", album: "", state: "pause"};
|
|
delete music.new;
|
|
const w = Bangle.appRect.w-50; // title/album need to leave room for icon
|
|
let artist, album;
|
|
if (music.album && settings().showAlbum) {
|
|
// max 2 lines for artist/album
|
|
artist = g.setFont(fontNormal).wrapString(music.artist, w).slice(0, 2).join("\n");
|
|
album = g.wrapString(music.album, w).slice(0, 2).join("\n");
|
|
} else {
|
|
// no album: artist gets 3 lines
|
|
artist = g.setFont(fontNormal).wrapString(music.artist, w).slice(0, 3).join("\n");
|
|
album = "";
|
|
}
|
|
// place (subtitle) on a new line
|
|
let track = music.track.replace(/ \(/, "\n(");
|
|
track = g.wrapString(track, Bangle.appRect.w).slice(0, 5).join("\n");
|
|
// "unknown" n/c/dur can show up as -1
|
|
let num, dur;
|
|
if ("n" in music && music.n>0) {
|
|
num = "#"+music.n;
|
|
if ("c" in music && music.c>0) {
|
|
num += "/"+music.c;
|
|
}
|
|
num = {type: "txt", font: fontTiny, bgCol: g.theme.bg, label: num};
|
|
}
|
|
if ("dur" in music && music.dur>0) {
|
|
dur = Math.floor(music.dur/60)+":"+(music.dur%60).toString().padStart(2, "0");
|
|
dur = {type: "txt", font: fontTiny, bgCol: g.theme.bg, label: dur};
|
|
}
|
|
let info;
|
|
if (num && dur) info = {type: "h", fillx: 1, c: [{fillx: 1}, dur, {fillx: 1}, num, {fillx: 1},]};
|
|
else if (num) info = num;
|
|
else if (dur) info = dur;
|
|
else info = {};
|
|
|
|
layout = new Layout({
|
|
type: "v", c: [
|
|
{
|
|
type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [
|
|
{
|
|
id: "musicIcon", type: "img", pad: 10, bgCol: g.theme.bg2, col: g.theme.fg2
|
|
, src: () => getIcon((music.state==="play") ? "music" : "pause")
|
|
},
|
|
{
|
|
type: "v", fillx: 1, c: [
|
|
{type: "txt", font: fontNormal, col: g.theme.fg2, bgCol: g.theme.bg2, label: artist, pad: 2, id: "artist"},
|
|
album ? {type: "txt", font: fontNormal, col: g.theme.fg2, bgCol: g.theme.bg2, label: album, pad: 2, id: "album"} : {},
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{type: "txt", halign: 0, font: fontNormal, bgCol: g.theme.bg, label: track, fillx: 1, filly: 1, pad: 2, id: "track"},
|
|
settings().musicButtons ? {
|
|
type: "h", fillx: 1, c: [
|
|
B2 ? {} : {width: 4},
|
|
{
|
|
type: "btn", id: "previous", cb: () => doMusic("previous")
|
|
, src: () => getIcon("previous")
|
|
},
|
|
{fillx: 1},
|
|
{
|
|
type: "btn", id: "musicToggle", cb: () => doMusic("toggle")
|
|
, src: () => getIcon((music.state==="play") ? "pause" : "play")
|
|
},
|
|
{fillx: 1},
|
|
{
|
|
type: "btn", id: "next", cb: () => doMusic("next")
|
|
, src: () => getIcon("next")
|
|
},
|
|
B2 ? {} : {width: 4},
|
|
]
|
|
} : {},
|
|
info,
|
|
]
|
|
});
|
|
layout.render();
|
|
let options = {mode: "updown"};
|
|
// B1 with buttons: left hand side of screen is used for "previous"
|
|
if (B2 || !settings().musicButtons) options.back = goBack;
|
|
setUI(options, ud => {
|
|
if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup");
|
|
else {
|
|
if (B2 || settings().musicButtons) goBack(); // B1 left-hand touch is "previous", so we need a way to go back
|
|
else doMusic("toggle");
|
|
}
|
|
});
|
|
|
|
Bangle.swipeHandler = dir => {
|
|
if (dir!==0) doMusic(dir===RIGHT ? "previous" : "next");
|
|
};
|
|
Bangle.on("swipe", Bangle.swipeHandler);
|
|
|
|
if (Bangle.touchHandler) Bangle.removeListener("touch", Bangle.touchHandler);
|
|
if (settings().musicButtons) {
|
|
// visible buttons
|
|
// left = previous, middle = toggle, right = next
|
|
if (B2) Bangle.touchHandler = (_side, xy) => {
|
|
// accept touches on the whole bottom and pick the closest button
|
|
if (xy.y<Bangle.appRect.y+Bangle.appRect.h/2) return; // ignore touches top half
|
|
if (xy.x<Bangle.appRect.w/3) doMusic("previous");
|
|
else if (xy.x>2*Bangle.appRect.w/3) doMusic("next");
|
|
else doMusic("toggle");
|
|
};
|
|
else Bangle.touchHandler = (side) => {
|
|
if (side===1) doMusic("previous");
|
|
if (side===2) doMusic("next");
|
|
if (side===3) doMusic("toggle");
|
|
};
|
|
} else {
|
|
// no buttons: touch = toggle
|
|
// B2 setUI sets touchHandler, override that (we only want up/down swipes from the UI)
|
|
Bangle.touchHandler = (side, e) => {
|
|
// B1: side 1 (left) = back, B2: only toggle for e outside widget area
|
|
if ((!B2 && side>1) || (B2 && e.y>Bangle.appRect.y)) doMusic("toggle");
|
|
};
|
|
}
|
|
Bangle.on("touch", Bangle.touchHandler);
|
|
};
|
|
|
|
let layout;
|
|
|
|
const clearStuff = function() {
|
|
delete Bangle.appRect;
|
|
layout = undefined;
|
|
setUI();
|
|
g.reset().clearRect(Bangle.appRect);
|
|
};
|
|
const setActive = function(screen, args) {
|
|
clearStuff();
|
|
if (active && screen!==active) back = active;
|
|
if (screen==="messages") messageNum = args;
|
|
active = screen;
|
|
};
|
|
/**
|
|
* Go back to previous screen, preserving history
|
|
*/
|
|
const goBack = function() {
|
|
if (back==="call" && call) showCall();
|
|
else if (back==="map" && map) showMap();
|
|
else if (back==="music" && music) showMusic();
|
|
else if (back==="messages" && MESSAGES.length) showMessage();
|
|
else if (back) showMain(); // previous screen was "main", or no longer valid
|
|
else quitApp(); // no previous screen: go back to clock
|
|
};
|
|
/**
|
|
* Leave screen, and make sure goBack() won't take us there anymore;
|
|
* @param {string} screen
|
|
*/
|
|
const exitScreen = function(screen) {
|
|
if (back===screen) back = (active==="main") ? undefined : "main";
|
|
if (active===screen) {
|
|
active = undefined;
|
|
goBack();
|
|
}
|
|
};
|
|
const showMain = function() {
|
|
setActive("main");
|
|
let grid = {"": {title:/*LANG*/"Messages", align: 0, back: load}};
|
|
if (call) grid[/*LANG*/"Incoming Call"] = {icon: "Phone", cb: showCall};
|
|
if (alarm) grid[/*LANG*/"Alarm"] = {icon: "Alarm", cb: showAlarm};
|
|
const unread = MESSAGES.filter(m => m.new).length;
|
|
if (unread) {
|
|
grid[unread+" "+/*LANG*/"New"] = {icon: "Unread", cb: () => showMessage(MESSAGES.findIndex(m => m.new))};
|
|
grid[/*LANG*/"All"+` (${MESSAGES.length})`] = {icon: "Notification", cb: showMessage};
|
|
} else {
|
|
const allLabel = MESSAGES.length+" "+(MESSAGES.length===1 ?/*LANG*/"Message" :/*LANG*/"Messages");
|
|
if (MESSAGES.length) grid[allLabel] = {icon: "Notification", cb: showMessage};
|
|
else grid[/*LANG*/"No Messages"] = {icon: "Neg", cb: load};
|
|
}
|
|
if (unread<MESSAGES.length) {
|
|
grid[/*LANG*/"Dismiss Read"] = {
|
|
icon: "Trash", cb: () => {
|
|
E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Dismiss Read Messages"}).then(isYes => {
|
|
if (isYes) {
|
|
MESSAGES.filter(m => !m.new).forEach(msg => {
|
|
Bangle.messageResponse(msg, false);
|
|
remove(msg);
|
|
});
|
|
}
|
|
showMain();
|
|
});
|
|
}
|
|
};
|
|
}
|
|
if (map) grid[/*LANG*/"Map"] = {icon: "Map", cb: showMap};
|
|
if (music || settings().alwaysShowMusic) grid[/*LANG*/"Music"] = {icon: "Music", cb: showMusic};
|
|
grid[/*LANG*/"settings"] = {icon: "settings", cb: showSettings};
|
|
showGrid(grid);
|
|
};
|
|
const clamp = function(val, min, max) {
|
|
if (val<min) return min;
|
|
if (val>max) return max;
|
|
return val;
|
|
};
|
|
/**
|
|
* Show grid of labeled buttons,
|
|
*
|
|
* items:
|
|
* {
|
|
* cb: callback,
|
|
* img: button image,
|
|
* icon: icon name, // string, use getIcon(icon) instead of img
|
|
* col: icon color, // optional: defaults to getColor(icon)
|
|
* }
|
|
* "" item is options:
|
|
* {
|
|
* title: string,
|
|
* back: callback,
|
|
* rows/cols: (optional) fit to this many columns/rows, omit for automatic fit
|
|
* align: bottom row alignment if items don't fit perfectly into a grid
|
|
* -1: left
|
|
* 1: right
|
|
* 0: left, but move final button to the right
|
|
* undefined: spread (can be unaligned with rest of grid!)
|
|
* }
|
|
* @param items
|
|
*/
|
|
const showGrid = function(items) {
|
|
clearStuff();
|
|
const options = items[""] || {},
|
|
back = options.back || items["< Back"];
|
|
const keys = Object.keys(items).filter(k => k!=="" && k!=="< Back");
|
|
let cols;
|
|
if (options.cols) {
|
|
cols = options.cols;
|
|
} else if (options.rows) {
|
|
cols = Math.ceil(keys.length/options.rows);
|
|
} else {
|
|
const rows = Math.round(Math.sqrt(keys.length));
|
|
cols = Math.ceil(keys.length/rows);
|
|
}
|
|
|
|
let l = {type: "v", c: []};
|
|
if (options.title) {
|
|
l.c.push({id: "title", type: "txt", label: options.title, font: (B2 ? "12x20" : "6x8:2"), fillx: 1});
|
|
}
|
|
const w = Bangle.appRect.w/cols, // set explicit width, because labels can stick out
|
|
bgs = [g.theme.bgH, g.theme.bg2], // background colors used for buttons
|
|
newRow = () => ({type: "h", filly: 1, c: []});
|
|
let row = newRow(),
|
|
cbs = [[]]; // callbacks for Bangle.js 2 touchHandler below
|
|
keys.forEach(key => {
|
|
const item = items[key],
|
|
label = g.setFont(fontTiny).wrapString(key, w).join("\n");
|
|
let color = "col" in item ? item.col : getIconColor(item.icon || "Unknown");
|
|
if (color && bgs.includes(g.setColor(color).getColor())) color = undefined; // make sure button is not invisible
|
|
row.c.push({
|
|
type: "v", pad: 2, width: w, c: [
|
|
{
|
|
type: "btn",
|
|
src: item.img || (() => getIcon(item.icon || "Unknown")),
|
|
col: color,
|
|
cb: B2
|
|
? undefined // We handle B2 touches below
|
|
: () => setTimeout(item.cb), // prevent MEMORY error from running cb() inside the Layout touchHandler
|
|
},
|
|
{height: 2},
|
|
{type: "txt", label: label, font: fontTiny},
|
|
]
|
|
});
|
|
if (B2) cbs[cbs.length-1].push(item.cb);
|
|
if (row.c.length>=cols) {
|
|
l.c.push(row);
|
|
row = newRow();
|
|
if (B2) cbs.push([]);
|
|
}
|
|
});
|
|
if (row.c.length) {
|
|
if (options.align!==undefined) {
|
|
const filler = {width: w*(cols-row.c.length)};
|
|
if (options.align=== -1) row.c.unshift(filler); // left
|
|
else if (options.align===1) row.c.push(filler); // right
|
|
else if (options.align===0) row.c.splice(row.c.length-1, 0, filler); // left, but final item on right
|
|
}
|
|
l.c.push(row);
|
|
}
|
|
layout = new Layout(l, {back: back});
|
|
layout.render();
|
|
|
|
if (B2) {
|
|
// override touchHandler: no need to hit buttons exactly, just pick the nearest
|
|
if (Bangle.touchHandler) Bangle.removeListener("touch", Bangle.touchHandler);
|
|
Bangle.touchHandler = (side, xy) => {
|
|
if (xy.y<=Bangle.appRect.y) return; // widgetbar: ignore
|
|
let rows = l.c.length,
|
|
y = Bangle.appRect.y, h = Bangle.appRect.h;
|
|
if (options.title) {
|
|
rows--;
|
|
y += layout.title.h;
|
|
h -= layout.title.h;
|
|
}
|
|
const r = clamp(Math.floor(rows*(xy.y-y)/h), 0, rows-1); // row (0-indexed)
|
|
let c; // column (0-indexed)
|
|
if (r<rows-1) {
|
|
// regular row
|
|
c = clamp(Math.floor(cols*(xy.x-Bangle.appRect.x)/Bangle.appRect.w), 0, cols-1);
|
|
} else {
|
|
// bottom row: layout depends on align
|
|
if (options.align=== -1) { // left
|
|
c = clamp(Math.floor(cols*(xy.x-Bangle.appRect.x)/Bangle.appRect.w), 0, cols-1);
|
|
} else if (options.align===1) { // right
|
|
c = clamp(Math.floor(cols*(xy.x-Bangle.appRect.x)/Bangle.appRect.w)+(cols-cbs[r].length), 0, cols-1);
|
|
} else if (options.align===0) { // left, but with final item on right
|
|
c = clamp(Math.floor(cols*(xy.x-Bangle.appRect.x)/Bangle.appRect.w), 0, cols-1);
|
|
if (c===cols-1) c = cbs[r].length-1; // final row=last item
|
|
else if (c>cbs[r].length-2) return; // gap before final item
|
|
} else { // spread
|
|
c = clamp(Math.floor(cbs[r].length*(xy.x-Bangle.appRect.x)/Bangle.appRect.w), 0, cols-1);
|
|
}
|
|
}
|
|
if (r<cbs.length && c<cbs[r].length) {// should always be true
|
|
Bangle.buzz(50);
|
|
cbs[r][c]();
|
|
}
|
|
};
|
|
Bangle.on("touch", Bangle.touchHandler);
|
|
}
|
|
};
|
|
|
|
const showSettings = function() {
|
|
setActive("settings");
|
|
eval(require("Storage").read("messagelist.settings.js"))(() => {
|
|
setFont();
|
|
showMain();
|
|
});
|
|
};
|
|
const showCall = function() {
|
|
setActive("call");
|
|
delete call.new;
|
|
Bangle.setLocked(false);
|
|
Bangle.setLCDPower(1);
|
|
|
|
const w = g.getWidth()-48,
|
|
lines = g.setFont(fontNormal).wrapString(call.title, w),
|
|
title = (lines.length>2) ? lines.slice(0, 2).join("\n")+"..." : lines.join("\n");
|
|
const respond = function(accept) {
|
|
Bangle.buzz(50);
|
|
Bangle.messageResponse(call, accept);
|
|
remove(call);
|
|
call = undefined;
|
|
goBack();
|
|
};
|
|
let options = {};
|
|
if (!B2) {
|
|
options.btns = [
|
|
{
|
|
label:/*LANG*/"accept",
|
|
cb: () => respond(true),
|
|
}, {
|
|
label:/*LANG*/"ignore",
|
|
cb: goBack,
|
|
}, {
|
|
label:/*LANG*/"reject",
|
|
cb: () => respond(false),
|
|
}
|
|
];
|
|
}
|
|
|
|
layout = new Layout({
|
|
type: "v", c: [
|
|
{
|
|
type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [
|
|
{type: "img", pad: 10, src: () => getIcon("phone"), col: getIconColor("phone")},
|
|
{
|
|
type: "v", fillx: 1, c: [
|
|
{type: "txt", font: fontTiny, label: call.src ||/*LANG*/"Incoming Call", bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, pad: 2, halign: 1},
|
|
title ? {type: "txt", font: fontNormal, label: title, bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, pad: 2} : {},
|
|
]
|
|
},
|
|
]
|
|
},
|
|
{type: "txt", font: fontNormal, label: call.body, fillx: 1, filly: 1, pad: 2, wrap: true},
|
|
{
|
|
type: "h", fillx: 1, c: [
|
|
// button callbacks won't actually be used: setUI below overrides the touchHandler set by Layout
|
|
{type: B2 ? "btn" : "img", src: () => getIcon("Neg"), cb: () => respond(false)},
|
|
{fillx: 1},
|
|
{type: B2 ? "btn" : "img", src: () => getIcon("Pos"), cb: () => respond(true)},
|
|
]
|
|
}
|
|
]
|
|
}, options);
|
|
layout.render();
|
|
setUI({
|
|
mode: "custom",
|
|
back: goBack,
|
|
touch: (side, xy) => {
|
|
if (B2 && xy.y<Bangle.appRect.y) return;
|
|
if (side===1) respond(false);
|
|
if (side===2) respond(true);
|
|
},
|
|
btn: b => {
|
|
if (B2 || b===2) goBack();
|
|
else if (b===1) respond(true);
|
|
else respond(false);
|
|
},
|
|
swipe: dir => {
|
|
if (dir===RIGHT) showMain();
|
|
},
|
|
});
|
|
};
|
|
const showAlarm = function() {
|
|
// dismissing alarms doesn't seem to work, so this is simple */
|
|
setActive("alarm");
|
|
delete alarm.new;
|
|
Bangle.setLocked(false);
|
|
Bangle.setLCDPower(1);
|
|
|
|
const w = g.getWidth()-48,
|
|
lines = g.setFont(fontNormal).wrapString(alarm.title, w),
|
|
title = (lines.length>2) ? lines.slice(0, 2).join("\n")+"..." : lines.join("\n");
|
|
layout = new Layout({
|
|
type: "v", c: [
|
|
{
|
|
type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [
|
|
alarm.body ? {type: "img", pad: 10, src: () => getIcon("alarm"), col: getIconColor("alarm")} : {},
|
|
{type: "txt", font: fontNormal, label: title ||/*LANG*/"Alarm", bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, pad: 2, halign: 1},
|
|
]
|
|
},
|
|
alarm.body
|
|
? {type: "txt", font: fontNormal, label: alarm.body, fillx: 1, filly: 1, pad: 2, wrap: true}
|
|
: {type: "img", pad: 10, scale: 3, src: () => getIcon("alarm"), col: getIconColor("alarm")},
|
|
]
|
|
});
|
|
layout.render();
|
|
setUI({
|
|
mode: "custom",
|
|
back: goBack,
|
|
btn: b => {
|
|
if (B2 || b===2) goBack();
|
|
},
|
|
swipe: dir => {
|
|
if (dir===RIGHT) showMain();
|
|
},
|
|
});
|
|
};
|
|
/**
|
|
* Send message response, and delete it from list
|
|
* @param {string|boolean} reply Response text, false to dismiss (true to open on phone)
|
|
*/
|
|
const respondToMessage = function(reply) {
|
|
const msg = MESSAGES[messageNum];
|
|
if (msg) {
|
|
Bangle.messageResponse(msg, reply);
|
|
if (reply===false) remove(msg);
|
|
}
|
|
if (MESSAGES.length<1) goBack(); // no more messages
|
|
else showMessage((msg && reply===false) ? messageNum : messageNum+1); // show next message
|
|
};
|
|
const showMessageActions = function() {
|
|
let title = MESSAGES[messageNum].title || "";
|
|
if (g.setFont(fontNormal).stringMetrics(title).width>Bangle.appRect.w-(B2 ? 0 : 20)) {
|
|
title = g.wrapString("..."+title, Bangle.appRect.w-(B2 ? 0 : 20))[0].substring(3)+"...";
|
|
}
|
|
clearStuff();
|
|
let grid = {
|
|
"": {
|
|
title: title ||/*LANG*/"Message",
|
|
back: () => showMessage(messageNum),
|
|
cols: 3, // fit all replies on first row, dismiss on bottom
|
|
}
|
|
};
|
|
// Text replies don't work (yet)
|
|
// grid[/*LANG*/"OK"] = {icon: "Ok", col: "#0f0", cb: () => respondToMessage("\u{1F44D}")}; // "Thumbs up" emoji
|
|
// grid[/*LANG*/"Nak"] = {icon: "Nak", col: "#f00", cb: () => respondToMessage("\u{1F44E}")}; // "Thumbs down" emoji
|
|
// grid[/*LANG*/"No Phone"] = {icon: "NoPhone", col: "#f0f", cb: () => respondToMessage("\u{1F4F5}")}; // "No Mobile Phones" emoji
|
|
|
|
grid[/*LANG*/"Dismiss"] = {icon: "Trash", col: "#ff0", cb: () => respondToMessage(false)};
|
|
showGrid(grid);
|
|
};
|
|
/**
|
|
* Show message
|
|
*
|
|
* @param {number} [num=0] Message to show
|
|
* @param {boolean} [bottom=false] Scroll message to bottom right away
|
|
*/
|
|
let buzzing = false, moving = false, switching = false;
|
|
let h, fh, offset;
|
|
|
|
/**
|
|
* draw (sticky) footer
|
|
*/
|
|
const drawFooter = function() {
|
|
// left hint: swipe from left for main menu
|
|
g.reset().clearRect(Bangle.appRect.x, Bangle.appRect.y2-fh, Bangle.appRect.x2, Bangle.appRect.y2)
|
|
.setFont(fontTiny)
|
|
.setFontAlign(-1, 1) // bottom left
|
|
.drawString(
|
|
"\0"+atob("CAiBACBA/EIiAnwA")+ // back
|
|
"\0"+atob("CAiBAEgkEgkSJEgA"), // >>
|
|
Bangle.appRect.x+(B2 ? 1 : 28), Bangle.appRect.y2
|
|
);
|
|
// center message count+hints: swipe up/down for next/prev message
|
|
const footer = ` ${messageNum+1}/${MESSAGES.length} `,
|
|
fw = g.stringWidth(footer);
|
|
g.setFontAlign(0, 1); // bottom center
|
|
if (B2 && messageNum>0 && offset<=0)
|
|
g.drawString("\0"+atob("CAiBAABBIhRJIhQI"), Bangle.appRect.x+Bangle.appRect.w/2-fw/2, Bangle.appRect.y2); // ^ swipe to prev
|
|
g.drawString(footer, Bangle.appRect.x+Bangle.appRect.w/2, Bangle.appRect.y2);
|
|
if (B2 && messageNum<MESSAGES.length-1 && offset>=h-(Bangle.appRect.h-fh))
|
|
g.drawString("\0"+atob("CAiBABAoRJIoRIIA"), Bangle.appRect.x+Bangle.appRect.w/2+fw/2, Bangle.appRect.y2); // v swipe to next
|
|
// right hint: swipe from right for message actions
|
|
g.setFontAlign(1, 1) // bottom right
|
|
.drawString(
|
|
"\0"+atob("CAiBABIkSJBIJBIA")+ // <<
|
|
"\0"+atob("CAiBAP8AAP8AAP8A"), // = ("hamburger menu")
|
|
Bangle.appRect.x2-(B2 ? 1 : 28), Bangle.appRect.y2
|
|
);
|
|
};
|
|
const showMessage = function(num, bottom) {
|
|
if (num<0) num = 0;
|
|
if (!num) num = 0; // no number: show first
|
|
if (num>=MESSAGES.length) num = MESSAGES.length-1;
|
|
setActive("messages", num);
|
|
if (!MESSAGES.length) {
|
|
// I /think/ this should never happen...
|
|
return E.showPrompt(/*LANG*/"No Messages", {
|
|
title:/*LANG*/"Messages",
|
|
img: require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")),
|
|
buttons: {/*LANG*/"Ok": 1}
|
|
}).then(showMain);
|
|
}
|
|
Bangle.setLocked(false);
|
|
Bangle.setLCDPower(1);
|
|
// only clear msg.new on user input
|
|
const msg = MESSAGES[messageNum]; // message
|
|
fh = 10; // footer height
|
|
offset = 0;
|
|
let oldOffset = 0;
|
|
const move = (dy) => {
|
|
offset = Math.max(0, Math.min(h-(Bangle.appRect.h-fh), offset+dy)); // clip at message height
|
|
dy = oldOffset-offset; // real dy
|
|
// move all elements to new offset
|
|
const offsetRecurser = function(l) {
|
|
if (l.y) l.y += dy;
|
|
if (l.c) l.c.forEach(offsetRecurser);
|
|
};
|
|
offsetRecurser(layout.l);
|
|
oldOffset = offset;
|
|
draw();
|
|
};
|
|
const draw = () => {
|
|
g.reset()
|
|
.clearRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2-fh)
|
|
.setClipRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2-fh);
|
|
g.reset = () => g.setColor(g.theme.fg).setBgColor(g.theme.bg); // stop Layout resetting ClipRect
|
|
layout.render();
|
|
if (layout.button && h>Bangle.appRect.h-fh && offset<h-(Bangle.appRect.h-fh)) {
|
|
// not yet at bottom: draw button "disabled", i.e. in fg+bg instead of fg2/fgH+bg2/bgH
|
|
g.theme = Object.assign({}, g.theme); // allow overriding
|
|
g.theme.fg2 = g.theme.fgH = g.theme.fg;
|
|
g.theme.bg2 = g.theme.bgH = g.theme.bg;
|
|
layout.render(layout.button);
|
|
delete g.theme; // restore original
|
|
}
|
|
delete g.reset;
|
|
// draw scrollbar (simply on top of everything)
|
|
if (h>(Bangle.appRect.h-fh)) {
|
|
const sbh = (Bangle.appRect.h-fh)/h*(Bangle.appRect.h-fh), // scrollbar height
|
|
y1 = Bangle.appRect.y+offset/h*(Bangle.appRect.h-fh), y2 = y1+sbh;
|
|
g.setColor(g.theme.bg).drawLine(Bangle.appRect.x2, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2-fh);
|
|
g.setColor(g.theme.fg).drawLine(Bangle.appRect.x2, y1, Bangle.appRect.x2, y2);
|
|
}
|
|
drawFooter();
|
|
};
|
|
const buzzOnce = () => {
|
|
if (buzzing) return;
|
|
buzzing = true;
|
|
Bangle.buzz(50).then(() => setTimeout(() => {buzzing = false;}, 500));
|
|
};
|
|
|
|
layout = getMessageLayout(msg);
|
|
h = layout.l.h; // message height
|
|
if (bottom) move(h); // scrolling backwards: jump to bottom of message
|
|
else draw();
|
|
const PAGE_SIZE = Bangle.appRect.h-fh;
|
|
const // shared B1/B2 handlers
|
|
back = () => {
|
|
delete msg.new; // we mark messages as read on any input
|
|
goBack();
|
|
},
|
|
swipe = dir => {
|
|
delete msg.new;
|
|
if (dir===RIGHT) showMain();
|
|
else if (dir===LEFT) showMessageActions();
|
|
},
|
|
touch = (side, xy) => {
|
|
delete msg.new;
|
|
if (h<=Bangle.appRect.h-fh || offset>=h-(Bangle.appRect.h-fh)) { // already at bottom
|
|
// B2: check for button-press
|
|
// setUI overrides Layout listeners, so we need to check for button presses ourselves
|
|
if (B2 && layout.button) {
|
|
const b = layout.button;
|
|
// the button is at the bottom of the screen, so we accept touches all the way down
|
|
if (xy.x>=b.x && xy.y>=b.y && xy.x<=b.x+b.w /*&& xy.y<=b.y+b.h*/) return b.cb();
|
|
}
|
|
if (B2 && xy.y<Bangle.appRect.y) return;
|
|
}
|
|
|
|
// first: pagedown until we reach the bottom
|
|
if (h>Bangle.appRect.h-fh && offset<h-(Bangle.appRect.h-fh)) return move(+PAGE_SIZE);
|
|
|
|
// B1: when scrolled all the way down, treat whole right-side as button
|
|
if (!B2 && layout.button && side===2) return layout.button.cb();
|
|
|
|
// touch anywhere else:
|
|
switch(settings().onTap) {
|
|
case 1: // Dismiss
|
|
return respondToMessage(false);
|
|
case 2: // Back
|
|
return goBack();
|
|
case 3: // Do nothing
|
|
return;
|
|
case 0: // Message Menu
|
|
default:
|
|
return showMessageActions();
|
|
}
|
|
};
|
|
|
|
if (B2) { // Bangle.js 2
|
|
setUI({
|
|
mode: "custom",
|
|
back: back,
|
|
swipe: swipe,
|
|
drag: e => {
|
|
delete msg.new;
|
|
if (!switching) {
|
|
const dy = -e.dy;
|
|
if (dy>0) { // up
|
|
if (h>Bangle.appRect.h-fh && offset<h-(Bangle.appRect.h-fh)) {
|
|
moving = true; // prevent scrolling right into next message
|
|
move(dy);
|
|
} else if (messageNum<MESSAGES.length-1) { // already at bottom: show next
|
|
if (!moving) { // don't scroll right through to next message
|
|
Bangle.buzz(30);
|
|
switching = true; // don't process any more drag events until we lift our finger
|
|
showMessage(messageNum+1);
|
|
}
|
|
} else { // already at bottom of last message
|
|
buzzOnce();
|
|
}
|
|
} else if (dy<0) { // down
|
|
if (offset>0) {
|
|
moving = true; // prevent scrolling right into prev message
|
|
move(dy);
|
|
} else if (messageNum>0) { // already at top: show prev
|
|
if (!moving) { // don't scroll right through to previous message
|
|
Bangle.buzz(30);
|
|
switching = true; // don't process any more drag events until we lift our finger
|
|
showMessage(messageNum-1, true);
|
|
}
|
|
} else { // already at top of first message
|
|
buzzOnce();
|
|
}
|
|
}
|
|
}
|
|
if (!e.b) {
|
|
// touch end: we can swipe to another message (if we reached the top/bottom) or move the new message
|
|
moving = false;
|
|
switching = false;
|
|
}
|
|
},
|
|
touch: touch,
|
|
});
|
|
} else { // Bangle.js 1
|
|
setUI({
|
|
mode: "updown",
|
|
back: back,
|
|
}, dir => {
|
|
delete msg.new;
|
|
if (dir===DOWN) {
|
|
if (h>Bangle.appRect.h-fh && offset<h-(Bangle.appRect.h-fh)) {
|
|
move(+PAGE_SIZE);
|
|
} else if (messageNum<MESSAGES.length-1) { // bottom reached: show next
|
|
Bangle.buzz(30);
|
|
showMessage(messageNum+1);
|
|
} else {
|
|
buzzOnce(); // already at bottom of last message
|
|
}
|
|
} else if (dir===UP) {
|
|
if (offset>0) {
|
|
move(-PAGE_SIZE);
|
|
} else if (messageNum>0) { // top reached: show previous
|
|
Bangle.buzz(30);
|
|
showMessage(messageNum-1, true);
|
|
} else {
|
|
buzzOnce(); // already at top of first message
|
|
}
|
|
} else { // button
|
|
showMessageActions();
|
|
}
|
|
});
|
|
Bangle.swipeHandler = swipe;
|
|
Bangle.on("swipe", Bangle.swipeHandler);
|
|
Bangle.touchHandler = touch;
|
|
Bangle.on("touch", Bangle.touchHandler);
|
|
} // Bangle.js 1/2
|
|
};
|
|
/**
|
|
* Determine message layout information: size, fonts, and wrapped title/body texts
|
|
*
|
|
* @param msg
|
|
* @returns {{h: number, w: number,
|
|
* src: (string),
|
|
* title: (string), titleFont: (string),
|
|
* body: (string), bodyFont: (string)}}
|
|
*/
|
|
const getMessageLayoutInfo = function(msg) {
|
|
// header: [icon][title]
|
|
// [ src]
|
|
//
|
|
// But: no title? -> use src as title
|
|
let w, src = msg.src || "",
|
|
title = msg.title || "",
|
|
body = msg.body || "",
|
|
h = 0, // total height
|
|
th = 0, // title height
|
|
ih = 46; // icon height: // icon(24) + internal padding(20) + icon<->src spacer(2)
|
|
if (!title) {
|
|
title = src;
|
|
src = "";
|
|
}
|
|
|
|
// top bar
|
|
if (title) {
|
|
w = Bangle.appRect.w-59; // icon(24) + padding:left(5) + padding:btn-txt(5) + internal btn padding(20) + padding:right(5)
|
|
title = g.setFont(fontNormal).wrapString(title, w).join("\n");
|
|
th += 2+g.stringMetrics(title).height; // 2px padding
|
|
}
|
|
if (src) {
|
|
w = 59; // icon(24) + padding:left(5) + padding:btn-txt(5) + internal btn padding(20) + padding:right(5)
|
|
src = g.setFont(fontTiny).wrapString(src, w).join("\n");
|
|
ih += g.stringMetrics(src).height;
|
|
}
|
|
|
|
h = Math.max(ih, th); // maximum of icon/title
|
|
|
|
// body
|
|
w = Bangle.appRect.w-4; // padding(2x2)
|
|
body = g.setFont(fontNormal).wrapString(msg.body, w).join("\n");
|
|
h += 4+g.stringMetrics(body).height; // padding(2x2)
|
|
|
|
if (settings().button) h += 44; // icon(24) + padding(2x2) + internal btn padding(16)
|
|
|
|
w = Bangle.appRect.w;
|
|
// always expand to <full height>-<(10x)footer>
|
|
h = Math.max(h, Bangle.appRect.h-10);
|
|
|
|
return {
|
|
src: src,
|
|
title: title,
|
|
body: body,
|
|
h: h,
|
|
w: w,
|
|
};
|
|
};
|
|
|
|
const getMessageLayout = function(msg) {
|
|
// Crafted so that on B2, with "medium" font, a message with
|
|
// icon + src + 2-line title + 2-line body + button
|
|
// fits exactly, i.e. no need for scrolling
|
|
const info = getMessageLayoutInfo(msg);
|
|
const hCol = msg.new ? g.theme.fgH : g.theme.fg2,
|
|
hBg = msg.new ? g.theme.bgH : g.theme.bg2;
|
|
|
|
// lie to Layout library about available space
|
|
Bangle.appRect = Object.assign({}, Bangle.appRect,
|
|
{w: info.w, h: info.h, x2: Bangle.appRect.x+info.w-1, y2: Bangle.appRect.y+info.h-1});
|
|
|
|
// make sure icon is not invisible
|
|
let imageCol = getImageColor(msg);
|
|
if (g.setColor(imageCol).getColor()==hBg) imageCol = hCol;
|
|
|
|
layout = new Layout({
|
|
type: "v", c: [
|
|
{
|
|
type: "h", fillx: 1, bgCol: hBg, col: hCol, c: [
|
|
{width: 3},
|
|
{
|
|
type: "v", c: [
|
|
{type: "img", /*pad: 2,*/ src: () => getMessageImage(msg), col: imageCol},
|
|
{height: 2},
|
|
info.src ? {type: "txt", font: fontTiny, label: info.src, bgCol: hBg, col: hCol} : {},
|
|
]
|
|
},
|
|
info.title ? {type: "txt", font: fontNormal, label: info.title, bgCol: hBg, col: hCol, fillx: 1, pad: 2} : {},
|
|
{width: 3},
|
|
]
|
|
},
|
|
{type: "txt", font: fontNormal, label: info.body, fillx: 1, filly: 1, pad: 2},
|
|
{filly: 1},
|
|
settings().button ? {
|
|
type: "h", c: [
|
|
B2 ? {} : {fillx: 1}, // Bangle.js 1: touching right side = press button
|
|
{id: "button", type: "btn", pad: 2, src: () => getIcon("trash"), cb: () => respondToMessage(false)},
|
|
]
|
|
} : {},
|
|
]
|
|
});
|
|
layout.update();
|
|
delete Bangle.appRect;
|
|
return layout;
|
|
};
|
|
|
|
/** this is a timeout if the app has started and is showing a single message
|
|
but the user hasn't seen it (e.g. no user input) - in which case
|
|
we should start a timeout for settings().unreadTimeout to return
|
|
to the clock. */
|
|
let unreadTimeout;
|
|
/**
|
|
* Stop auto-unload timeout and buzzing, remove listeners for this function
|
|
*/
|
|
const clearUnreadStuff = function() {
|
|
require("messages").stopBuzz();
|
|
if (unreadTimeout) clearTimeout(unreadTimeout);
|
|
unreadTimeout = undefined;
|
|
["touch", "drag", "swipe"].forEach(l => Bangle.removeListener(l, clearUnreadStuff));
|
|
watches.forEach(w => clearWatch(w));
|
|
watches = [];
|
|
};
|
|
|
|
let messageNum, // currently visible message
|
|
watches = [], // button watches
|
|
savedMusic = false; // did we find a stored "music" message when loading?
|
|
// special messages
|
|
let call, music, map, alarm;
|
|
/**
|
|
* Find special messages, and remove them from MESSAGES
|
|
*/
|
|
const findSpecials = function() {
|
|
let idx = MESSAGES.findIndex(m => m.id==="call");
|
|
if (idx>=0) call = MESSAGES.splice(idx, 1)[0];
|
|
idx = MESSAGES.findIndex(m => m.id==="music");
|
|
if (idx>=0) {
|
|
music = MESSAGES.splice(idx, 1)[0];
|
|
savedMusic = true;
|
|
}
|
|
idx = MESSAGES.findIndex(m => m.id==="map");
|
|
if (idx>=0) map = MESSAGES.splice(idx, 1)[0];
|
|
idx = MESSAGES.findIndex(m => m.src && m.src.toLowerCase().startsWith("alarm"));
|
|
if (idx>=0) alarm = MESSAGES.splice(idx, 1)[0];
|
|
};
|
|
if (MESSAGES!==undefined) { // only if loading MESSAGES worked
|
|
g.reset().clear();
|
|
Bangle.loadWidgets();
|
|
require("messages").toggleWidget(false);
|
|
Bangle.drawWidgets();
|
|
findSpecials(); // sets global vars for special messages
|
|
// any message we asked to show?
|
|
const showIdx = MESSAGES.findIndex(m => m.show);
|
|
// any new text messages?
|
|
const newIdx = MESSAGES.findIndex(m => m.new);
|
|
|
|
// figure out why the app was loaded
|
|
if (showIdx>=0) show(showIdx);
|
|
else if (call && call.new) showCall();
|
|
else if (alarm && alarm.new) showAlarm();
|
|
else if (map && map.new) showMap();
|
|
else if (music && music.new && settings().openMusic) {
|
|
if (settings().alwaysShowMusic===undefined) {
|
|
// if not explicitly disabled, enable this the first time we see music
|
|
let s = settings();
|
|
s.alwaysShowMusic = true;
|
|
require("Storage").writeJSON("messages.settings.json", s);
|
|
}
|
|
showMusic();
|
|
}
|
|
// check for new message last: Maybe we already showed it, but timed out before
|
|
// if that happened, and we're loading for e.g. music now, we want to show the music screen
|
|
else if (newIdx>=0) {
|
|
showMessage(newIdx);
|
|
// auto-loaded for message(s): auto-close after timeout
|
|
let unreadTimeoutSecs = settings().unreadTimeout;
|
|
if (unreadTimeoutSecs===undefined) unreadTimeoutSecs = 60;
|
|
if (unreadTimeoutSecs) {
|
|
unreadTimeout = setTimeout(load, unreadTimeoutSecs*1000);
|
|
}
|
|
} else if (MESSAGES.length) { // not autoloaded, but we have messages to show
|
|
back = "main"; // prevent "back" from loading clock
|
|
showMessage();
|
|
} else showMain();
|
|
|
|
// stop buzzing, auto-close timeout on input
|
|
["touch", "drag", "swipe"].forEach(l => Bangle.on(l, clearUnreadStuff));
|
|
(B2 ? [BTN1] : [BTN1, BTN2, BTN3]).forEach(b => watches.push(setWatch(clearUnreadStuff, b, false)));
|
|
}
|
|
} |