Merge pull request #530 from rigrig/notify_api

Notify api
pull/532/head
Gordon Williams 2020-07-20 08:38:01 +01:00 committed by GitHub
commit 0af0778fb4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 240 additions and 106 deletions

View File

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

View File

@ -16,3 +16,4 @@
0.15: Don't keep LCD on while playing music
0.16: Handle dismissing notifications on the phone
Nicer display of alarm clock notifications
0.17: Modified music notification for updated 'notify' library

View File

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

View File

@ -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 => {
render:a => {
if (a.h>200) {
// large:
// [icon]
// [ ]
// <artist>
// ------------- middle of screen
// <title>
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);
g.drawImage(require("heatshrink").decompress(atob("jEYwILI/EAv/8gP/ARcMgOAASN8h+A/kfwP8n4CD/E/gHgjg/HA=")), 8, y + 8);
// 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);
var x = 40;
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));
}

View File

@ -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`

View File

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

View File

@ -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;
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;
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);
}
if (options.src) {
g.setColor(-1).setFontAlign(1, -1, 0).setFont("6x8", 1);
g.drawString(options.src.substring(0, 10), 215, 322-h);
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);

View File

@ -1,3 +1,4 @@
0.01: New Library!
0.02: Add notification ID option
0.03: Fix custom render callback
0.04: Pass `area{x,y,w,h}` to render callback instead of just `y`

View File

@ -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.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);
g.drawString(options.src.substring(0, 10), 235, y-32);
// 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);