From bc3ab8ef76f7d244fd5b36c80f0025574af19635 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Sat, 26 Nov 2022 15:28:09 +0100 Subject: [PATCH] messagelist: new app --- apps/messagelist/ChangeLog | 1 + apps/messagelist/README.md | 69 ++ apps/messagelist/TODO.txt | 17 + apps/messagelist/app-icon.js | 1 + apps/messagelist/app.js | 1207 ++++++++++++++++++++++++++++++ apps/messagelist/app.png | Bin 0 -> 8988 bytes apps/messagelist/boot.js | 3 + apps/messagelist/lib.js | 246 ++++++ apps/messagelist/metadata.json | 28 + apps/messagelist/screenshot0.png | Bin 0 -> 19719 bytes apps/messagelist/screenshot1.png | Bin 0 -> 20953 bytes apps/messagelist/screenshot2.png | Bin 0 -> 20743 bytes apps/messagelist/screenshot3.png | Bin 0 -> 16791 bytes apps/messagelist/settings.js | 139 ++++ bin/sanitycheck.js | 1 + 15 files changed, 1712 insertions(+) create mode 100644 apps/messagelist/ChangeLog create mode 100644 apps/messagelist/README.md create mode 100644 apps/messagelist/TODO.txt create mode 100644 apps/messagelist/app-icon.js create mode 100644 apps/messagelist/app.js create mode 100644 apps/messagelist/app.png create mode 100644 apps/messagelist/boot.js create mode 100644 apps/messagelist/lib.js create mode 100644 apps/messagelist/metadata.json create mode 100644 apps/messagelist/screenshot0.png create mode 100644 apps/messagelist/screenshot1.png create mode 100644 apps/messagelist/screenshot2.png create mode 100644 apps/messagelist/screenshot3.png create mode 100644 apps/messagelist/settings.js mode change 100644 => 100755 bin/sanitycheck.js diff --git a/apps/messagelist/ChangeLog b/apps/messagelist/ChangeLog new file mode 100644 index 000000000..759f68777 --- /dev/null +++ b/apps/messagelist/ChangeLog @@ -0,0 +1 @@ +0.01: New app! \ No newline at end of file diff --git a/apps/messagelist/README.md b/apps/messagelist/README.md new file mode 100644 index 000000000..776d0d0e6 --- /dev/null +++ b/apps/messagelist/README.md @@ -0,0 +1,69 @@ +# Message List + +Display messages inline as a single list: +Displays one message at a time, if it doesn't fit on the screen you can scroll +up/down. When you reach the bottom, you can scroll on to the next message. + +## Installation +**First** uninstall the default [Message UI](/?id=messagegui) app (`messagegui`, +not the library!). +Then install this app. + +## Screenshots + +### Main menu: +![Screenshot](screenshot0.png) + +### Unread message: +![Screenshot](screenshot1.png) +The chevrons are hints for swipe actions: +- Swipe right to go back +- Swipe left for the message-actions menu +- Swipe down to show the previous message: We are currently viewing message 2 of 2, + so message 1 is "above" this one. + +### Long (read) message: +![Screenshot](screenshot2.png) +The button is disabled until you scroll all the way to the bottom. + +### Music: +![Screenshot](screenshot3.png) +Minimal setup: album name and buttons disabled through settings. +Swipe for next/previous song, tap to pause/resume. + +## Settings + +### Interface +* `Font size` - The font size used when displaying messages/music. +* `On Tap` - If messages are too large to fit on the screen, tapping the screen scrolls down. + This is the action to take when tapping a message after reaching the bottom: + - `Message menu`: Open menu with message actions + - `Dismiss`: Dismiss message right away + - `Back`: Go back to clock/main menu + - `Nothing`: Do nothing +* `Dismiss button` - Show inline button to dismiss message right away + +### Behaviour +* `Vibrate` - The pattern of buzzes when a new message is received. +* `Vibrate for calls` - The pattern of buzzes for incoming calls. +* `Vibrate for alarms` - The pattern of buzzes for (phone) alarms. +* `Repeat` - How often buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds. +* `Unread timer` - When a new message is received the Messages app is opened. + If there is no user input for this amount of time then the app will exit and return to the clock. +* `Auto-open` - Automatically open app when a new message arrives. +* `Respect quiet mode` - Prevent auto-opening during quiet mode. + +### Music +* `Auto-open` - Automatically open app when music starts playing. +* `Always visible` - Show "music" in the main menu even when nothing is playing. +* `Buttons` - Show `previous`/`play/pause`/`next` buttons on music screen. +* `Show album` - Display album names? + + +### Util +* `Delete all` - Erase all messages. + + +## Attributions + +Some icons used in this app are from https://icons8.com diff --git a/apps/messagelist/TODO.txt b/apps/messagelist/TODO.txt new file mode 100644 index 000000000..3a6d7b664 --- /dev/null +++ b/apps/messagelist/TODO.txt @@ -0,0 +1,17 @@ +## Nice to have: +* Add labels to B1 music HW buttons +* Add volume buttons to B2 music screen (when controls are enabled) +* Draw messages ourselves instead of piling hacks on Layout +* Make sure all icons are 24x24px: icon sizes affect layout +* Check/optimize layout for B1, other fonts (scrolling for just 5px is a shame) + +## Wishlist: +* Option to swipe-dismiss (instead of action menu) +* Maybe refactor showGrid() out into a general-use module? + +* Message replies (needs `android` support) +* Customize replies +* Custom replies (i.e. `textinput`) +* Hooks to add custom replies/actions, + e.g. external code could add "Send intent" option to Home Assistant messages + Maybe just use this for all replies, so we don't hardcode anything in "messages"? diff --git a/apps/messagelist/app-icon.js b/apps/messagelist/app-icon.js new file mode 100644 index 000000000..6ed3c1141 --- /dev/null +++ b/apps/messagelist/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///rkcAYP9ohL/ABMBqoAEoALDioLFqgLDBQoABERIkEBZcFBY9QBed61QAC1oLF7wLD24LF24LD7wLF1vqBQOrvQLFA4IuC9QLFD4IuC1QLGGAQOBBYwgBEwQLHvQBBEZHVq4jI7wWBHY5TLNZaDLTZazLffMBBY9ABZsABY4KCgEVBQtUBYYkGEQYA/AAwA=")) diff --git a/apps/messagelist/app.js b/apps/messagelist/app.js new file mode 100644 index 000000000..c10ac726d --- /dev/null +++ b/apps/messagelist/app.js @@ -0,0 +1,1207 @@ +/* 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; + }; + 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")) Bangle.showClock(); + }, 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")) Bangle.showClock(); + }, 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: "", artist: "", 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.y2*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 Bangle.showClock(); // 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 { + 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 (valmax) 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 (rcbs[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 { + 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 { + 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=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(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.yBangle.appRect.h-fh && offset { + delete msg.new; + if (!switching) { + const dy = -e.dy; + if (dy>0) { // up + if (h>Bangle.appRect.h-fh && offset0) { + 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 && offset0) { + 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 -<(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))); + } +} \ No newline at end of file diff --git a/apps/messagelist/app.png b/apps/messagelist/app.png new file mode 100644 index 0000000000000000000000000000000000000000..6eae4bb9691a39f7239c11fbde094a34558d53e0 GIT binary patch literal 8988 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 G3;rJ{)jY%i literal 0 HcmV?d00001 diff --git a/apps/messagelist/boot.js b/apps/messagelist/boot.js new file mode 100644 index 000000000..994a2cfed --- /dev/null +++ b/apps/messagelist/boot.js @@ -0,0 +1,3 @@ +(function() { + Bangle.on("message", require("messagegui").messageListener); +})(); \ No newline at end of file diff --git a/apps/messagelist/lib.js b/apps/messagelist/lib.js new file mode 100644 index 000000000..33b6d9d69 --- /dev/null +++ b/apps/messagelist/lib.js @@ -0,0 +1,246 @@ +// Handle incoming messages while the app is not loaded +// The messages app overrides Bangle.messageListener +// (placed in separate file, so we don't read this all at boot time) +exports.messageListener = function(type, msg) { + if (msg.handled || (global.__FILE__ && __FILE__.startsWith("messagelist."))) return; // already handled/app open + // clean up, in case previous message didn't load the app after all + if (exports.loadTimeout) clearTimeout(exports.loadTimeout); + delete exports.loadTimeout; + delete exports.buzz; + const quiet = () => (require("Storage").readJSON("setting.json", 1) || {}).quiet; + /** + * Quietly load the app for music/map, if not already loading + */ + function loadQuietly(msg) { + if (exports.loadTimeout) return; // already loading + exports.loadTimeout = setTimeout(function() { + Bangle.load("messagelist.app.js"); + }, 500); + } + function loadNormal(msg) { + if (exports.loadTimeout) clearTimeout(exports.loadTimeout); // restart timeout + exports.loadTimeout = setTimeout(function() { + delete exports.loadTimeout; + // check there are still new messages (for #1362) + let messages = require("messages").getMessages(msg); + (Bangle.MESSAGES || []).forEach(m => require("messages").apply(m, messages)); + if (!messages.some(m => m.new)) return; // don't use `status()`: also load for new music! + // if we're in a clock, or it's important, open app + if (Bangle.CLOCK || msg.important) { + if (exports.buzz) require("messages").buzz(msg.src); + Bangle.load("messagelist.app.js"); + } + }, 500); + } + + /** + * Mark message as handled, and save it for the app + */ + const handled = () => { + if (!Bangle.MESSAGES) Bangle.MESSAGES = []; + require("messages").apply(msg, Bangle.MESSAGES); + if (!Bangle.MESSAGES.length) delete Bangle.MESSAGES; + if (msg.t==="remove") require("messages").save(msg); + else msg.handled = true; + }; + /** + * Write messages to flash after all, when not laoding the app + */ + const saveToFlash = () => { + (Bangle.MESSAGES||[]).forEach(m=>require("messages").save(m)); + delete Bangle.MESSAGES; + } + + switch(type) { + case "music": + if (!Bangle.CLOCK) return; + // only load app if we are playing, and we know which song + if (msg.state!=="play" || !msg.title) return; + if (exports.openMusic===undefined) { + // only read settings for first music message + exports.openMusic = !!(exports.settings().openMusic); + } + if (!exports.openMusic) return; // we don't care about music + if (quiet()) return; + msg.new = true; + handled(); + return loadQuietly(); + + case "map": + handled(); + if (msg.t!=="remove" && Bangle.CLOCK) loadQuietly(); + else saveToFlash(); + return; + + case "text": + handled(); + if (exports.settings().autoOpen===false) return saveToFlash(); + if (quiet()) return saveToFlash(); + if (msg.t!=="add" || !msg.new || !(Bangle.CLOCK || msg.important)) { + // not important enough to load the app + if (msg.t==="add" && msg.new) require("messages").buzz(msg); + return saveToFlash(); + } + if (msg.t==="add" && msg.new) exports.buzz = true; + return loadNormal(msg); + + case "alarm": + if (quiet()<2) return saveToFlash(); + // fall through + case "call": + handled(); + exports.buzz = true; + return loadNormal(msg); + + // case "clearAll": do nothing + } +}; + +exports.settings = function() { + return Object.assign({ + // Interface // + fontSize: 1, + onTap: 0, // [Message menu, Dismiss, Back, Nothing] + button: true, + + // Behaviour // + vibrate: ":", + vibrateCalls: ":", + vibrateAlarms: ":", + repeat: 4, + vibrateTimeout: 60, + unreadTimeout: 60, + autoOpen: true, + + // Music // + openMusic: true, + // no default: alwaysShowMusic (auto-enabled by app when music happens) + showAlbum: true, + musicButtons: false, + + // Widget // + flash: true, + // showRead: false, + + // Utils // + }, + // fall back to default app settings if not set for messagelist + (require("Storage").readJSON("messages.settings.json", true) || {}), + (require("Storage").readJSON("messagelist.settings.json", true) || {})); +}; + +/** + * @param {string} icon Icon name + * @returns string Icon image string, for use with g.drawImage() + */ +exports.getIcon = function(icon) { + // TODO: icons should be 24x24px with 1bpp colors + switch(icon.toLowerCase()) { + // generic icons: + case "alert": + return atob("GBgBAAAAAP8AA//AD8PwHwD4HBg4ODwcODwccDwOcDwOYDwGYDwGYBgGYBgGcBgOcAAOOBgcODwcHDw4Hxj4D8PwA//AAP8AAAAA"); + case "alarm": + case "alarmclockreceiver": + return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA="); + case "back": // TODO: 22x22 + return atob("FhYBAAAAEAAAwAAHAAA//wH//wf//g///BwB+DAB4EAHwAAPAAA8AADwAAPAAB4AAHgAB+AH/wA/+AD/wAH8AA=="); + case "calendar": + return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA=="); + case "mail": // TODO: 28x18 + case "sms message": + case "notification": + return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A=="); + case "map": // TODO: 25x25, + return atob("GRmBAAAAAAAAAAAAAAIAYAHx/wH//+D/+fhz75w/P/4f//8P//uH///D///h3f/w4P+4eO/8PHZ+HJ/nDu//g///wH+HwAYAIAAAAAAAAAAAAAA="); + case "menu": + return atob("GBiBAAAAAAAAAAAAAAAAAP///////wAAAAAAAAAAAAAAAAAAAP///////wAAAAAAAAAAAAAAAAAAAP///////wAAAAAAAAAAAAAAAA=="); + case "music": // TODO: 22x22 + return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A="); + case "nak": // TODO: 22x25 + return atob("FhmBAA//wH//j//+P//8///7///v//+///7//////////////v//////////z//+D8AAPwAAfgAB+AAD4AAPgAAeAAB4AAHAAA=="); + case "neg": // TODO: 22x22 + return atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA="); + case "next": + return atob("GBiBAAAAAAAAAAAAAAwAcB8A+B+A+B/g+B/4+B/8+B//+B//+B//+B//+B//+B//+B/8+B/4+B/g+B+A+B8A+AwAcAAAAAAAAAAAAA=="); + case "nophone": // TODO: 30x30 + return atob("Hh6BAAAAAAGAAAAHAAAADgAAABwADwA4Af8AcA/8AOB/+AHH/+ADv/8AB//wAA/HAAAeAAACOAAADHAAAHjgAAPhwAAfg4AAfgcAAfwOAA/wHAA/wDgA/gBwA/gA4AfAAcAfAAOAGAAHAAAADgAAABgAAAAA"); + case "ok": // TODO: 22x25 + return atob("FhmBAAHAAAeAAB4AAPgAA+AAH4AAfgAD8AAPwAD//+//////////////7//////////////v//+///7///v//8///gf/+A//wA=="); + case "pause": + return atob("GBiBAAAAAAAAAAAAAAOBwAfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AOBwAAAAAAAAAAAAA=="); + case "phone": // TODO: 23x23 + case "call": + return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA="); + case "play": + return atob("GBiBAAAAAAAAAAAAAAcAAA+AAA/gAA/4AA/8AA//AA//wA//4A//8A//8A//4A//wA//AA/8AA/4AA/gAA+AAAcAAAAAAAAAAAAAAA=="); + case "pos": // TODO: 25x20 + return atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA=="); + case "previous": + return atob("GBiBAAAAAAAAAAAAAA4AMB8A+B8B+B8H+B8f+B8/+B//+B//+B//+B//+B//+B//+B8/+B8f+B8H+B8B+B8A+A4AMAAAAAAAAAAAAA=="); + case "settings": // TODO: 20x20 + return atob("FBSBAAAAAA8AAPABzzgf/4H/+A//APnwfw/n4H5+B+fw/g+fAP/wH/+B//gc84APAADwAAAA"); + case "to do": + return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA"); + case "trash": + return atob("GBiBAAAAAAAAAAB+AA//8A//8AYAYAYAYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAYAYAYAYAf/4AP/wAAAAAAAAA=="); + case "unknown": // TODO: 30x30 + return atob("Hh6BAAAAAAAAAAAAAAAAAAPwAAA/8AAB/+AAD//AAD4fAAHwPgAHwPgAAAPgAAAfAAAA/AAAD+AAAH8AAAHwAAAPgAAAPgAAAPgAAAAAAAAAAAAAAAAAAHAAAAPgAAAPgAAAPgAAAHAAAAAAAAAAAAAAAAAA"); + case "unread": // TODO: 29x24 + return atob("HRiBAAAAH4AAAf4AAB/4AAHz4AAfn4AA/Pz/5+fj/z8/j/n5/j/P//j/Pn3j+PPPx+P8fx+Pw/x+AF/B4A78RiP3xwOPvHw+Pcf/+Ox//+NH//+If//+B///+A=="); + default: //should never happen + return exports.getIcon("unknown"); + } +}; +/** + * @param {string} icon Icon + * @returns {string} Color to use with g.setColor() + */ +exports.getColor = function(icon) { + switch(icon.toLowerCase()) { + // generic colors, using B2-safe colors + case "alert": + return "#ff0"; + case "alarm": + return "#fff"; + case "calendar": + return "#f00"; + case "mail": + return "#ff0"; + case "map": + return "#f0f"; + case "music": + return "#f0f"; + case "neg": + return "#f00"; + case "notification": + return "#0ff"; + case "phone": + case "call": + return "#0f0"; + case "settings": + return "#000"; + case "sms message": + return "#0ff"; + case "trash": + return "#f00"; + case "unknown": + return g.theme.fg; + case "unread": + return "#ff0"; + default: + return g.theme.fg; + } +}; + +/** + * Launch GUI app with given message + * @param {object} msg + */ +exports.open = function(msg) { + if (msg && msg.id && !msg.show) { + // store which message to load + msg.show = 1; + } + + Bangle.load((msg && msg.new && msg.id!=="music") ? "messagelist.new.js" : "messagelist.app.js"); +}; diff --git a/apps/messagelist/metadata.json b/apps/messagelist/metadata.json new file mode 100644 index 000000000..7947e2db4 --- /dev/null +++ b/apps/messagelist/metadata.json @@ -0,0 +1,28 @@ +{ + "id": "messagelist", + "name": "Message List", + "version": "0.01", + "description": "Display notifications from iOS and Gadgetbridge/Android as a list", + "icon": "app.png", + "type": "app", + "tags": "tool,system", + "screenshots": [ + {"url": "screenshot0.png"}, + {"url": "screenshot1.png"}, + {"url": "screenshot2.png"}, + {"url": "screenshot3.png"} + ], + "supports": ["BANGLEJS","BANGLEJS2"], + "dependencies" : { "messageicons":"module" }, + "provides_modules": ["messagegui"], + "readme": "README.md", + "storage": [ + {"name":"messagelist.boot.js","url":"boot.js"}, + {"name":"messagegui","url":"lib.js"}, + {"name":"messagelist.app.js","url":"app.js"}, + {"name":"messagelist.settings.js","url":"settings.js"}, + {"name":"messagelist.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"messagelist.settings.json"}], + "sortorder": -9 +} diff --git a/apps/messagelist/screenshot0.png b/apps/messagelist/screenshot0.png new file mode 100644 index 0000000000000000000000000000000000000000..b6f37c05339a18e22f99416b654884a8730e623e GIT binary patch literal 19719 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 literal 0 HcmV?d00001 diff --git a/apps/messagelist/screenshot1.png b/apps/messagelist/screenshot1.png new file mode 100644 index 0000000000000000000000000000000000000000..f4d4db9fa1e08faa61ae66dd8eb4ed9708d0dada GIT binary patch literal 20953 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 oTU`-?hP3%$;(j`?Rm^sEqj;q6)Fvoz!`Iz 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 updateSetting(setting, v) + }; + } + + function showIfMenu() { + const tapOptions = [/*LANG*/"Message menu",/*LANG*/"Dismiss",/*LANG*/"Back",/*LANG*/"Nothing"]; + E.showMenu({ + "": {"title": /*LANG*/"Interface"}, + "< Back": () => showMainMenu(), + /*LANG*/"Font size": { + value: 0|settings.fontSize, + min: 0, max: 2, + format: v => [/*LANG*/"Small",/*LANG*/"Medium",/*LANG*/"Large",/*LANG*/"Huge"][v], + onchange: v => updateSetting("fontSize", v) + }, + /*LANG*/"On Tap": { + value: settings.onTap, + min: 0, max: tapOptions.length-1, wrap: true, + format: v => tapOptions[v], + onchange: v => updateSetting("onTap", v) + }, + /*LANG*/"Dismiss button": toggler("button"), + }); + } + + function showBMenu() { + E.showMenu({ + "": {"title": /*LANG*/"Behaviour"}, + "< Back": () => showMainMenu(), + /*LANG*/"Vibrate": require("buzz_menu").pattern(settings.vibrate, v => updateSetting("vibrate", v)), + /*LANG*/"Vibrate for calls": require("buzz_menu").pattern(settings.vibrateCalls, v => updateSetting("vibrateCalls", v)), + /*LANG*/"Vibrate for alarms": require("buzz_menu").pattern(settings.vibrateAlarms, v => updateSetting("vibrateAlarms", v)), + /*LANG*/"Repeat": { + value: settings.repeat, + min: 0, max: 10, + format: v => v ? v+"s" :/*LANG*/"Off", + onchange: v => updateSetting("repeat", v) + }, + /*LANG*/"Vibrate timer": { + value: settings.vibrateTimeout, + min: 0, max: 240, step: 10, + format: v => v ? v+"s" :/*LANG*/"Forever", + onchange: v => updateSetting("vibrateTimeout", v) + }, + /*LANG*/"Unread timer": { + value: settings.unreadTimeout, + min: 0, max: 240, step: 10, + format: v => v ? v+"s" :/*LANG*/"Off", + onchange: v => updateSetting("unreadTimeout", v) + }, + /*LANG*/"Auto-open": toggler("autoOpen"), + }); + } + + function showMusicMenu() { + E.showMenu({ + "": {"title": /*LANG*/"Music"}, + "< Back": () => showMainMenu(), + /*LANG*/"Auto-open": toggler("openMusic"), + /*LANG*/"Always visible": toggler("alwaysShowMusic"), + /*LANG*/"Buttons": toggler("musicButtons"), + /*LANG*/"Show album": toggler("showAlbum"), + }); + } + + function showWidMenu() { + E.showMenu({ + "": {"title": /*LANG*/"Widget"}, + "< Back": () => showMainMenu(), + /*LANG*/"Flash icon": toggler("flash"), + // /*LANG*/"Show Read": toggler("showRead"), + }); + } + + function showUtilsMenu() { + let m = E.showMenu({ + "": {"title": /*LANG*/"Utilities"}, + "< Back": () => showMainMenu(), + /*LANG*/"Delete all": () => { + E.showPrompt(/*LANG*/"Are you sure?", + {title:/*LANG*/"Delete All Messages"}) + .then(isYes => { + if (isYes) require("messages").write([]); + showUtilsMenu(); + }); + } + }); + } + + function showMainMenu() { + E.showMenu({ + "": {"title": inApp ?/*LANG*/"Settings" :/*LANG*/"Messages"}, + "< Back": back, + /*LANG*/"Interface": () => showIfMenu(), + /*LANG*/"Behaviour": () => showBMenu(), + /*LANG*/"Music": () => showMusicMenu(), + /*LANG*/"Widget": () => showWidMenu(), + /*LANG*/"Utils": () => showUtilsMenu(), + }); + } + + showMainMenu(); +}); diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js old mode 100644 new mode 100755 index 838f99895..82b2896b8 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -94,6 +94,7 @@ const INTERNAL_FILES_IN_APP_TYPE = { // list of app types and files they SHOULD var KNOWN_WARNINGS = [ "App gpsrec data file wildcard .gpsrc? does not include app ID", "App owmweather data file weather.json is also listed as data file for app weather", + "App messagegui storage file messagegui is also listed as storage file for app messagelist", ]; function globToRegex(pattern) {