From 8df352cca9c93975f254b4501e7355792ab013c7 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Sun, 19 Jul 2020 21:54:25 +0200 Subject: [PATCH 1/4] Add some sample GB() messages, for testing purposes --- apps/gbridge/sample_messages.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 apps/gbridge/sample_messages.js diff --git a/apps/gbridge/sample_messages.js b/apps/gbridge/sample_messages.js new file mode 100644 index 000000000..be33a25b8 --- /dev/null +++ b/apps/gbridge/sample_messages.js @@ -0,0 +1,21 @@ +/** + * Some sample messages to test Gadgetbridge notifications + */ +// send both of these to trigger music notification +GB({"t":"musicinfo","artist":"Some Artist Name","album":"The Album Name","track":"The Track Title Goes Here","dur":241,"c":2,"n":2}) +GB({"t":"musicstate","state":"play","position":0,"shuffle":1,"repeat":1}) + +// WhatsApp group +GB({"t":"notify","id":1592721712,"src":"WhatsApp","title":"Sample Group: Sam","body":"This is a test WhatsApp message"}) +GB({"t":"notify","id":1592721714,"src":"WhatsApp","title":"Sample Group (2 messages): Robin","body":"This is another test WhatsApp message"}) +GB({"t":"notify","id":1592721719,"src":"WhatsApp","title":"Sample Group (3 messages): Kim","body":"This is yet another test WhatsApp message, but it is really really really really really really long. Almost as if somebody wanted to test how much characters you could stuff into a notification before all of the body text just wouldn't fit anymore."}) + +// Alarm clock + dismissal +GB({"t":"notify","id":1592721714,"src":"ALARMCLOCKRECEIVER"}) +GB({"t":"notify-","id":1592721714}) + +// Weather update (doesn't show a notification, not handled by gbridge app: see weather app) +GB({"t":"weather","temp":288,"hum":94,"txt":"Light rain","wind":0,"loc":"Test City"}) + +// Nextcloud updated a file +GB({"t":"notify","id":1594184421,"src":"Nextcloud","title":"Downloaded","body":"test.file downloaded"}) From 7bb98d38e09c4e581df2d5993eb9de3d83dc8620 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Sun, 19 Jul 2020 21:57:49 +0200 Subject: [PATCH 2/4] notify: Update notify API for #527 Also some cleanup: - fall back to "src" if title is missing - move message-splitting into separate function --- apps.json | 2 +- apps/notify/ChangeLog | 1 + apps/notify/README.md | 2 +- apps/notify/notify.js | 144 +++++++++++++++++++++++++++--------------- 4 files changed, 97 insertions(+), 52 deletions(-) diff --git a/apps.json b/apps.json index cb66eef39..092aa1642 100644 --- a/apps.json +++ b/apps.json @@ -80,7 +80,7 @@ "name": "Notifications (default)", "shortName":"Notifications", "icon": "notify.png", - "version":"0.02", + "version":"0.03", "description": "A handler for displaying notifications that displays them in a bar at the top of the screen", "tags": "widget", "type": "notify", diff --git a/apps/notify/ChangeLog b/apps/notify/ChangeLog index 0dccc7930..cb974e12d 100644 --- a/apps/notify/ChangeLog +++ b/apps/notify/ChangeLog @@ -1,2 +1,3 @@ 0.01: New Library! 0.02: Add notification ID option +0.03: Pass `area{x,y,w,h}` to render callback instead of just `y` \ No newline at end of file diff --git a/apps/notify/README.md b/apps/notify/README.md index a17caccea..cef9f2124 100644 --- a/apps/notify/README.md +++ b/apps/notify/README.md @@ -16,7 +16,7 @@ options = { src : string, // optional source name body : string, // optional body text icon : string, // optional icon (image string) - render function(y) {} // function callback to render + render function(area) {} // function callback to render in area{x,y,w,h} }; // eg... show notification require("notify").show({title:"Test", body:"Hello"}); diff --git a/apps/notify/notify.js b/apps/notify/notify.js index 51569d912..d8168e048 100644 --- a/apps/notify/notify.js +++ b/apps/notify/notify.js @@ -1,5 +1,36 @@ -var pos = 0; -var id = null; +let pos = 0; +let id = null; + +/** + * Fit text into area, trying to insert newlines between words + * Appends "..." if more text was present but didn't fit + * + * @param {string} text + * @param {number} rows Maximum number of rows + * @param {number} width Maximum line length, in characters + */ +function fitWords(text,rows,width) { + // We never need more than rows*width characters anyway, split by any whitespace + const words = text.trim().substr(0,rows*width).split(/\s+/); + let row=1,len=0,limit=width; + let result = ""; + for (let word of words) { + // len==0 means first word of row, after that we also add a space + if ((len?len+1:0)+word.length > limit) { + if (row>=rows) { + result += "..."; + break; + } + result += "\n"; + len=0; + row++; + if (row===rows) limit -= 3; // last row needs space for "..." + } + result += (len?" ":"") + word; + len += (len?1:0) + word.length; + } + return result; +} /** options = { @@ -13,77 +44,90 @@ var id = null; render function(y) // function callback to render } */ +/* + The screen is 240x240px, but has a 240x320 buffer, used like this: + 0,0: top-left ... 239,0: top-right + [Normal screen contents: lines 0-239] + 239,0: bottom-left ... 239,239: bottom-right + [Usually off-screen: lines 240-319] + 319,0: last line in buffer ... 319,239: last pixel in buffer + + When moving the display area, the buffer wraps around + + So we draw notifications at the end of the buffer, + then shift the display down to show them without touching regular content. + Apps don't know about this, so can just keep updating the usual display area. + + For example, a size 40 notification: + - Draws in bottom 40 buffer lines (279-319) + - Shifts display down by 40px + Display now shows buffer lines 279-319,0-199 + Apps/widgets keep drawing to buffer line 0-239 like nothing happened + */ exports.show = function(options) { - options = options||{}; - if (options.on===undefined) options.on=true; + options = options || {}; + if (options.on===undefined) options.on = true; id = ("id" in options)?options.id:null; - var h = options.size||80; - var oldMode = Bangle.getLCDMode(); + let size = options.size || 80; + if (size>80) {size = 80} + const oldMode = Bangle.getLCDMode(); // TODO: throw exception if double-buffered? + // TODO: throw exception if size>80? Bangle.setLCDMode("direct"); - var y = 320-h; - var x = 4; - g.setClipRect(0, y, 239, 319); + // drawing area + let x = 0, + y = 320-size, + w = 240, + h = size, + b = y+h-1, r = x+w-1; // bottom,right + g.setClipRect(x,y, r,b); // clear area - g.setColor(0).fillRect(0, y+1, 239, 317); - // border - g.setColor(0x39C7).fillRect(0, 318, 239, 319); - // top bar - var top = 0; - if (options.title) { - g.setColor(0x39C7).fillRect(0, y, 239, y+20); + g.setColor(0).fillRect(x,y, r,b); + // bottom border + g.setColor(0x39C7).fillRect(0,b-1, r,b); + b -= 2;h -= 2; + // title bar + if (options.title || options.src) { + g.setColor(0x39C7).fillRect(x,y, r,y+20); + const title = options.title||options.src; g.setColor(-1).setFontAlign(-1, -1, 0).setFont("6x8", 2); - g.drawString(options.title.trim().substring(0, 13), 25, y+3); - y+=20; - } - if (options.src) { - g.setColor(-1).setFontAlign(1, -1, 0).setFont("6x8", 1); - g.drawString(options.src.substring(0, 10), 215, 322-h); + g.drawString(title.trim().substring(0, 13), x+25,y+3); + if (options.title && options.src) { + g.setFont("6x8", 1); + g.drawString(options.src.substring(0, 10), x+215,y+5); + } + y += 20;h -= 20; } if (options.icon) { - let i = options.icon; + let i = options.icon, iw; g.drawImage(i, x,y+4); - if ("string"==typeof i) x += i.charCodeAt(0); - else x += i[0]; + if ("string"==typeof i) {iw = i.charCodeAt(0)} + else {iw = i[0]} + x += iw;w -= iw; } // body text if (options.body) { - var body = options.body; - const maxChars = Math.floor((300-x)/8); - var limit = maxChars; - let row = 1; - let words = body.trim().replace("\n", " ").split(" "); - body = ""; - for (var i = 0; i < words.length; i++) { - if (body.length + words[i].length + 1 > limit) { - if (row>=5) { - body += "..."; - break; - } - body += "\n " + words[i]; - row++; - limit += maxChars; - if (row==5) limit -= 4; - } else { - body += " " + words[i]; - } - } - g.setColor(-1).setFont("6x8", 1).setFontAlign(-1, -1, 0).drawString(body, x-4, y+4); + const maxRows=Math.floor((h-4)/8), // font=6x8 + maxChars=Math.floor(w/6)-2, + text=fitWords(options.body, maxRows, maxChars); + g.setColor(-1).setFont("6x8", 1).setFontAlign(-1, -1, 0).drawString(text, x+6,y+4); } - if (options.render) options.render(320 - h); + if (options.render) { + options.render({x:x, y:y, w:w, h:h}); + } if (options.on) Bangle.setLCDPower(1); // light up Bangle.setLCDMode(oldMode); // clears cliprect function anim() { pos -= 2; - if (pos < -h) { - pos = -h; + if (pos < -size) { + pos = -size; } Bangle.setLCDOffset(pos); - if (pos > -h) setTimeout(anim, 15); + if (pos > -size) setTimeout(anim, 15); } anim(); Bangle.on("touch", exports.hide); From 48bf67dfb155cd4af348fb10a7984bffd739af55 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Sun, 19 Jul 2020 21:59:28 +0200 Subject: [PATCH 3/4] notifyfs: Update notify API for #527 Also some cleanup: - fall back to "src" if title is missing - move message-splitting into separate function - make icon position depend on whether titlebar is present --- apps.json | 2 +- apps/notifyfs/ChangeLog | 3 +- apps/notifyfs/notify.js | 103 ++++++++++++++++++++++++---------------- 3 files changed, 64 insertions(+), 44 deletions(-) diff --git a/apps.json b/apps.json index 092aa1642..e3cda8672 100644 --- a/apps.json +++ b/apps.json @@ -93,7 +93,7 @@ "name": "Fullscreen Notifications", "shortName":"Notifications", "icon": "notify.png", - "version":"0.03", + "version":"0.04", "description": "A handler for displaying notifications that displays them fullscreen. This may not fully restore the screen after on some apps. See `Notifications (default)` for more information about the notifications library.", "tags": "widget", "type": "notify", diff --git a/apps/notifyfs/ChangeLog b/apps/notifyfs/ChangeLog index 3a1d646bd..bf5c73161 100644 --- a/apps/notifyfs/ChangeLog +++ b/apps/notifyfs/ChangeLog @@ -1,3 +1,4 @@ 0.01: New Library! 0.02: Add notification ID option -0.03: Fix custom render callback \ No newline at end of file +0.03: Fix custom render callback +0.04: Pass `area{x,y,w,h}` to render callback instead of just `y` \ No newline at end of file diff --git a/apps/notifyfs/notify.js b/apps/notifyfs/notify.js index 2bfc8de2d..7d317fa43 100644 --- a/apps/notifyfs/notify.js +++ b/apps/notifyfs/notify.js @@ -1,6 +1,32 @@ -var pos = 0; -var oldg; -var id = null; +let oldg; +let id = null; + +/** + * See notify/notify.js + */ +function fitWords(text,rows,width) { + // We never need more than rows*width characters anyway, split by any whitespace + const words = text.trim().substr(0,rows*width).split(/\s+/); + let row=1,len=0,limit=width; + let result = ""; + for (let word of words) { + // len==0 means first word of row, after that we also add a space + if ((len?len+1:0)+word.length > limit) { + if (row>=rows) { + result += "..."; + break; + } + result += "\n"; + len=0; + row++; + if (row===rows) limit -= 3; // last row needs space for "..." + } + result += (len?" ":"") + word; + len += (len?1:0) + word.length; + } + return result; +} + /** options = { @@ -19,56 +45,49 @@ exports.show = function(options) { options = options||{}; if (options.on===undefined) options.on=true; id = ("id" in options)?options.id:null; - var h = options.size||120; + let size = options.size||120; + if (size>120) {size=120} Bangle.setLCDMode("direct"); - var y = 40; - var x = 4; - // clear area + let x = 0, + y = 0, + w = 240, + h = 240; + // clear screen g.clear(1); // top bar - var top = 0; - if (options.title) { - g.setColor(0x39C7).fillRect(0, y, 239, y+30); + if (options.title||options.src) { + y=40;h=size; + const title = options.title || options.src + g.setColor(0x39C7).fillRect(x, y, x+w-1, y+30); g.setColor(-1).setFontAlign(-1, -1, 0).setFont("6x8", 3); - g.drawString(options.title.trim().substring(0, 13), 5, y+3); - y+=30; - } - if (options.src) { - g.setColor(-1).setFontAlign(1, 1, 0).setFont("6x8", 2); - g.drawString(options.src.substring(0, 10), 235, y-32); + g.drawString(title.trim().substring(0, 13), x+5, y+3); + if (options.title && options.src) { + g.setColor(-1).setFontAlign(1, 1, 0).setFont("6x8", 2); + // above drawing area, but we are fullscreen + g.drawString(options.src.substring(0, 10), x+235, y-32); + } + y += 30;h -= 30; } if (options.icon) { - let i = options.icon; - g.drawImage(i, x,y+4); - if ("string"==typeof i) x += i.charCodeAt(0); - else x += i[0]; + let i = options.icon, iw,ih; + if ("string"==typeof i) {iw=i.charCodeAt(0); ih=i.charCodeAt(1)} + else {iw=i[0]; ih=i[1]} + const iy=y ? (y+4) : (h-ih)/2; // show below title bar if present, otherwise center vertically + g.drawImage(i, x+4,iy); + x += iw+4;w -= iw+4; } // body text if (options.body) { - var body = options.body; - const maxChars = Math.floor((300-x)/16); - var limit = maxChars; - let row = 1; - let words = body.trim().replace("\n", " ").split(" "); - body = ""; - for (var i = 0; i < words.length; i++) { - if (body.length + words[i].length + 1 > limit) { - if (row>=8) { - body += "..."; - break; - } - body += "\n " + words[i]; - row++; - limit += maxChars; - if (row==8) limit -= 4; - } else { - body += " " + words[i]; - } - } - g.setColor(-1).setFont("6x8", 2).setFontAlign(-1, -1, 0).drawString(body, x-4, y+4); + const maxRows = Math.floor((h-4)/16), // font=2*(6x8) + maxChars = Math.floor((w-4)/12), + text=fitWords(options.body, maxRows, maxChars); + g.setColor(-1).setFont("6x8", 2).setFontAlign(-1, -1, 0).drawString(text, x+4, y+4); } - if (options.render) options.render(120-h/2); + if (options.render) { + const area={x:x, y:y, w:w, h:h} + options.render(area); + } if (options.on) Bangle.setLCDPower(1); // light up Bangle.on("touch", exports.hide); From 6406788165b4ae7c8a5985febb1cc4a6c3a15178 Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Sun, 19 Jul 2020 21:59:59 +0200 Subject: [PATCH 4/4] gbridge: Modified music notification for updated 'notify' library --- apps.json | 2 +- apps/gbridge/ChangeLog | 3 +- apps/gbridge/widget.js | 63 ++++++++++++++++++++++++++++++++++++------ 3 files changed, 58 insertions(+), 10 deletions(-) diff --git a/apps.json b/apps.json index e3cda8672..412b4be10 100644 --- a/apps.json +++ b/apps.json @@ -121,7 +121,7 @@ { "id": "gbridge", "name": "Gadgetbridge", "icon": "app.png", - "version":"0.16", + "version":"0.17", "description": "The default notification handler for Gadgetbridge notifications from Android", "tags": "tool,system,android,widget", "type":"widget", diff --git a/apps/gbridge/ChangeLog b/apps/gbridge/ChangeLog index c614ee179..9a3090f5d 100644 --- a/apps/gbridge/ChangeLog +++ b/apps/gbridge/ChangeLog @@ -15,4 +15,5 @@ 0.14: Added 'find' event handling 0.15: Don't keep LCD on while playing music 0.16: Handle dismissing notifications on the phone - Nicer display of alarm clock notifications \ No newline at end of file + Nicer display of alarm clock notifications +0.17: Modified music notification for updated 'notify' library diff --git a/apps/gbridge/widget.js b/apps/gbridge/widget.js index f9e38a407..b4c7e1f7b 100644 --- a/apps/gbridge/widget.js +++ b/apps/gbridge/widget.js @@ -29,7 +29,7 @@ case "ALARMCLOCKRECEIVER": return { id: event.id, - title: event.title || "Alarm", + title: event.title || "Alarm: "+require('locale').time(new Date(), true), body: event.body, // same icon as apps/alarm/app-icon.js icon: require("heatshrink").decompress(atob("mEwwkGswAhiMRCCAREAo4eHBIQLEAgwYHsIJDiwHB5gACBpIhHCoYZEGA4gFCw4ABGA4HEjgXJ4IXGAwcUB4VEmf//8zogICoJIFAodMBoNDCoIADmgJB4gXIFwXDCwoABngwFC4guB4k/CQXwh4EC+YMCC44iBp4qDC4n/+gNBC41sEIJCEC4v/GAPGC4dhXYRdFC4xhCCYIXCdQRdDC5HzegQXCsxGHC45IDCwQXCUgwXHJAIXGRogXJSIIXcOw4XIPAYXcBwv/mEDBAwXOgtQC65QGC5vzoEAJAx3Nmk/mEABIiPN+dDAQIwFC4zXGFwKRCGAjvMFwQECGAgXI4YuGGAUvAgU8C4/EFwwGCAgdMC4p4EFwobFOwoXDJAIoEAApGBC4xIEABJGHGAapEAAqNBFwwXD4heI+YuBC5BIBVQhdHIw4wD5inFS4IKCCxFmigNCokzCoMzogICoIWIsMRjgPCAA3BiMWC48RBQIXJEgMRFxAJCCw4lEC44IECooOIBAaBJKwhgIAH4ACA==")), @@ -51,13 +51,60 @@ if (state.music === "play") { require("notify").show(Object.assign({ size:40, id:"music", - render:y => { - g.setColor(-1); - g.drawImage(require("heatshrink").decompress(atob("jEYwILI/EAv/8gP/ARcMgOAASN8h+A/kfwP8n4CD/E/gHgjg/HA=")), 8, y + 8); - g.setFontAlign(-1, -1); - var x = 40; - g.setFont("4x6", 2).drawString(state.musicInfo.artist, x, y + 8); - g.setFont("6x8", 1).drawString(state.musicInfo.track, x, y + 22); + render:a => { + if (a.h>200) { + // large: + // [icon] + // [ ] + // + // ------------- middle of screen + // + const iconSize = 24*3; // 24x24px, scale 3 + let x = a.x, + y = a.y+a.h/2, // we use coords relative to vertical middle + w=a.w,h=a.h; + // try to fit musicInfo property into width + const fitFont = (prop,max) => { + if (!(prop in state.musicInfo)) return max; + let size = Math.floor(w/(state.musicInfo[prop].length*6)); + if (size<1) {size=1} + if (size>max) {size=max} + return size; + } + let aSize = fitFont('artist',3); + // TODO: split long title over multiple lines instead + let tSize = fitFont('track',2); + let bSize = fitFont('album',2); + g.setColor(-1); + // TODO: use a nicer large icon? + g.drawImage( + require("heatshrink").decompress(atob("jEYwILI/EAv/8gP/ARcMgOAASN8h+A/kfwP8n4CD/E/gHgjg/HA=")), + x+(w-iconSize)/2, y-(iconSize+aSize*8)-12, {scale: 3}); + // artist: centered above middle + g.setFontAlign(0, 1).setFont("6x8", aSize).drawString(state.musicInfo.artist, x+w/2, y-4); + // title: left-aligned below middle + g.setFontAlign(-1, -1).setFont("6x8", tSize).drawString(state.musicInfo.track, x, y+4); + // album: centered at bottom + if (state.musicInfo.album) { + // note: using a.y rather than y + g.setFontAlign(0, 1).setFont("6x8", bSize).drawString(state.musicInfo.album, x+w/2, a.y+h); + } + } else { + // regular size: + // [icon] <artist> + // [ ] <title> + const size=40, iconSize = 24; + let x = a.x, + y = a.y+(a.h-size)/2; // center vertically in available area + g.setColor(-1); + g.drawImage( + require("heatshrink").decompress(atob("jEYwILI/EAv/8gP/ARcMgOAASN8h+A/kfwP8n4CD/E/gHgjg/HA=")), + x+8, y+8); + g.setFontAlign(-1, -1); + x += iconSize+16; + g.setFont("4x6", 2).drawString(state.musicInfo.artist, x, y+8); + g.setFont("6x8", 1).drawString(state.musicInfo.track, x, y+22); + } }}, options)); }