Merge pull request #2385 from rigrig/messagelist

messagelist: new app
pull/2406/head
Gordon Williams 2022-12-15 09:35:14 +00:00 committed by GitHub
commit 8a4f5930a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 1713 additions and 0 deletions

View File

@ -0,0 +1 @@
0.01: New app!

View File

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

17
apps/messagelist/TODO.txt Normal file
View File

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

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEw4UA///rkcAYP9ohL/ABMBqoAEoALDioLFqgLDBQoABERIkEBZcFBY9QBed61QAC1oLF7wLD24LF24LD7wLF1vqBQOrvQLFA4IuC9QLFD4IuC1QLGGAQOBBYwgBEwQLHvQBBEZHVq4jI7wWBHY5TLNZaDLTZazLffMBBY9ABZsABY4KCgEVBQtUBYYkGEQYA/AAwA="))

1208
apps/messagelist/app.js Normal file

File diff suppressed because it is too large Load Diff

BIN
apps/messagelist/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

3
apps/messagelist/boot.js Normal file
View File

@ -0,0 +1,3 @@
(function() {
Bangle.on("message", require("messagegui").messageListener);
})();

246
apps/messagelist/lib.js Normal file
View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,139 @@
(function(back) {
let settings = require("messagegui").settings();
const inApp = (global.__FILE__ && __FILE__.startsWith("messagelist."));
function updateSetting(setting, value) {
settings[setting] = value;
let file;
switch(setting) {
case "flash":
case "showRead":
case "iconColorMode":
case "maxMessages":
case "maxUnreadTimeout":
case "openMusic":
case "repeat":
case "unlockWatch":
case "unreadTimeout":
case "vibrate":
case "vibrateCalls":
case "vibrateTimeout":
// Default app has this setting: update that file
file = "messages";
break;
default:
// write to our own settings file
file = "messagelist";
}
file += ".settings.json";
let saved = require("Storage").readJSON(file, true) || {};
saved[setting] = value;
require("Storage").writeJSON(file, saved);
}
function toggler(setting) {
return {
value: !!settings[setting],
onchange: v => 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();
});

1
bin/sanitycheck.js Normal file → Executable file
View File

@ -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) {