1
0
Fork 0

Merge branch 'espruino:master' into sleeplogalarm

master
storm64 2022-09-26 22:01:23 +02:00 committed by GitHub
commit 7f1641b649
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 1182 additions and 930 deletions

View File

@ -10,3 +10,4 @@
0.10: Added separate Bangle.js 2 file with Bangle.js 2 kickstarter pixels (as of 28 Oct 2021)
0.11: Bangle.js2: New pixels, btn1 to exit
0.12: Actual pixels as of 29th Nov 2021
0.13: Bangle.js 2: Use setUI to add software back button

View File

@ -69,4 +69,7 @@ function drawImage() {
// TODO: a nice little animation before
setTimeout(drawInfo, 1000);
setWatch(_=>load(), BTN1);
Bangle.setUI({
mode : "custom",
back : load
});

View File

@ -1,7 +1,7 @@
{
"id": "about",
"name": "About",
"version": "0.12",
"version": "0.13",
"description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers",
"icon": "app.png",
"tags": "tool,system",

View File

@ -6,4 +6,5 @@
0.06: Add a temperature threshold to detect (and not alert) if the BJS isn't worn. Better support for the peoples using the app at night
0.07: Fix bug on the cutting edge firmware
0.08: Use default Bangle formatter for booleans
0.09: New app screen (instead of showing settings or the alert) and some optimisations
0.09: New app screen (instead of showing settings or the alert) and some optimisations
0.10: Add software back button via setUI

View File

@ -47,8 +47,12 @@
Bangle.loadWidgets();
Bangle.drawWidgets();
drawInfo();
Bangle.setUI({
mode : "custom",
back : load
})
}
run();
})();
})();

View File

@ -3,7 +3,7 @@
"name": "Activity Reminder",
"shortName":"Activity Reminder",
"description": "A reminder to take short walks for the ones with a sedentary lifestyle",
"version":"0.09",
"version":"0.10",
"icon": "app.png",
"type": "app",
"tags": "tool,activity",

View File

@ -3,3 +3,4 @@
0.03: Disable past events display from settings
0.04: Added awareness of allDay field
0.05: Displaying calendar colour and name
0.06: Added clkinfo for clocks.

View File

@ -0,0 +1,29 @@
(function() {
var agendaItems = {
name: "Agenda",
img: atob("GBiBAf////////85z/AAAPAAAPgAAP////AAAPAAAPAAAPAAAOAAAeAAAeAAAcAAA8AAAoAABgAADP//+P//8PAAAPAAAPgAAf///w=="),
items: []
};
var now = new Date();
var agenda = storage.readJSON("android.calendar.json")
.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000)
.sort((a,b)=>a.timestamp - b.timestamp);
agenda.forEach((entry, i) => {
var title = entry.title.slice(0,18);
var date = new Date(entry.timestamp*1000);
var dateStr = locale.date(date).replace(/\d\d\d\d/,"");
dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : "";
agendaItems.items.push({
name: "agendaEntry-" + i,
get: () => ({ text: title + "\n" + dateStr, img: null}),
show: function() { agendaItems.items[i].emit("redraw"); },
hide: function () {}
});
});
return agendaItems;
})

View File

@ -1,7 +1,7 @@
{
"id": "agenda",
"name": "Agenda",
"version": "0.05",
"version": "0.06",
"description": "Simple agenda",
"icon": "agenda.png",
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],
@ -12,6 +12,7 @@
"storage": [
{"name":"agenda.app.js","url":"agenda.js"},
{"name":"agenda.settings.js","url":"settings.js"},
{"name":"agenda.clkinfo.js","url":"agenda.clkinfo.js"},
{"name":"agenda.img","url":"agenda-icon.js","evaluate":true}
],
"data": [{"name":"agenda.settings.json"}]

View File

@ -1,9 +1,9 @@
{ "id": "agpsdata",
"name": "A-GPS Data",
"name": "A-GPS Data Downloader App",
"shortName":"A-GPS Data",
"icon": "agpsdata.png",
"version":"0.02",
"description": "Download assisted GPS (A-GPS) data directly to your Bangle.js **using Gadgetbridge**",
"description": "Once installed, this app allows you to download assisted GPS (A-GPS) data directly to your Bangle.js **via Gadgetbridge on an Android phone** when you run the app. If you just want to upload the latest AGPS data from this app loader, please use the `Assisted GPS Update (AGPS)` app.",
"tags": "boot,tool,assisted,gps,agps,http",
"allow_emulator":true,
"supports": ["BANGLEJS2"],

View File

@ -10,4 +10,5 @@
week is buffered until date or timezone changes
0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users)
0.08: fixed calendar weeknumber not shortened to two digits
0.09: Use default Bangle formatter for booleans
0.09: Use default Bangle formatter for booleans
0.10: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on 2v16

File diff suppressed because one or more lines are too long

View File

@ -1,7 +1,7 @@
{
"id": "antonclk",
"name": "Anton Clock",
"version": "0.09",
"version": "0.10",
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
"readme":"README.md",
"icon": "app.png",

View File

@ -1,11 +1,12 @@
{
"id": "assistedgps",
"name": "Assisted GPS Update (AGPS)",
"name": "Assisted GPS Updater (AGPS)",
"version": "0.03",
"description": "Downloads assisted GPS (AGPS) data to Bangle.js 1 or 2 for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
"description": "Downloads assisted GPS (AGPS) data to Bangle.js for faster GPS startup and more accurate fixes. **No app will be installed**, this just uploads new data to the GPS chip.",
"sortorder": -1,
"icon": "app.png",
"type": "RAM",
"tags": "tool,outdoors,agps",
"tags": "tool,outdoors,agps,gps,a-gps",
"supports": ["BANGLEJS","BANGLEJS2"],
"custom": "custom.html",
"customConnect": true,

View File

@ -2,3 +2,4 @@
0.02: Add sit ups
Add more feedback to the user about the exercises
Clean up code
0.03: Add software back button on main menu

View File

@ -71,7 +71,8 @@ function showMainMenu() {
let menu;
menu = {
"": {
title: "BanglExercise"
title: "BanglExercise",
back: load
}
};
@ -381,4 +382,5 @@ Bangle.on('HRM', function(hrm) {
});
g.clear(1);
Bangle.loadWidgets();
showMainMenu();

View File

@ -1,7 +1,7 @@
{ "id": "banglexercise",
"name": "BanglExercise",
"shortName":"BanglExercise",
"version":"0.02",
"version":"0.03",
"description": "Can automatically track exercises while wearing the Bangle.js watch.",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],

View File

@ -4,3 +4,4 @@
0.04: bug fix
0.05: proper fix for the race condition in queueDraw()
0.06: Tell clock widgets to hide.
0.07: Better battery graphic - now has green, yellow and red sections; battery status reflected in the bar across the middle of the screen; current battery state checked only once every 15 minutes, leading to longer-lasting battery charge

View File

@ -11,6 +11,8 @@ Graphics.prototype.setFontOpenSans = function(scale) {
};
var drawTimeout;
var lastBattCheck = 0;
var width = 0;
function queueDraw(millis_now) {
if (drawTimeout) clearTimeout(drawTimeout);
@ -24,12 +26,15 @@ function draw() {
var date = new Date();
var h = date.getHours(),
m = date.getMinutes();
var d = date.getDate(),
w = date.getDay(); // d=1..31; w=0..6
const level = E.getBattery();
const width = level + (level/2);
var d = date.getDate();
var is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
var dows = require("date_utils").dows(0,1);
var dow = require("date_utils").dows(0,1)[date.getDay()];
if ((date.getTime() >= lastBattCheck + 15*60000) || Bangle.isCharging()) {
lastBattcheck = date.getTime();
width = E.getBattery();
width += width/2;
}
g.reset();
g.clear();
@ -47,24 +52,35 @@ function draw() {
g.drawString(d, g.getWidth() -6, 98);
g.setFont('Vector', 52);
g.setFontAlign(-1, -1);
g.drawString(dows[w].slice(0,2).toUpperCase(), 6, 103);
g.drawString(dow.slice(0,2).toUpperCase(), 6, 103);
g.fillRect(9,159,166,171);
g.fillRect(167,163,170,167);
if (Bangle.isCharging()) {
g.setColor(1,1,0);
} else if (level > 40) {
g.setColor(0,1,0);
g.fillRect(12,162,12+width,168);
} else {
g.setColor(1,0,0);
g.fillRect(12,162,57,168);
g.setColor(1,1,0);
g.fillRect(58,162,72,168);
g.setColor(0,1,0);
g.fillRect(73,162,162,168);
}
g.fillRect(12,162,12+width,168);
if (level < 100) {
if (width < 150) {
g.setColor(g.theme.bg);
g.fillRect(12+width+1,162,162,168);
}
g.setColor(0, 1, 0);
if (Bangle.isCharging()) {
g.setColor(1,1,0);
} else if (width <= 45) {
g.setColor(1,0,0);
} else if (width <= 60) {
g.setColor(1,1,0);
} else {
g.setColor(0, 1, 0);
}
g.fillRect(0, 90, g.getWidth(), 94);
// widget redraw

View File

@ -1,7 +1,7 @@
{ "id": "bigdclock",
"name": "Big digit clock containing just the essentials",
"shortName":"Big digit clk",
"version":"0.06",
"version":"0.07",
"description": "A clock containing just the essentials, made as easy to read as possible for those of us that need glasses. It contains the time, the day-of-week, the day-of-month, and the current battery state-of-charge.",
"icon": "bigdclock.png",
"type": "clock",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -18,4 +18,6 @@
0.18: Set timer for an agenda entry by simply clicking in the middle of the screen. Only one timer can be set.
0.19: Fix - Compatibility with "Digital clock widget"
0.20: Better handling of async data such as getPressure.
0.21: On the default menu the week of year can be shown.
0.21: On the default menu the week of year can be shown.
0.22: Use the new clkinfo module for the menu.
0.23: Feedback of apps after run is now optional and decided by the corresponding clkinfo.

View File

@ -1,12 +1,15 @@
# BW Clock
A very minimalistic clock to mainly show date and time.
A very minimalistic clock.
![](screenshot.png)
## Features
The BW clock provides many features and also 3rd party integrations:
The BW clock implements features that are exposed by other apps through the `clkinfo` module.
For example, if you install the HomeAssistant app, this menu item will be shown if you click right
and additionally allows you to send triggers directly from the clock (select triggers via up/down and
send via click center). Here are examples of other apps that are integrated:
- Bangle data such as steps, heart rate, battery or charging state.
- A timer can be set directly. *Requirement: Scheduler library*
- Show agenda entries. A timer for an agenda entry can also be set by simply clicking in the middle of the screen. This can be used to not forget a meeting etc. Note that only one agenda-timer can be set at a time. *Requirement: Gadgetbridge calendar sync enabled*
- Weather temperature as well as the wind speed can be shown. *Requirement: Weather app*
- HomeAssistant triggers can be executed directly. *Requirement: HomeAssistant app*
@ -20,27 +23,27 @@ Note: If some apps are not installed (e.gt. weather app), then this menu item is
- Your bangle uses the sys color settings so you can change the color too.
## Menu structure
2D menu allows you to display lots of different data including data from 3rd party apps and it's also possible to control things e.g. to set a timer or send a HomeAssistant trigger.
2D menu allows you to display lots of different data including data from 3rd party apps and it's also possible to control things e.g. to trigger HomeAssistant.
Simply click left / right to go through the menu entries such as Bangle, Timer etc.
Simply click left / right to go through the menu entries such as Bangle, Weather etc.
and click up/down to move into this sub-menu. You can then click in the middle of the screen
to e.g. send a trigger via HomeAssistant once you selected it.
to e.g. send a trigger via HomeAssistant once you selected it. The actions really depend
on the app that provide this sub-menu through the `clkinfo` module.
```
+5min
|
Bangle -- Timer[Optional] -- Agenda 1[Optional] -- Weather[Optional] -- HomeAssistant [Optional]
| | | | |
Bpm -5min Agenda 2 Temperature Trigger1
| | | |
Steps ... ... ...
Bangle -- Agenda -- Weather -- HomeAssistant
| | | |
Battery Entry 1 Temperature Trigger1
| | | |
Steps ... ... ...
|
Battery
...
```
## Thanks to
<a href="https://www.flaticon.com/free-icons/" title="Icons">Icons created by Flaticon</a>
- Thanks to Gordon Williams not only for the great BangleJs, but specifically also for the implementation of `clkinfo` which simplified the BWClock a lot and moved complexety to the apps where it should be located.
- <a href="https://www.flaticon.com/free-icons/" title="Icons">Icons created by Flaticon</a>
## Creator
[David Peer](https://github.com/peerdavid)

View File

@ -1,19 +1,21 @@
/************
/************************************************
* Includes
*/
const locale = require('locale');
const storage = require('Storage');
const clock_info = require("clock_info");
/************
* Statics
/************************************************
* Globals
*/
const SETTINGS_FILE = "bwclk.setting.json";
const TIMER_IDX = "bwclk_timer";
const TIMER_AGENDA_IDX = "bwclk_agenda";
const W = g.getWidth();
const H = g.getHeight();
var lock_input = false;
/************
/************************************************
* Settings
*/
let settings = {
@ -29,8 +31,7 @@ for (const key in saved_settings) {
settings[key] = saved_settings[key]
}
/************
/************************************************
* Assets
*/
// Manrope font
@ -45,14 +46,12 @@ Graphics.prototype.setLargeFont = function(scale) {
return this;
};
Graphics.prototype.setMediumFont = function(scale) {
// Actual height 41 (42 - 2)
this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHAAAAAAAB/AAAAAAAP/AAAAAAD//AAAAAA///AAAAAP///AAAAB///8AAAAf///AAAAH///wAAAB///+AAAAH///gAAAAH//4AAAAAH/+AAAAAAH/wAAAAAAH8AAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///8AAAAH////AAAAP////wAAAf////4AAA/////8AAB/////+AAD/gAAH+AAD+AAAD/AAH8AAAB/AAH4AAAA/gAH4AAAAfgAH4AAAAfgAPwAAAAfgAPwAAAAfgAPwAAAAfgAHwAAAAfgAH4AAAAfgAH4AAAA/gAH8AAAA/AAD+AAAD/AAD/gAAH/AAB/////+AAB/////8AAA/////4AAAf////wAAAH////gAAAB///+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPgAAAAAAAfwAAAAAAA/gAAAAAAA/AAAAAAAB/AAAAAAAD+AAAAAAAD8AAAAAAAH8AAAAAAAH//////AAH//////AAH//////AAH//////AAH//////AAH//////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD4AAA/AAAP4AAB/AAAf4AAD/AAA/4AAD/AAB/4AAH/AAD/4AAP/AAH/AAAf/AAH8AAA//AAH4AAB//AAP4AAD//AAPwAAH+/AAPwAAP8/AAPwAAf4/AAPwAA/4/AAPwAA/w/AAPwAB/g/AAPwAD/A/AAP4AH+A/AAH8AP8A/AAH/A/4A/AAD///wA/AAD///gA/AAB///AA/AAA//+AA/AAAP/8AA/AAAD/wAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAH4AAAHwAAH4AAAH4AAH4AAAH8AAH4AAAP+AAH4AAAH+AAH4A4AB/AAH4A+AA/AAH4B/AA/gAH4D/AAfgAH4H+AAfgAH4P+AAfgAH4f+AAfgAH4/+AAfgAH5/+AAfgAH5//AAfgAH7+/AA/gAH/8/gB/AAH/4f4H/AAH/wf//+AAH/gP//8AAH/AH//8AAH+AD//wAAH8AB//gAAD4AAf+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAAAAAAD/AAAAAAAP/AAAAAAB//AAAAAAH//AAAAAAf//AAAAAB///AAAAAH///AAAAAf/8/AAAAB//w/AAAAH/+A/AAAA//4A/AAAD//gA/AAAH/+AA/AAAH/4AA/AAAH/gAA/AAAH+AAA/AAAHwAAA/AAAHAAf///AAEAAf///AAAAAf///AAAAAf///AAAAAf///AAAAAf///AAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAP/AHgAAH///AP4AAH///gP8AAH///gP8AAH///gP+AAH///gD/AAH/A/AB/AAH4A/AA/gAH4A+AAfgAH4B+AAfgAH4B+AAfgAH4B8AAfgAH4B8AAfgAH4B+AAfgAH4B+AAfgAH4B+AA/gAH4B/AA/AAH4A/gD/AAH4A/4H+AAH4Af//+AAH4AP//8AAH4AP//4AAHwAD//wAAAAAB//AAAAAAAf8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///8AAAAD////AAAAP////wAAAf////4AAA/////8AAB/////+AAD/gP4H+AAD/AfgD/AAH8A/AB/AAH8A/AA/gAH4B+AAfgAH4B+AAfgAPwB8AAfgAPwB8AAfgAPwB+AAfgAPwB+AAfgAH4B+AAfgAH4B/AA/gAH8B/AB/AAH+A/wD/AAD+A/8P+AAB8Af//+AAB4AP//8AAAwAH//4AAAAAD//gAAAAAA//AAAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPwAAAAAAAPwAAAAAAAPwAAAAAAAPwAAAAAAAPwAAAAHAAPwAAAA/AAPwAAAD/AAPwAAAf/AAPwAAB//AAPwAAP//AAPwAA//8AAPwAH//wAAPwAf/+AAAPwB//4AAAPwP//AAAAPw//8AAAAP3//gAAAAP//+AAAAAP//wAAAAAP//AAAAAAP/4AAAAAAP/gAAAAAAP+AAAAAAAHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAH+A//gAAAf/h//4AAA//z//8AAB/////+AAD/////+AAD///+H/AAH+H/4B/AAH8B/wA/gAH4A/gAfgAH4A/gAfgAPwA/AAfgAPwA/AAfgAPwA/AAfgAPwA/AAfgAH4A/gAfgAH4A/gAfgAH8B/wA/gAH/H/4B/AAD///+H/AAD/////+AAB/////+AAA//z//8AAAf/h//4AAAH+A//gAAAAAAH+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAD/8AAAAAAP/+AAAAAAf//AAcAAA///gA8AAB///wB+AAD/x/4B/AAD+AP4B/AAH8AH8A/gAH4AH8A/gAH4AD8AfgAP4AD8AfgAPwAB8AfgAPwAB8AfgAPwAB8AfgAPwAB8AfgAH4AD8AfgAH4AD4A/gAH8AH4B/AAD+APwD/AAD/g/wP+AAB/////+AAA/////8AAAf////4AAAP////wAAAH////AAAAA///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8APwAAAAD8APwAAAAD8APwAAAAD8APwAAAAD8APwAAAAD8APwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("DxcjFyAfISAiHCAiEg=="), 54+(scale<<8)+(1<<16));
return this;
};
Graphics.prototype.setSmallFont = function(scale) {
// Actual height 28 (27 - 0)
this.setFontCustom(
@ -64,7 +63,6 @@ Graphics.prototype.setSmallFont = function(scale) {
return this;
};
Graphics.prototype.setMiniFont = function(scale) {
// Actual height 16 (15 - 0)
this.setFontCustom(
@ -76,8 +74,6 @@ Graphics.prototype.setMiniFont = function(scale) {
return this;
};
function imgLock(){
return {
width : 16, height : 16, bpp : 1,
@ -86,434 +82,100 @@ function imgLock(){
}
}
function imgSteps(){
return {
width : 24, height : 24, bpp : 1,
transparent : 1,
buffer : require("heatshrink").decompress(atob("/H///wv4CBn4CD8ACCj4IBj8f+Eeh/wjgCBngCCg/4nEH//4h/+jEP/gRBAQX+jkf/wgB//8GwP4FoICDHgICCBwIA=="))
}
}
function imgBattery(){
return {
width : 24, height : 24, bpp : 1,
transparent : 1,
buffer : require("heatshrink").decompress(atob("/4AN4EAg4TBgd///9oEAAQv8ARQRDDQQgCEwQ4OA"))
}
}
function imgCharging() {
return {
width : 24, height : 24, bpp : 1,
transparent : 1,
buffer : require("heatshrink").decompress(atob("//+v///k///4AQPwBANgBoMxBoMb/P+h/w/kH8H4gfB+EBwfggHH4EAt4CBn4CBj4CBh4FCCIO/8EB//Agf/wEH/8Gh//x////fAQIA="))
}
}
function imgBpm() {
return {
width : 24, height : 24, bpp : 1,
transparent : 1,
buffer : require("heatshrink").decompress(atob("/4AOn4CD/wCCjgCCv/8jF/wGYgOA5MB//BC4PDAQnjAQPnAQgANA"))
}
}
function imgTemperature() {
return {
width : 24, height : 24, bpp : 1,
transparent : 1,
buffer : require("heatshrink").decompress(atob("//D///wICBjACBngCNkgCP/0kv/+s1//nDn/8wICEBAIOC/08v//IYJECA=="))
}
}
function imgWeather(){
return {
width : 24, height : 24, bpp : 1,
transparent : 0,
buffer : require("heatshrink").decompress(atob("AAcYAQ0MgEwAQUAngLB/8AgP/wACCgf/4Fz//OAQQICCIoaCEAQpGHA4ACA="))
}
}
function imgWind () {
return {
width : 24, height : 24, bpp : 1,
transparent : 1,
buffer : require("heatshrink").decompress(atob("/0f//8h///Pn//zAQXzwf/88B//mvGAh18gEevn/DIICB/PwgEBAQMHBAIADFwM/wEAGAP/54CD84CE+eP//wIQU/A=="))
}
}
function imgHumidity () {
return {
width : 24, height : 24, bpp : 1,
transparent : 1,
buffer : require("heatshrink").decompress(atob("//7///+YCB+ICB8ACE4F/AQX9AQP54H//AOB+F/34CBj/gn8f4E+h/Aj0H4Ecg+AjED4ACE8E4gfwvEDEgICB/kHGwMP"))
}
}
function imgTimer() {
return {
width : 24, height : 24, bpp : 1,
transparent : 1,
buffer : require("heatshrink").decompress(atob("/+B/4CD84CEBAPygFP+F+h/x/+P+fz5/n+HnAQNn5/wuYCBmYCC5kAAQfOgFz80As/ngHn+fD54mC/F+j/+gF/HAQA=="))
}
}
function imgWatch() {
return {
width : 24, height : 24, bpp : 1,
transparent : 1,
buffer : require("heatshrink").decompress(atob("/8B//+ARANB/l4//5/1/+f/n/n5+fAQnf9/P44CC8/n7/n+YOB/+fDQQgCEwQsCHBBEC"))
}
}
function imgHomeAssistant() {
return {
width : 24, height : 24, bpp : 1,
transparent : 1,
buffer : require("heatshrink").decompress(atob("/4AF84CB4YCBwICBCAP+jFH/k8g/4kkH+AFB8ACB4cY4eHzPhgmZkHnzPn8fb4/gvwUD8EYARhAC"))
}
}
function imgAgenda() {
return {
width : 24, height : 24, bpp : 1,
transparent : 1,
buffer : require("heatshrink").decompress(atob("/4AFnPP+ALBAQX4CIgLFAQvggEBAQvAgEDAQMCwEAgwTBhgiB/AlCGQ8BGQQ"))
}
}
function imgMountain() {
return {
width : 24, height : 24, bpp : 1,
transparent : 1,
buffer : atob("//////////////////////3///n///D//uZ//E8//A+/+Z+f8//P5//n7//3z//zn//5AAAAAAAA////////////////////")
}
}
/************
* 2D MENU with entries of:
* [name, icon, opt[customDownFun], opt[customUpFun], opt[customCenterFun]]
*
/************************************************
* Menu
*/
var menu = [
[
function(){ return [ null, null ] },
function(){ return [ "Week " + weekOfYear(), null ] },
],
[
function(){ return [ "Bangle", imgWatch() ] },
function(){ return [ E.getBattery() + "%", Bangle.isCharging() ? imgCharging() : imgBattery() ] },
function(){ return [ getSteps(), imgSteps() ] },
function(){ return [ Math.round(Bangle.getHealthStatus("last").bpm) + " bpm", imgBpm()] },
function(){ return [ measureAltitude, imgMountain() ]},
// Custom bwItems menu - therefore, its added here and not in a clkinfo.js file.
var bwItems = {
name: null,
img: null,
items: [
{ name: "WeekOfYear",
get: () => ({ text: "Week " + weekOfYear(), img: null}),
show: function() { bwItems.items[0].emit("redraw"); },
hide: function () {}
},
]
]
};
/*
* Timer Menu
*/
try{
require('sched');
menu.push([
function(){
var text = isAlarmEnabled(TIMER_IDX) ? getAlarmMinutes(TIMER_IDX) + " min." : "Timer";
return [text, imgTimer(), () => decreaseAlarm(TIMER_IDX), () => increaseAlarm(TIMER_IDX), null ]
},
]);
} catch(ex) {
// If sched is not installed, we hide this menu item
}
/*
* AGENDA MENU
* Note that we handle the agenda differently in order to hide old entries...
*/
var agendaIdx = 0;
var agendaTimerIdx = 0;
if(storage.readJSON("android.calendar.json") !== undefined){
function nextAgendaEntry(){
agendaIdx += 1;
}
function previousAgendaEntry(){
agendaIdx -= 1;
}
menu.push([
function(){
var now = new Date();
var agenda = storage.readJSON("android.calendar.json")
.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000)
.sort((a,b)=>a.timestamp - b.timestamp);
if(agenda.length <= 0){
return ["All done", imgAgenda()]
}
agendaIdx = agendaIdx < 0 ? 0 : agendaIdx;
agendaIdx = agendaIdx >= agenda.length ? agendaIdx -1 : agendaIdx;
var entry = agenda[agendaIdx];
var title = entry.title.slice(0,14);
var date = new Date(entry.timestamp*1000);
var dateStr = locale.date(date).replace(/\d\d\d\d/,"");
dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : "";
function dynImgAgenda(){
if(isAlarmEnabled(TIMER_AGENDA_IDX) && agendaTimerIdx == agendaIdx){
return imgTimer();
} else {
return imgAgenda();
}
}
return [title + "\n" + dateStr, dynImgAgenda(), () => nextAgendaEntry(), () => previousAgendaEntry(), function(){
try{
var alarm = require('sched')
// If other time, we disable the old one and enable this one.
if(agendaIdx != agendaTimerIdx){
agendaTimerIdx = -1;
alarm.setAlarm(TIMER_AGENDA_IDX, undefined);
}
// Disable alarm if enabled
if(isAlarmEnabled(TIMER_AGENDA_IDX)){
agendaTimerIdx = -1;
alarm.setAlarm(TIMER_AGENDA_IDX, undefined);
alarm.reload();
return
}
// Otherwise, set alarm for given event
agendaTimerIdx = agendaIdx;
alarm.setAlarm(TIMER_AGENDA_IDX, {
msg: title,
timer : parseInt((date - now)),
});
alarm.reload();
} catch(ex){ }
}]
},
]);
}
/*
* WEATHER MENU
*/
if(storage.readJSON('weather.json') !== undefined){
menu.push([
function(){ return [ "Weather", imgWeather() ] },
function(){ return [ getWeather().temp, imgTemperature() ] },
function(){ return [ getWeather().hum, imgHumidity() ] },
function(){ return [ getWeather().wind, imgWind() ] },
]);
}
/*
* HOME ASSISTANT MENU
*/
try{
var triggers = require("ha.lib.js").getTriggers();
var haMenu = [
function(){ return [ "Home", imgHomeAssistant() ] },
];
triggers.forEach(trigger => {
haMenu.push(function(){
return [trigger.display, trigger.getIcon(), () => {}, () => {}, function(){
var ha = require("ha.lib.js");
ha.sendTrigger("TRIGGER_BW");
ha.sendTrigger(trigger.trigger);
}]
});
})
menu.push(haMenu);
} catch(ex){
// If HomeAssistant is not installed, we hide this item
}
function getMenuEntry(){
// In case the user removes HomeAssistant entries, showInfo
// could be larger than infoArray.length...
settings.menuPosX = settings.menuPosX % menu.length;
settings.menuPosY = settings.menuPosY % menu[settings.menuPosX].length;
var menuEntry = menu[settings.menuPosX][settings.menuPosY]();
if(menuEntry[0] == null){
return menuEntry;
}
// For the first entry we always convert it into a callback function
// such that the menu is compatible with async functions such as
// measuring the pressure, altitude or sending http requests...
if(typeof menuEntry[0] !== 'function'){
var value = menuEntry[0];
menuEntry[0] = function(callbackFun){
callbackFun(String(value), settings.menuPosX, settings.menuPosY);
}
}
return menuEntry;
}
/************
* Helper
*/
function isFullscreen(){
var s = settings.screen.toLowerCase();
if(s == "dynamic"){
return Bangle.isLocked()
} else {
return s == "full"
}
}
function getSteps() {
var steps = 0;
try{
if (WIDGETS.wpedom !== undefined) {
steps = WIDGETS.wpedom.getSteps();
} else if (WIDGETS.activepedom !== undefined) {
steps = WIDGETS.activepedom.getSteps();
} else {
steps = Bangle.getHealthStatus("day").steps;
}
} catch(ex) {
// In case we failed, we can only show 0 steps.
}
return steps;
}
function getWeather(){
var weatherJson;
try {
weatherJson = storage.readJSON('weather.json');
var weather = weatherJson.weather;
// Temperature
weather.temp = locale.temp(weather.temp-273.15);
// Humidity
weather.hum = weather.hum + "%";
// Wind
const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
weather.wind = Math.round(wind[1]) + "kph";
return weather
} catch(ex) {
// Return default
}
return {
temp: " ? ",
hum: " ? ",
txt: " ? ",
wind: " ? ",
wdir: " ? ",
wrose: " ? "
};
}
// From https://weeknumber.com/how-to/javascript
function weekOfYear() {
var date = new Date();
date.setHours(0, 0, 0, 0);
// Thursday in current week decides the year.
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
// January 4 is always in week 1.
var week1 = new Date(date.getFullYear(), 0, 4);
// Adjust to Thursday in week 1 and count number of weeks from date to week1.
return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000
- 3 + (week1.getDay() + 6) % 7) / 7);
var date = new Date();
date.setHours(0, 0, 0, 0);
// Thursday in current week decides the year.
date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7);
// January 4 is always in week 1.
var week1 = new Date(date.getFullYear(), 0, 4);
// Adjust to Thursday in week 1 and count number of weeks from date to week1.
return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000
- 3 + (week1.getDay() + 6) % 7) / 7);
}
function isAlarmEnabled(idx){
try{
var alarm = require('sched');
var alarmObj = alarm.getAlarm(idx);
if(alarmObj===undefined || !alarmObj.on){
return false;
// Load menu
var menu = clock_info.load();
menu = menu.concat(bwItems);
// Ensure that our settings are still in range (e.g. app uninstall). Otherwise reset the position it.
if(settings.menuPosX >= menu.length || settings.menuPosY > menu[settings.menuPosX].items.length ){
settings.menuPosX = 0;
settings.menuPosY = 0;
}
// Set draw functions for each item
menu.forEach((menuItm, x) => {
menuItm.items.forEach((item, y) => {
function drawItem() {
// For the clock, we have a special case, as we don't wanna redraw
// immediately when something changes. Instead, we update data each minute
// to save some battery etc. Therefore, we hide (and disable the listener)
// immedeately after redraw...
item.hide();
// After drawing the item, we enable inputs again...
lock_input = false;
var info = item.get();
drawMenuItem(info.text, info.img);
}
return true;
} catch(ex){ }
return false;
}
item.on('redraw', drawItem);
})
});
function getAlarmMinutes(idx){
if(!isAlarmEnabled(idx)){
return -1;
function canRunMenuItem(){
if(settings.menuPosY == 0){
return false;
}
var alarm = require('sched');
var alarmObj = alarm.getAlarm(idx);
return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000));
var menuEntry = menu[settings.menuPosX];
var item = menuEntry.items[settings.menuPosY-1];
return item.run !== undefined;
}
function increaseAlarm(idx){
function runMenuItem(){
if(settings.menuPosY == 0){
return;
}
var menuEntry = menu[settings.menuPosX];
var item = menuEntry.items[settings.menuPosY-1];
try{
var minutes = isAlarmEnabled(idx) ? getAlarmMinutes(idx) : 0;
var alarm = require('sched');
alarm.setAlarm(idx, {
timer : (minutes+5)*60*1000,
});
alarm.reload();
} catch(ex){ }
}
function decreaseAlarm(idx){
try{
var minutes = getAlarmMinutes(idx);
minutes -= 5;
var alarm = require('sched')
alarm.setAlarm(idx, undefined);
if(minutes > 0){
alarm.setAlarm(idx, {
timer : minutes*60*1000,
});
var ret = item.run();
if(ret){
Bangle.buzz(300, 0.6);
}
alarm.reload();
} catch(ex){ }
}
function measureAltitude(callbackFun){
var oldX = settings.menuPosX;
var oldY = settings.menuPosY;
try{
Bangle.getPressure().then(data=>{
if(data && data.altitude && data.altitude > -100){
callbackFun(Math.round(data.altitude) + "m", oldX, oldY);
} else {
callbackFun("???", oldX, oldY);
}
});
}catch(ex){
callbackFun("err", oldX, oldY);
} catch (ex) {
// Simply ignore it...
}
}
/************
* DRAW
/************************************************
* Draw
*/
function draw() {
// Queue draw again
@ -521,7 +183,7 @@ function draw() {
// Draw clock
drawDate();
drawTime();
drawMenuAndTime();
drawLock();
drawWidgets();
}
@ -529,12 +191,12 @@ function draw() {
function drawDate(){
// Draw background
var y = H/5*2;
g.reset().clearRect(0,0,W,W);
var y = H/5*2 + (isFullscreen() ? 0 : 8);
g.reset().clearRect(0,0,W,y);
// Draw date
y = parseInt(y/2)+4;
y += isFullscreen() ? 0 : 13;
y += isFullscreen() ? 0 : 8;
var date = new Date();
var dateStr = date.getDate();
dateStr = ("0" + dateStr).substr(-2);
@ -557,11 +219,8 @@ function drawDate(){
}
function drawTime(){
function drawTime(y, smallText){
// Draw background
var y = H/5*2 + (isFullscreen() ? 0 : 8);
g.setColor(g.theme.fg);
g.fillRect(0,y,W,H);
var date = new Date();
// Draw time
@ -577,56 +236,65 @@ function drawTime(){
// Set y coordinates correctly
y += parseInt((H - y)/2) + 5;
var menuEntry = getMenuEntry();
var menuTextFun = menuEntry[0];
var menuImg = menuEntry[1];
var printImgLeft = settings.menuPosY != 0;
// Show large or small time depending on info entry
if(menuTextFun == null){
g.setLargeFont();
g.drawString(timeStr, W/2, y);
return;
} else {
if(smallText){
y -= 15;
g.setMediumFont();
g.drawString(timeStr, W/2, y);
} else {
g.setLargeFont();
}
g.drawString(timeStr, W/2, y);
}
// Async set the menu (could be that some data is async fetched)
menuTextFun((menuText, oldX, oldY) => {
function drawMenuItem(text, image){
// First clear the time region
var y = H/5*2 + (isFullscreen() ? 0 : 8);
// We display the text IFF the user did not change the menu
if(settings.menuPosX != oldX || settings.menuPosY != oldY){
return;
}
g.setColor(g.theme.fg);
g.fillRect(0,y,W,H);
// As its a callback, we have to ensure that the color
// font etc. is still correct...
g.setColor(g.theme.bg);
// Draw menu text
var hasText = (text != null && text != "");
if(hasText){
g.setFontAlign(0,0);
y += 35;
if(menuText.split('\n').length > 1){
// For multiline text we show an even smaller font...
text = String(text);
if(text.split('\n').length > 1){
g.setMiniFont();
} else {
g.setSmallFont();
}
var imgWidth = 0;
if(menuImg){
imgWidth = 24.0;
var strWidth = g.stringWidth(menuText);
var scale = imgWidth / menuImg.width;
g.drawImage(
menuImg,
W/2 + (printImgLeft ? -strWidth/2-4 : strWidth/2+4) - parseInt(imgWidth/2),
y - parseInt(imgWidth/2),
{ scale: scale }
);
var imgWidth = image == null ? 0 : 24;
var strWidth = g.stringWidth(text);
g.setColor(g.theme.fg).fillRect(0, 149-14, W, H);
g.setColor(g.theme.bg).drawString(text, W/2 + imgWidth/2 + 2, 149+3);
if(image != null){
var scale = imgWidth / image.width;
g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 149 - parseInt(imgWidth/2), {scale: scale});
}
g.drawString(menuText, printImgLeft ? W/2 + imgWidth/2 + 2 : W/2 - imgWidth/2 - 2, y+3);
});
}
// Draw time
drawTime(y, hasText);
}
function drawMenuAndTime(){
var menuEntry = menu[settings.menuPosX];
// The first entry is the overview...
if(settings.menuPosY == 0){
drawMenuItem(menuEntry.name, menuEntry.img);
return;
}
// Draw item if needed
lock_input = true;
var item = menuEntry.items[settings.menuPosY-1];
item.show();
}
@ -647,9 +315,19 @@ function drawWidgets(){
}
function isFullscreen(){
var s = settings.screen.toLowerCase();
if(s == "dynamic"){
return Bangle.isLocked()
} else {
return s == "full"
}
}
/*
* Draw timeout
/************************************************
* Listener
*/
// timeout used to update every minute
var drawTimeout;
@ -692,7 +370,7 @@ Bangle.on('charging',function(charging) {
drawTimeout = undefined;
// Jump to battery
settings.menuPosX = 1;
settings.menuPosX = 0;
settings.menuPosY = 1;
draw();
});
@ -710,17 +388,15 @@ Bangle.on('touch', function(btn, e){
var is_right = e.x > right && !is_upper && !is_lower;
var is_center = !is_upper && !is_lower && !is_left && !is_right;
if(lock_input){
return;
}
if(is_lower){
Bangle.buzz(40, 0.6);
settings.menuPosY = (settings.menuPosY+1) % menu[settings.menuPosX].length;
settings.menuPosY = (settings.menuPosY+1) % (menu[settings.menuPosX].items.length+1);
// Handle custom menu entry function
var menuEntry = getMenuEntry();
if(menuEntry.length > 2){
menuEntry[2]();
}
drawTime();
drawMenuAndTime();
}
if(is_upper){
@ -730,53 +406,29 @@ Bangle.on('touch', function(btn, e){
Bangle.buzz(40, 0.6);
settings.menuPosY = settings.menuPosY-1;
settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].length-1 : settings.menuPosY;
settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].items.length : settings.menuPosY;
// Handle custom menu entry function
var menuEntry = getMenuEntry();
if(menuEntry.length > 3){
menuEntry[3]();
}
drawTime();
drawMenuAndTime();
}
if(is_right){
// A bit hacky but we ensure that always the first agenda entry is shown...
agendaIdx = 0;
Bangle.buzz(40, 0.6);
settings.menuPosX = (settings.menuPosX+1) % menu.length;
settings.menuPosY = 0;
drawTime();
drawMenuAndTime();
}
if(is_left){
// A bit hacky but we ensure that always the first agenda entry is shown...
agendaIdx = 0;
Bangle.buzz(40, 0.6);
settings.menuPosY = 0;
settings.menuPosX = settings.menuPosX-1;
settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX;
drawTime();
drawMenuAndTime();
}
if(is_center){
var menuEntry = getMenuEntry();
if(menuEntry.length > 4 && menuEntry[4] != null){
Bangle.buzz(80, 0.6).then(()=>{
try{
menuEntry[4]();
setTimeout(()=>{
Bangle.buzz(80, 0.6);
drawTime();
}, 250);
} catch(ex){
// In case it fails, we simply ignore it.
}
}
);
if(canRunMenuItem()){
runMenuItem();
}
}
});
@ -791,9 +443,10 @@ E.on("kill", function(){
});
/*
* Draw clock the first time
/************************************************
* Startup Clock
*/
// The upper part is inverse i.e. light if dark and dark if light theme
// is enabled. In order to draw the widgets correctly, we invert the
// dark/light theme as well as the colors.

View File

@ -1,7 +1,7 @@
{
"id": "bwclk",
"name": "BW Clock",
"version": "0.21",
"version": "0.23",
"description": "A very minimalistic clock to mainly show date and time.",
"readme": "README.md",
"icon": "app.png",

View File

@ -1,2 +1,4 @@
0.01: Release
0.02: Includeas the ha.lib.js library that can be used by other apps or clocks.
0.02: Includeas the ha.lib.js library that can be used by other apps or clocks.
0.03: Added clkinfo for clocks.
0.04: Feedback if clkinfo run is called.

26
apps/ha/ha.clkinfo.js Normal file
View File

@ -0,0 +1,26 @@
(function() {
var ha = require("ha.lib.js");
var triggers = ha.getTriggers();
var haItems = {
name: "Home",
img: atob("GBiBAf/////////n///D//+B//8A//48T/wkD/gkD/A8D+AYB8AYA4eZ4QyZMOyZN+fb5+D/B+B+B+A8B+AYB+AYB+AYB+AYB+A8Bw=="),
items: []
};
triggers.forEach((trigger, i) => {
haItems.items.push({
name: "haTrigger-" + i,
get: () => ({ text: trigger.display, img: trigger.getIcon()}),
show: function() { haItems.items[i].emit("redraw"); },
hide: function () {},
run: function() {
ha.sendTrigger("TRIGGER_BW");
ha.sendTrigger(trigger.trigger);
return true;
}
});
});
return haItems;
})

View File

@ -1,7 +1,7 @@
{
"id": "ha",
"name": "HomeAssistant",
"version": "0.02",
"version": "0.04",
"description": "Integrates your BangleJS into HomeAssistant.",
"icon": "ha.png",
"type": "app",
@ -20,6 +20,7 @@
"storage": [
{"name":"ha.app.js","url":"ha.app.js"},
{"name":"ha.lib.js","url":"ha.lib.js"},
{"name":"ha.clkinfo.js","url":"ha.clkinfo.js"},
{"name":"ha.img","url":"ha.icon.js","evaluate":true}
]
}

View File

@ -14,3 +14,4 @@
Add /*LANG*/ tags for internationalisation
0.13: Add fullscreen mode
0.14: Use default Bangle formatter for booleans
0.15: Support for unload and quick return to the clock on 2v16

View File

@ -1,7 +1,8 @@
var s = require("Storage");
var scaleval = 1;
var vectorval = 20;
var font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
{ // must be inside our own scope here so that when we are unloaded everything disappears
let s = require("Storage");
let scaleval = 1;
let vectorval = 20;
let font = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
let settings = Object.assign({
showClocks: true,
fullscreen: false
@ -20,7 +21,7 @@ if ("font" in settings){
scaleval = (font.split("x")[1])/20;
}
}
var apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || !app.type));
let apps = s.list(/\.info$/).map(app=>{var a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || !app.type));
apps.sort((a,b)=>{
var n=(0|a.sortorder)-(0|b.sortorder);
if (n) return n; // do sortorder first
@ -69,18 +70,30 @@ E.showScroller({
}
});
function returnToClock() {
// unload everything manually
// ... or we could just call `load();` but it will be slower
Bangle.setUI(); // remove scroller's handling
if (lockTimeout) clearTimeout(lockTimeout);
Bangle.removeListener("lock", lockHandler);
// now load the default clock - just call .bootcde as this has the code already
setTimeout(eval,0,s.read(".bootcde"));
}
// on bangle.js 2, the screen is used for navigating, so the single button goes back
// on bangle.js 1, the buttons are used for navigating
if (process.env.HWVERSION==2) {
setWatch(_=>load(), BTN1, {edge:"falling"});
setWatch(returnToClock, BTN1, {edge:"falling"});
}
// 10s of inactivity goes back to clock
Bangle.setLocked(false); // unlock initially
var lockTimeout;
Bangle.on("lock", locked => {
let lockTimeout;
function lockHandler(locked) {
if (lockTimeout) clearTimeout(lockTimeout);
lockTimeout = undefined;
if (locked)
lockTimeout = setTimeout(_=>load(), 10000);
});
lockTimeout = setTimeout(returnToClock, 10000);
}
Bangle.on("lock", lockHandler);
}

View File

@ -2,7 +2,7 @@
"id": "launch",
"name": "Launcher",
"shortName": "Launcher",
"version": "0.14",
"version": "0.15",
"description": "This is needed to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.",
"readme": "README.md",
"icon": "app.png",

View File

@ -66,4 +66,8 @@
0.50: Add `getMessages` and `status` functions to library
Option to disable auto-open of messages
Option to make message icons monochrome (not colored)
messages widget buzz now returns a promise
messages widget buzz now returns a promise
0.51: Emit "message events"
Setting to hide widget
Add custom event handlers to prevent default app form loading
Move WIDGETS.messages.buzz() to require("messages").buzz()

View File

@ -25,7 +25,7 @@ it starts getting clipped.
* `Auto-Open Music` - Should the app automatically open when the phone starts playing music?
* `Unlock Watch` - Should the app unlock the watch when a new message arrives, so you can touch the buttons at the bottom of the app?
* `Flash Icon` - Toggle flashing of the widget icon.
* `Widget messages` - The maximum amount of message icons to show on the widget.
* `Widget messages` - The maximum amount of message icons to show on the widget, or `Hide` the widget completely.
## New Messages
@ -56,6 +56,24 @@ _2. What the notify icon looks like (it's touchable on Bangle.js2!)_
![](screenshot-notify.gif)
## Events (for app/widget developers)
When a new message arrives, a `"message"` event is emitted, you can listen for
it like this:
```js
myMessageListener = Bangle.on("message", (type, message)=>{
if (message.handled) return; // another app already handled this message
// <type> is one of "text", "call", "alarm", "map", "music", or "clearAll"
if (type === "clearAll") return; // not a message
// see `messages/lib.js` for possible <message> formats
// message.t could be "add", "modify" or "remove"
E.showMessage(`${message.title}\n${message.body}`, `${message.t} ${type} message`);
// You can prevent the default `message` app from loading by setting `message.handled = true`:
message.handled = true;
});
```
## Requests

View File

@ -54,8 +54,7 @@ var onMessagesModified = function(msg) {
// TODO: if new, show this new one
if (msg && msg.id!=="music" && msg.new && active!="map" &&
!((require('Storage').readJSON('setting.json', 1) || {}).quiet)) {
if (WIDGETS["messages"]) WIDGETS["messages"].buzz(msg.src);
else Bangle.buzz();
require("messages").buzz(msg.src);
}
if (msg && msg.id=="music") {
if (msg.state && msg.state!="play") openMusic = false; // no longer playing music to go back to
@ -356,13 +355,13 @@ function checkMessages(options) {
// If we have a new message, show it
if (options.showMsgIfUnread && newMessages.length) {
showMessage(newMessages[0].id);
// buzz after showMessage, so beingbusy during layout doesn't affect the buzz pattern
// buzz after showMessage, so being busy during layout doesn't affect the buzz pattern
if (global.BUZZ_ON_NEW_MESSAGE) {
// this is set if we entered the messages app by loading `messages.new.js`
// ... but only buzz the first time we view a new message
global.BUZZ_ON_NEW_MESSAGE = false;
// messages.buzz respects quiet mode - no need to check here
WIDGETS.messages.buzz(newMessages[0].src);
require("messages").buzz(newMessages[0].src);
}
return;
}

View File

@ -8,15 +8,11 @@ function openMusic() {
/* Push a new message onto messages queue, event is:
{t:"add",id:int, src,title,subject,body,sender,tel, important:bool, new:bool}
{t:"add",id:int, id:"music", state, artist, track, etc} // add new
{t:"remove-",id:int} // remove
{t:"remove",id:int} // remove
{t:"modify",id:int, title:string} // modified
*/
exports.pushMessage = function(event) {
var messages, inApp = "undefined"!=typeof MESSAGES;
if (inApp)
messages = MESSAGES; // we're in an app that has already loaded messages
else // no app - load messages
messages = require("Storage").readJSON("messages.json",1)||[];
var messages = exports.getMessages();
// now modify/delete as appropriate
var mIdx = messages.findIndex(m=>m.id==event.id);
if (event.t=="remove") {
@ -35,69 +31,81 @@ exports.pushMessage = function(event) {
else Object.assign(messages[mIdx], event);
if (event.id=="music" && messages[mIdx].state=="play") {
messages[mIdx].new = true; // new track, or playback (re)started
type = 'music';
}
}
require("Storage").writeJSON("messages.json",messages);
var message = mIdx<0 ? {id:event.id, t:'remove'} : messages[mIdx];
// if in app, process immediately
if (inApp) return onMessagesModified(mIdx<0 ? {id:event.id} : messages[mIdx]);
if ("undefined"!=typeof MESSAGES) return onMessagesModified(message);
// emit message event
var type = 'text';
if (["call", "music", "map"].includes(message.id)) type = message.id;
if (message.src && message.src.toLowerCase().startsWith("alarm")) type = "alarm";
Bangle.emit("message", type, message);
// update the widget icons shown
if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages,true);
var handleMessage = () => {
// if no new messages now, make sure we don't load the messages app
if (event.t=="remove" && exports.messageTimeout && !messages.some(m=>m.new)) {
clearTimeout(exports.messageTimeout);
delete exports.messageTimeout;
}
// ok, saved now
if (event.id=="music" && Bangle.CLOCK && messages[mIdx].new && openMusic()) {
// just load the app to display music: no buzzing
load("messages.app.js");
} else if (event.t!="add") {
// we only care if it's new
return;
} else if(event.new == false) {
return;
}
// otherwise load messages/show widget
var loadMessages = Bangle.CLOCK || event.important;
var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet;
var appSettings = require('Storage').readJSON('messages.settings.json',1)||{};
var unlockWatch = appSettings.unlockWatch;
// don't auto-open messages in quiet mode if quietNoAutOpn is true
if((quiet && appSettings.quietNoAutOpn) || appSettings.noAutOpn)
loadMessages = false;
delete appSettings;
// after a delay load the app, to ensure we have all the messages
if (exports.messageTimeout) clearTimeout(exports.messageTimeout);
exports.messageTimeout = setTimeout(function() {
exports.messageTimeout = undefined;
// if we're in a clock or it's important, go straight to messages app
if (loadMessages){
if(!quiet && unlockWatch){
Bangle.setLocked(false);
Bangle.setLCDPower(1); // turn screen on
}
// we will buzz when we enter the messages app
return load("messages.new.js");
if (event.t=="remove" && exports.messageTimeout && !messages.some(m => m.new)) {
clearTimeout(exports.messageTimeout);
delete exports.messageTimeout;
}
if (!quiet && (!global.WIDGETS || !WIDGETS.messages)) return Bangle.buzz(); // no widgets - just buzz once to let someone know
if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages);
}, 500);
// ok, saved now
if (event.id=="music" && Bangle.CLOCK && messages[mIdx].new && openMusic()) {
// just load the app to display music: no buzzing
load("messages.app.js");
} else if (event.t!="add") {
// we only care if it's new
return;
} else if (event.new==false) {
return;
}
// otherwise load messages/show widget
var loadMessages = Bangle.CLOCK || event.important;
var quiet = (require('Storage').readJSON('setting.json', 1) || {}).quiet;
var appSettings = require('Storage').readJSON('messages.settings.json', 1) || {};
var unlockWatch = appSettings.unlockWatch;
// don't auto-open messages in quiet mode if quietNoAutOpn is true
if ((quiet && appSettings.quietNoAutOpn) || appSettings.noAutOpn)
loadMessages = false;
delete appSettings;
// after a delay load the app, to ensure we have all the messages
if (exports.messageTimeout) clearTimeout(exports.messageTimeout);
exports.messageTimeout = setTimeout(function() {
exports.messageTimeout = undefined;
// if we're in a clock or it's important, go straight to messages app
if (loadMessages) {
if (!quiet && unlockWatch) {
Bangle.setLocked(false);
Bangle.setLCDPower(1); // turn screen on
}
// we will buzz when we enter the messages app
return load("messages.new.js");
}
if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages);
exports.buzz(message.src);
}, 500);
};
setTimeout(()=>{
if (!message.handled) handleMessage();
},0);
}
/// Remove all messages
exports.clearAll = function(event) {
var messages, inApp = "undefined"!=typeof MESSAGES;
if (inApp) {
exports.clearAll = function() {
if ("undefined"!= typeof MESSAGES) { // we're in a messages app, clear that as well
MESSAGES = [];
messages = MESSAGES; // we're in an app that has already loaded messages
} else // no app - empty messages
messages = [];
// Save all messages
require("Storage").writeJSON("messages.json",messages);
// update app if in app
if (inApp) return onMessagesModified();
}
// Clear all messages
require("Storage").writeJSON("messages.json", []);
// if we have a widget, update it
if (global.WIDGETS && WIDGETS.messages)
WIDGETS.messages.update(messages);
WIDGETS.messages.update([]);
// let message listeners know
Bangle.emit("message", "clearAll", {}); // guarantee listeners an object as `message`
// clearAll cannot be marked as "handled"
// update app if in app
if ("function"== typeof onMessagesModified) onMessagesModified();
}
/**
@ -126,6 +134,45 @@ exports.getMessages = function() {
}
};
/**
* Start buzzing for new message
* @param {string} msgSrc Message src to buzz for
* @return {Promise} Resolves when initial buzz finishes (there might be repeat buzzes later)
*/
exports.buzz = function(msgSrc) {
exports.stopBuzz(); // cancel any previous buzz timeouts
if ((require('Storage').readJSON('setting.json',1)||{}).quiet) return Promise.resolve(); // never buzz during Quiet Mode
var pattern;
if (msgSrc && msgSrc.toLowerCase() === "phone") {
// special vibration pattern for incoming calls
pattern = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrateCalls;
} else {
pattern = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrate;
}
if (pattern === undefined) { pattern = ":"; } // pattern may be "", so we can't use || ":" here
if (!pattern) return Promise.resolve();
var repeat = (require('Storage').readJSON("messages.settings.json", true) || {}).repeat;
if (repeat===undefined) repeat=4; // repeat may be zero
if (repeat) {
exports.buzzTimeout = setTimeout(()=>require("buzz").pattern(pattern), repeat*1000);
var vibrateTimeout = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrateTimeout;
if (vibrateTimeout===undefined) vibrateTimeout=60;
if (vibrateTimeout && !exports.stopTimeout) exports.stopTimeout = setTimeout(exports.stopTimeout, vibrateTimeout*1000);
}
return require("buzz").pattern(pattern);
};
/**
* Stop buzzing
*/
exports.stopBuzz = function() {
if (exports.buzzTimeout) clearTimeout(exports.buzzTimeout);
delete exports.buzzTimeout;
if (exports.stopTimeout) clearTimeout(exports.stopTimeout);
delete exports.stopTimeout;
};
exports.getMessageImage = function(msg) {
/*
* icons should be 24x24px or less with 1bpp colors and 'Transparency to Color'

View File

@ -1,7 +1,7 @@
{
"id": "messages",
"name": "Messages",
"version": "0.50",
"version": "0.51",
"description": "App to display notifications from iOS and Gadgetbridge/Android",
"icon": "app.png",
"type": "app",

View File

@ -73,7 +73,8 @@
},
/*LANG*/'Widget messages': {
value:0|settings().maxMessages,
min: 1, max: 5,
min: 0, max: 5,
format: v => v ? v :/*LANG*/"Hide",
onchange: v => updateSetting("maxMessages", v)
},
/*LANG*/'Icon color mode': {

View File

@ -1,4 +1,5 @@
(() => {
if ((require('Storage').readJSON("messages.settings.json", true) || {}).maxMessages===0) return;
function filterMessages(msgs) {
return msgs.filter(msg => msg.new && msg.id != "music")
@ -14,15 +15,14 @@ WIDGETS["messages"]={area:"tl", width:0, draw:function(recall) {
}
Bangle.removeListener('touch', this.touch);
if (!this.width) return;
var c = (Date.now()-this.t)/1000;
let settings = Object.assign({flash:true, maxMessages:3, repeat:4, vibrateTimeout:60},require('Storage').readJSON("messages.settings.json", true) || {});
let settings = Object.assign({flash:true, maxMessages:3},require('Storage').readJSON("messages.settings.json", true) || {});
if (recall !== true || settings.flash) {
var msgsShown = E.clip(this.msgs.length, 0, settings.maxMessages);
g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+23);
for(let i = 0;i < msgsShown;i++) {
const msg = this.msgs[i];
const colors = [g.theme.bg, g.setColor(require("messages").getMessageImageCol(msg)).getColor()];
if (settings.flash && (c&1)) {
if (settings.flash && ((Date.now()/1000)&1)) {
if (colors[1] == g.theme.fg) {
colors.reverse();
} else {
@ -35,38 +35,13 @@ WIDGETS["messages"]={area:"tl", width:0, draw:function(recall) {
this.x + 12 + i * 24, this.y + 12, {rotate:0/*force centering*/});
}
}
if (c<settings.vibrateTimeout && // not going on too long...
(settings.repeat || c<1) && // repeated, or no repeat and first attempt
(Date.now()-this.l)>settings.repeat*1000) { // the period between vibrations
this.l = Date.now();
WIDGETS["messages"].buzz(); // buzz every 4 seconds
}
WIDGETS["messages"].i=setTimeout(()=>WIDGETS["messages"].draw(true), 1000);
if (process.env.HWVERSION>1) Bangle.on('touch', this.touch);
},update:function(rawMsgs, quiet) {
},update:function(rawMsgs) {
const settings = Object.assign({maxMessages:3},require('Storage').readJSON("messages.settings.json", true) || {});
this.msgs = filterMessages(rawMsgs);
if (this.msgs.length === 0) {
delete this.t;
delete this.l;
} else {
this.t=Date.now(); // first time
this.l=Date.now()-10000; // last buzz
if (quiet) this.t -= 500000; // if quiet, set last time in the past so there is no buzzing
}
this.width = 24 * E.clip(this.msgs.length, 0, settings.maxMessages);
Bangle.drawWidgets();
},buzz:function(msgSrc) { // return a promise
if ((require('Storage').readJSON('setting.json',1)||{}).quiet) return Promise.resolve(); // never buzz during Quiet Mode
var pattern;
if (msgSrc != undefined && msgSrc.toLowerCase() == "phone") {
// special vibration pattern for incoming calls
pattern = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrateCalls;
} else {
pattern = (require('Storage').readJSON("messages.settings.json", true) || {}).vibrate;
}
if (pattern === undefined) { pattern = ":"; } // pattern may be "", so we can't use || ":" here
return require("buzz").pattern(pattern);
},touch:function(b,c) {
var w=WIDGETS["messages"];
if (!w||!w.width||c.x<w.x||c.x>w.x+w.width||c.y<w.y||c.y>w.y+24) return;
@ -74,8 +49,7 @@ WIDGETS["messages"]={area:"tl", width:0, draw:function(recall) {
}};
/* We might have returned here if we were in the Messages app for a
message but then the watch was never viewed. In that case we don't
want to buzz but should still show that there are unread messages. */
message but then the watch was never viewed. */
if (global.MESSAGES===undefined)
WIDGETS["messages"].update(require("messages").getMessages(), true);
WIDGETS["messages"].update(require("messages").getMessages());
})();

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkA/4AC+c/AoYAR+QTTj4DBkYXS+YUB+cSl4YS+P/mUiL4RLQ+chiUziPyAAIXPmJEC+Ui+UhO6QABj8iVKocEACUTC60j+YXWPoZHTkcyC6sibYaPpC63ziXzkYXUkXyO6nykMykIXUl8xmcykQ2BSZ4XBkTXB+LdBMgPyDRnzj8vmf/AIMyDgMjAoIALiQSCVYI0CD4QAL+MTIILFCI4TJOmMRkUzn40CGQRLBMRipBiIABkR2DTAIAQmURF4KcBZScxn5qBACgWWbwUhMCT7CmU/WQQAR+awBF6jdBVggXSPCwXXVAPyJCkimUieKinBI6sxAQIvUeAMyMQIXT+MjI6iNC+RIT+bAB+cSYiZdBMQMzSCkf+ZIUGAMiYKsjkTbVkMSl4A=="))

104
apps/numberchaser/app.js Normal file
View File

@ -0,0 +1,104 @@
var randomNumber;
var guessNumber = 1;
function mathRandomInt(a, b) {
if (a > b) {
// Swap a and b to ensure a is smaller.
var c = a;
a = b;
b = c;
}
return Math.floor(Math.random() * (b - a + 1) + a);
}
/**
* Describe this function...
*/
function game() {
g.drawString('',0,20,true);
E.showMenu(numMenu);
console.log(randomNumber);
}
var numMenu = {
"" : {
"title" : "Number Chaser",
},
"Guess Number" : {
value : guessNumber,
min:1,max:100,step:1,
onchange : v => { guessNumber=v; }
},
"OK" : function () {
g.clear();
if (guessNumber == randomNumber) {
//if guess is correct
g.setFont("Vector",13);g.setFontAlign(-1,-1);
status = "You won! ";
gameOver();
} else {
//if guess is incorrect
g.setFont("Vector",13);g.setFontAlign(-1,-1);
if (guessNumber > randomNumber) {
//Decreases number if guess is greater
randomNumber = randomNumber - 1;
status = "Too high!";
} else if (guessNumber < randomNumber) {
//Increases number if guess is lower
status = "Too low!";
randomNumber = randomNumber + 1;
}
if (randomNumber < 0 || randomNumber > 100) {
//You lose when the number is out of the 1 to 100 range
g.setFont("Vector",13);g.setFontAlign(-1,-1);
g.drawString('You have lost\nNumber is out\nof range.',10,10,true);
status = "You lost!";
} else {
g.drawString(status+"\nTry again!",10,10);
Bangle.on('tap', function() {
delay(3000).then(() => game());
}
);
}
}
}
};
function gameOver()
{
E.showPrompt(status+'Play again?',{title:""+'Number Chaser'}).then(function(a) {
if (a) {
randomNumber = mathRandomInt(1, 100);
game();
} else {
load();
}
}
);
}
function delay(time) {
return new Promise(resolve => setTimeout(resolve, time));
}
function instructions()
{
g.setFont("Vector",13);g.setFontAlign(-1,-1);
g.drawString('Guess the number\nbetween 1 and 100.\nGuess too high, it\ndecreases by 1.\nToo low, it increases\nby 1.\nIf the number\ngoes below 0 or\nabove 100, it\nis out of range\nand you have\nlost.',10,10,true);
randomNumber = mathRandomInt(1, 100);
delay(10000).then(() => game());
}
g.clear();
E.showPrompt('Do you need instructions?',{title:""+'Number Chaser'}).then(function(a)
{ if (a) {
instructions();
} else
{
randomNumber = mathRandomInt(1, 100);
game();
}
}
);

View File

@ -0,0 +1,13 @@
{ "id": "numberchaser",
"name": "Number Chaser",
"shortName":"Number Chaser",
"version":"0.01",
"description": "A number guessing game, but the number goes up or down based on if you're guessing too high or too low.",
"icon": "numberchaser.png",
"tags": "game,fun",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"numberchaser.app.js","url":"app.js"},
{"name":"numberchaser.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -66,20 +66,20 @@ TODO:
However some don't allow cross-origin use */
//var TILELAYER = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; // simple, high contrast, TOO SLOW
//var TILELAYER = 'http://a.tile.stamen.com/toner/{z}/{x}/{y}.png'; // black and white
var TILELAYER = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
var PREVIEWTILELAYER = 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
var TILELAYER = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
var PREVIEWTILELAYER = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
// Create map and try and set the location to where the browser thinks we are
var map = L.map('map').locate({setView: true, maxZoom: 16, enableHighAccuracy:true});
// Tiles used for Bangle.js itself
var bangleTileLayer = L.tileLayer(TILELAYER, {
maxZoom: 18,
attribution: 'Map data &copy; <a href="http://openstreetmap.org/copyright">OpenStreetMap</a> contributors</a>'
attribution: 'Map data &copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap</a> contributors</a>'
});
// Tiles used for the may the user sees (faster)
var previewTileLayer = L.tileLayer(PREVIEWTILELAYER, {
maxZoom: 18,
attribution: 'Map data &copy; <a href="http://openstreetmap.org/copyright">OpenStreetMap</a> contributors</a>'
attribution: 'Map data &copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap</a> contributors</a>'
});
// Could optionally overlay trails: https://wiki.openstreetmap.org/wiki/Tiles

27
apps/powersave/README.md Normal file
View File

@ -0,0 +1,27 @@
# Power Saver
Save your watch's battery power by halting foreground app execution while the screen is off.
## Features
- Stops foreground app processes
- Background processes still run
- Clears screen
- Decreases accelerometer polls
- Foreground app is returned to when screen is turned back on (app state is not preserved)
## Controls
- Automatically activates when screen times out, timing can be adjusted using normal timeout settings
- Deactivates when screen is turned back on
## Warnings
- Due to an Espruino bug, this does not take affect immediately when installed. Switch apps for these features to take affect.
- This is not compatible with apps that need to run in the foreground even while the screen is off, such as most stopwatch apps and some health trackers.
- If you check your watch super often (like multiple times per minute), this may end of costing you more power than it saves since the app you are using will have to restart everytime you check it.
## Requests
[Contact information is on my website](https://kyleplo.com/#contact)
## Creator
[kyleplo](https://kyleplo.com)

15
apps/powersave/boot.js Normal file
View File

@ -0,0 +1,15 @@
var Storage = Storage || require("Storage");
Bangle.on("lock", locked => {
if(locked){
g.clear().reset();
Bangle.setLCDBrightness(0);
Bangle.setPollInterval(1000);
load("powersave.screen.js");
}else{
load(Storage.read("resumeaftersleep") || JSON.parse(Storage.read("setting.json")).clock);
}
});
E.on("init", () => {
if(__FILE__ && __FILE__ !== "powersave.screen.js")
Storage.write("resumeaftersleep", __FILE__);
});

View File

@ -0,0 +1,15 @@
{
"id": "powersave",
"name": "Power Save",
"version": "0.01",
"description": "Halts foreground app execution while screen is off while still allowing background processes.",
"readme": "README.md",
"icon": "powersave.png",
"type": "bootloader",
"tags": "tool",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"powersave.boot.js","url":"boot.js"},
{"name":"powersave.screen.js","url":"boot.js"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

7
apps/powersave/screen.js Normal file
View File

@ -0,0 +1,7 @@
var Storage = Storage || require("Storage");
g.clear();
Bangle.setLCDBrightness(0);
Bangle.setPollInterval(1000);
if(!Bangle.isLocked()){
load(Storage.read("resumeaftersleep") || JSON.parse(Storage.read("setting.json")).clock);
}

View File

@ -1 +1,2 @@
0.01: First version for upload
0.02: Fix for leftover date artifacts on display.

View File

@ -57,6 +57,10 @@ function draw() {
var minutes = ("0"+m).substr(-2);
g.reset();
// If midnight clear display to remove day / date artifacts
if (h == 0 && m == 0)
g.clear();
// Convert to 12hr time mode
if (is12Hour && h > 12) {
h = h - 12;

View File

@ -4,7 +4,7 @@
"shortName":"Slash",
"icon": "slash.png",
"screenshots": [{"url":"screenshot.png"}],
"version":"0.01",
"version":"0.02",
"description": "Slash Watch based on Pebble watch face by Nikki.",
"tags": "clock",
"type": "clock",

View File

@ -1,2 +1,3 @@
0.01: Release
0.02: Rewrite with new interface
0.02: Rewrite with new interface
0.03: Added clock infos to expose timer functionality to clocks.

97
apps/smpltmr/clkinfo.js Normal file
View File

@ -0,0 +1,97 @@
(function() {
const TIMER_IDX = "smpltmr";
function isAlarmEnabled(){
try{
var alarm = require('sched');
var alarmObj = alarm.getAlarm(TIMER_IDX);
if(alarmObj===undefined || !alarmObj.on){
return false;
}
return true;
} catch(ex){ }
return false;
}
function getAlarmMinutes(){
if(!isAlarmEnabled()){
return -1;
}
var alarm = require('sched');
var alarmObj = alarm.getAlarm(TIMER_IDX);
return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000));
}
function getAlarmMinutesText(){
var min = getAlarmMinutes();
if(min < 0){
return "OFF";
}
return "T-" + String(min);
}
function increaseAlarm(t){
try{
var minutes = isAlarmEnabled() ? getAlarmMinutes() : 0;
var alarm = require('sched')
alarm.setAlarm(TIMER_IDX, {
timer : (minutes+t)*60*1000,
});
alarm.reload();
} catch(ex){ }
}
function decreaseAlarm(t){
try{
var minutes = getAlarmMinutes();
minutes -= t;
var alarm = require('sched')
alarm.setAlarm(TIMER_IDX, undefined);
if(minutes > 0){
alarm.setAlarm(TIMER_IDX, {
timer : minutes*60*1000,
});
}
alarm.reload();
} catch(ex){ }
}
var img = atob("GBiBAeAAB+AAB/v/3/v/3/v/3/v/3/v/n/n/H/z+P/48//85//+b//+b//8p//4E//yCP/kBH/oAn/oAX/oAX/oAX/oAX+AAB+AABw==")
var smpltmrItems = {
name: "Timer",
img: img,
items: [
{
name: "Timer",
get: () => ({ text: getAlarmMinutesText() + (isAlarmEnabled() ? " min" : ""), img: null}),
show: function() { smpltmrItems.items[0].emit("redraw"); },
hide: function () {},
run: function() { }
},
]
};
var offsets = [+1,+5,-1,-5];
offsets.forEach((o, i) => {
smpltmrItems.items = smpltmrItems.items.concat({
name: String(o),
get: () => ({ text: getAlarmMinutesText() + " (" + (o > 0 ? "+" : "") + o + ")", img: null}),
show: function() { smpltmrItems.items[i+1].emit("redraw"); },
hide: function () {},
run: function() {
if(o > 0) increaseAlarm(o);
else decreaseAlarm(Math.abs(o));
this.show();
}
});
});
return smpltmrItems;
})

View File

@ -2,7 +2,7 @@
"id": "smpltmr",
"name": "Simple Timer",
"shortName": "Simple Timer",
"version": "0.02",
"version": "0.03",
"description": "A very simple app to start a timer.",
"icon": "app.png",
"tags": "tool,alarm,timer",
@ -12,6 +12,7 @@
"readme": "README.md",
"storage": [
{"name":"smpltmr.app.js","url":"app.js"},
{"name":"smpltmr.clkinfo.js","url":"clkinfo.js"},
{"name":"smpltmr.img","url":"app-icon.js","evaluate":true}
]
}

1
apps/swscroll/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: Inital release.

9
apps/swscroll/README.md Normal file
View File

@ -0,0 +1,9 @@
This first release seems servicable in testing so far.
To get the standard menu scrolling back, just remove this app from your Bangle.
TODO:
- Maybe have how much of "trailing space" there are after the last entry should be dynamic in size, now it's always 8 pixels which corresponds to if there are a widget field and a menu title present.
- I want to change the size of menu entries to be a little bigger vertically.
<a target="_blank" href="https://icons8.com/icon/18714/drag-list-down">Drag List Down</a> icon by <a target="_blank" href="https://icons8.com">Icons8</a>

BIN
apps/swscroll/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 B

100
apps/swscroll/boot.js Normal file
View File

@ -0,0 +1,100 @@
E.showScroller = (function(options) {
/* options = {
h = height
c = # of items
scroll = initial scroll position
scrollMin = minimum scroll amount (can be negative)
draw = function(idx, rect)
select = function(idx)
}
returns {
draw = draw all
drawItem(idx) = draw specific item
}
*/
if (!options) return Bangle.setUI(); // remove existing handlers
var menuShowing = false;
var R = Bangle.appRect;
var Y = Bangle.appRect.y;
var n = Math.ceil(R.h/options.h);
var menuScrollMin = 0|options.scrollMin;
var menuScrollMax = options.h*options.c - R.h;
if (menuScrollMax<menuScrollMin) menuScrollMax=menuScrollMin;
function idxToY(i) {
return i*options.h + R.y - rScroll;
}
function YtoIdx(y) {
return Math.floor((y + rScroll - R.y)/options.h);
}
var s = {
scroll : E.clip(0|options.scroll,menuScrollMin,menuScrollMax),
draw : () => {
g.reset().clearRect(R.x,R.y,R.x2,R.y2);
g.setClipRect(R.x,R.y,R.x2,R.y2);
var a = YtoIdx(R.y);
var b = Math.min(YtoIdx(R.y2),options.c-1);
for (var i=a;i<=b;i++)
options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h});
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
}, drawItem : i => {
var y = idxToY(i);
g.reset().setClipRect(R.x,y,R.x2,y+options.h);
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
}};
var rScroll = s.scroll&~1; // rendered menu scroll (we only shift by 2 because of dither)
s.draw(); // draw the full scroller
g.flip(); // force an update now to make this snappier
Bangle.setUI({
mode : "custom",
back : options.back,
swipe : (hor,ver)=>{
pixels = 120;
var dy = ver*pixels;
if (s.scroll - dy > menuScrollMax)
dy = s.scroll - menuScrollMax-8; // Makes it so the last 'page' has the same position as previous pages. This should be done dynamically (change the static 8 to be a variable) so the offset is correct even when no widget field or title field is present.
if (s.scroll - dy < menuScrollMin)
dy = s.scroll - menuScrollMin;
s.scroll -= dy;
var oldScroll = rScroll;
rScroll = s.scroll &~1;
dy = oldScroll-rScroll;
if (!dy || options.c<=3) return; //options.c<=3 should maybe be dynamic, so 3 would be replaced by a variable dependent on R=Bangle.appRect. It's here so we don't try to scroll if all entries fit in the app rectangle.
g.reset().setClipRect(R.x,R.y,R.x2,R.y2);
g.scroll(0,dy);
var d = ver*pixels;
if (d < 0) {
g.setClipRect(R.x,R.y2-(1-d),R.x2,R.y2);
let i = YtoIdx(R.y2-(1-d));
let y = idxToY(i);
//print(i, options.c, options.c-i); //debugging info
while (y < R.y2 - (options.h*((options.c-i)<=0)) ) { //- (options.h*((options.c-i)<=0)) makes sure we don't go beyond the menu entries in the menu object "options". This has to do with "dy = s.scroll - menuScrollMax-8" above.
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
i++;
y += options.h;
}
} else { // d>0
g.setClipRect(R.x,R.y,R.x2,R.y+d);
let i = YtoIdx(R.y+d);
let y = idxToY(i);
//print(i, options.c, options.c-i); //debugging info
while (y > R.y-options.h) {
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
y -= options.h;
i--;
}
}
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
}, touch : (_,e)=>{
if (e.y<R.y-4) return;
var i = YtoIdx(e.y);
if ((menuScrollMin<0 || i>=0) && i<options.c)
options.select(i);
}
});
return s;
});

View File

@ -0,0 +1,14 @@
{
"id": "swscroll",
"name": "Swipe menus",
"version": "0.01",
"description": "Replace built in E.showScroller to act on swipe instead of drag. Navigate menus in discrete steps instead of a continuous motion.",
"readme": "README.md",
"icon": "app.png",
"type": "bootloader",
"tags": "system",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"swscroll.boot.js","url":"boot.js"}
]
}

4
apps/twenties/ChangeLog Normal file
View File

@ -0,0 +1,4 @@
0.01: New Widget!
0.02: Fix calling null on draw
0.03: Only vibrate during work
0.04: Convert to boot code

17
apps/twenties/README.md Normal file
View File

@ -0,0 +1,17 @@
# Twenties
Follow the [20-20-20 rule](https://www.aoa.org/AOA/Images/Patients/Eye%20Conditions/20-20-20-rule.pdf) with discrete reminders. Your Bangle will buzz every 20 minutes for you to look away from your screen, and then buzz 20 seconds later to look back. Additionally, alternate between standing and sitting every 20 minutes to be standing for [more than 30 minutes](https://uwaterloo.ca/kinesiology-health-sciences/how-long-should-you-stand-rather-sit-your-work-station) per hour.
## Usage
Download this app and it will automatically run in the background.
## Features
Vibrates to remind you to stand up and look away for healthy living.
Only vibrates during work days and hours.
## Creator
[@splch](https://github.com/splch/)

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

19
apps/twenties/boot.js Normal file
View File

@ -0,0 +1,19 @@
(() => {
const move = 20 * 60 * 1000; // 20 minutes
const look = 20 * 1000; // 20 seconds
const buzz = _ => {
const date = new Date();
const day = date.getDay();
const hour = date.getHours();
// buzz at work
if (day >= 1 && day <= 5 &&
hour >= 8 && hour <= 17) {
Bangle.buzz().then(_ => {
setTimeout(Bangle.buzz, look);
});
}
};
setInterval(buzz, move); // buzz to stand / sit
})();

View File

@ -1,13 +1,13 @@
{
"id": "widtwenties",
"id": "twenties",
"name": "Twenties",
"shortName": "twenties",
"version": "0.02",
"version": "0.04",
"description": "Buzzes every 20m to stand / sit and look 20ft away for 20s.",
"icon": "widget.png",
"type": "widget",
"tags": "widget,tools",
"icon": "app.png",
"type": "bootloader",
"tags": "alarm,tool",
"supports": ["BANGLEJS", "BANGLEJS2"],
"readme": "README.md",
"storage": [{ "name": "widtwenties.wid.js", "url": "widget.js" }]
"storage": [{ "name": "twenties.boot.js", "url": "boot.js" }]
}

View File

@ -13,3 +13,4 @@
0.14: Use weather condition code for icon selection
0.15: Fix widget icon
0.16: Don't mark app as clock
0.17: Added clkinfo for clocks.

43
apps/weather/clkinfo.js Normal file
View File

@ -0,0 +1,43 @@
(function() {
var weather = {
temp: "?",
hum: "?",
wind: "?",
};
var weatherJson = storage.readJSON('weather.json');
if(weatherJson !== undefined && weatherJson.weather !== undefined){
weather = weatherJson.weather;
weather.temp = locale.temp(weather.temp-273.15);
weather.hum = weather.hum + "%";
weather.wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/);
weather.wind = Math.round(weather.wind[1]) + "kph";
}
var weatherItems = {
name: "Weather",
img: atob("GBiBAf+///u5//n7//8f/9wHP8gDf/gB//AB/7AH/5AcP/AQH/DwD/uAD84AD/4AA/wAAfAAAfAAAfAAAfgAA/////+bP/+zf/+zfw=="),
items: [
{
name: "temperature",
get: () => ({ text: weather.temp, img: atob("GBiBAf/D//+B//8Y//88//88//88//88//88//8k//8k//8k//8k//8k//8k//4kf/5mf/zDP/yBP/yBP/zDP/5mf/48f/8A///D/w==")}),
show: function() { weatherItems.items[0].emit("redraw"); },
hide: function () {}
},
{
name: "humidity",
get: () => ({ text: weather.hum, img: atob("GBiBAf/7///z///x///g///g///Af//Af/3Af/nA//jg//B/v/B/H+A/H8A+D8AeB8AcB4AYA8AYA8AYA+A4A/B4A//4A//8B///Dw==")}),
show: function() { weatherItems.items[1].emit("redraw"); },
hide: function () {}
},
{
name: "wind",
get: () => ({ text: weather.wind, img: atob("GBiBAf4f//wP//nn//Pn//Pzg//nAf/meIAOfAAefP///P//+fAAAfAAB////////wAAP4AAH///z///z//nz//nz//zj//wH//8Pw==")}),
show: function() { weatherItems.items[2].emit("redraw"); },
hide: function () {}
},
]
};
return weatherItems;
})

View File

@ -1,7 +1,7 @@
{
"id": "weather",
"name": "Weather",
"version": "0.16",
"version": "0.17",
"description": "Show Gadgetbridge weather report",
"icon": "icon.png",
"screenshots": [{"url":"screenshot.png"}],
@ -13,7 +13,8 @@
{"name":"weather.wid.js","url":"widget.js"},
{"name":"weather","url":"lib.js"},
{"name":"weather.img","url":"icon.js","evaluate":true},
{"name":"weather.settings.js","url":"settings.js"}
{"name":"weather.settings.js","url":"settings.js"},
{"name":"weather.clkinfo.js","url":"clkinfo.js"}
],
"data": [{"name":"weather.json"}]
}

View File

@ -1,10 +1,10 @@
{ "id": "widagps",
"name": "AGPS Widget",
"name": "AGPS Widget (automatic download)",
"shortName":"AGPS Widget",
"icon": "widget.png",
"type": "widget",
"version":"0.01",
"description": "Load AGPS data in the background **using Gadgetbridge**",
"description": "Once installed, this widget allows your Bangle.js 2 to load AGPS data in the background **via Gadgetbridge on an Android phone** so it is always up to date. If you just want to upload the latest AGPS data from this app loader, please use the `Assisted GPS Update (AGPS)` app.",
"readme": "README.md",
"tags": "widget,agps,http",
"supports": ["BANGLEJS2"],

View File

@ -11,4 +11,5 @@
0.12: Prevent repeated execution of `draw()` from the current app.
0.13: Added "connection restored" notification. Fixed restoring of the watchface.
0.14: Added configuration option
0.15: Added option to hide widget when connected
0.15: Added option to hide widget when connected
0.16: Simplify code, add option to disable displaying a message

View File

@ -1,8 +1,8 @@
{
"id": "widbt_notify",
"name": "Bluetooth Widget with Notification",
"version": "0.15",
"description": "Show the current Bluetooth connection status in the top right of the watch. Optional buzz and/or and hide if disconnected",
"version": "0.16",
"description": "Show the current Bluetooth connection status with some optional features: show message, buzz on connect/loss, hide always/if connected.",
"icon": "widget.png",
"type": "widget",
"tags": "widget,bluetooth",

View File

@ -1,69 +1,57 @@
(function(back) {
var FILE = "widbt_notify.json";
var filename = "widbt_notify.json";
// set Storage and load settings
var storage = require("Storage");
var settings = Object.assign({
secondsOnUnlock: false,
}, require('Storage').readJSON(FILE, true) || {});
showWidget: true,
buzzOnConnect: true,
buzzOnLoss: true,
hideConnected: true,
showMessage: true,
nextBuzz: 30000
}, storage.readJSON(filename, true) || {});
function writeSettings() {
require('Storage').writeJSON(FILE, settings);
}
// Helper method which uses int-based menu item for set of string values
function stringItems(startvalue, writer, values) {
// setup boolean menu entries
function boolEntry(key) {
return {
value: (startvalue === undefined ? 0 : values.indexOf(startvalue)),
format: v => values[v],
min: 0,
max: values.length - 1,
wrap: true,
step: 1,
value: settings[key],
onchange: v => {
writer(values[v]);
writeSettings();
// change the value of key
settings[key] = v;
// write to storage
storage.writeJSON(filename, settings);
}
};
}
// Helper method which breaks string set settings down to local settings object
function stringInSettings(name, values) {
return stringItems(settings[name], v => settings[name] = v, values);
}
var mainmenu = {
// setup menu
var menu = {
"": {
"title": "Bluetooth Widget WN"
},
"< Back": () => back(),
"Show Widget": {
value: (settings.showWidget !== undefined ? settings.showWidget : true),
"Show Widget": boolEntry("showWidget"),
"Buzz on connect": boolEntry("buzzOnConnect"),
"Buzz on loss": boolEntry("buzzOnLoss"),
"Hide connected": boolEntry("hideConnected"),
"Show Message": boolEntry("showMessage"),
"Next Buzz": {
value: settings.nextBuzz,
step: 1000,
min: 1000,
max: 120000,
wrap: true,
format: v => (v / 1000) + "s",
onchange: v => {
settings.showWidget = v;
writeSettings();
settings.nextBuzz = v;
storage.writeJSON(filename, settings);
}
},
"Buzz on Connect": {
value: (settings.buzzOnConnect !== undefined ? settings.buzzOnConnect : true),
onchange: v => {
settings.buzzOnConnect = v;
writeSettings();
}
},
"Buzz on loss": {
value: (settings.buzzOnLoss !== undefined ? settings.buzzOnLoss : true),
onchange: v => {
settings.buzzOnLoss = v;
writeSettings();
}
},
"Hide connected": {
value: (settings.hideConnected !== undefined ? settings.hideConnected : false),
onchange: v => {
settings.hideConnected = v;
writeSettings();
}
}
}
};
E.showMenu(mainmenu);
// draw main menu
E.showMenu(menu);
});
})

View File

@ -1,110 +1,90 @@
WIDGETS.bluetooth_notify = {
(function() {
// load settings
var settings = Object.assign({
showWidget: true,
buzzOnConnect: true,
buzzOnLoss: true,
hideConnected: true,
showMessage: true,
nextBuzz: 30000
}, require("Storage").readJSON("widbt_notify.json", true) || {});
// setup widget with to hide if connected and option set
var widWidth = settings.hideConnected && NRF.getSecurityStatus().connected ? 0 : 15;
// write widget with loaded settings
WIDGETS.bluetooth_notify = Object.assign(settings, {
// set area and width
area: "tr",
width: 15,
width: widWidth,
// setup warning status
warningEnabled: 1,
// ------------ Settings -------- very lame - need to improve
readshowWidget: function() {
var showWidget;
const SETTINGSFILE = "widbt_notify.json";
function def (value, def) {return value !== undefined ? value : def;}
var settings = require('Storage').readJSON(SETTINGSFILE, true) || {};
showWidget = def(settings.showWidget, true);
return showWidget;
},
readBuzzOnConnect: function() {
var buzzOnConnect;
const SETTINGSFILE = "widbt_notify.json";
function def (value, def) {return value !== undefined ? value : def;}
var settings = require('Storage').readJSON(SETTINGSFILE, true) || {};
buzzOnConnect = def(settings.buzzOnConnect, true);
return buzzOnConnect;
},
readBuzzOnLoss: function() {
var buzzOnLoss;
const SETTINGSFILE = "widbt_notify.json";
function def (value, def) {return value !== undefined ? value : def;}
var settings = require('Storage').readJSON(SETTINGSFILE, true) || {};
buzzOnLoss = def(settings.buzzOnLoss, true);
return buzzOnLoss;
},
readHideConnected: function() {
var hideConnected;
const SETTINGSFILE = "widbt_notify.json";
function def (value, def) {return value !== undefined ? value : def;}
var settings = require('Storage').readJSON(SETTINGSFILE, true) || {};
hideConnected = def(settings.hideConnected, true);
return hideConnected;
},
// ------------ Settings --------
draw: function() {
if (WIDGETS.bluetooth_notify.readshowWidget()){
g.reset();
if (NRF.getSecurityStatus().connected) {
if (!WIDGETS.bluetooth_notify.readHideConnected()) {
g.setColor((g.getBPP() > 8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f"));
g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), 2 + this.x, 2 + this.y);
}
} else {
// g.setColor(g.theme.dark ? "#666" : "#999");
g.setColor("#f00"); // red is easier to distinguish from blue
g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), 2 + this.x, 2 + this.y);
}
if (this.showWidget) {
g.reset();
if (NRF.getSecurityStatus().connected) {
if (!this.hideConnected) {
g.setColor((g.getBPP() > 8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f"));
g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), 2 + this.x, 2 + this.y);
}
} else {
// g.setColor(g.theme.dark ? "#666" : "#999");
g.setColor("#f00"); // red is easier to distinguish from blue
g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), 2 + this.x, 2 + this.y);
}
}
},
redrawCurrentApp: function(){
if(typeof(draw)=='function'){
g.clear();
draw();
Bangle.loadWidgets();
Bangle.drawWidgets();
}else{
load(); // fallback. This might reset some variables
redrawCurrentApp: function() {
if (typeof(draw) == 'function') {
g.clear();
draw();
Bangle.loadWidgets();
Bangle.drawWidgets();
} else {
load(); // fallback. This might reset some variables
}
},
onNRF: function(connect) {
// setup widget with and reload widgets to show/hide if hideConnected is enabled
if (this.hideConnected) {
this.width = connect ? 0 : 15; // ensures correct redraw
Bangle.drawWidgets();
} else {
// redraw widget
this.draw();
}
if (this.warningEnabled) {
if (this.showMessage) {
E.showMessage( /*LANG*/ 'Connection\n' + (connect ? /*LANG*/ 'restored.' : /*LANG*/ 'lost.'), 'Bluetooth');
setTimeout(() => {
WIDGETS.bluetooth_notify.redrawCurrentApp();
}, 3000); // clear message - this will reload the widget, resetting 'warningEnabled'.
}
},
connect: function() {
if(WIDGETS.bluetooth_notify.warningEnabled == 1){
E.showMessage(/*LANG*/'Connection\nrestored.', 'Bluetooth');
setTimeout(()=>{WIDGETS.bluetooth_notify.redrawCurrentApp();}, 3000); // clear message - this will reload the widget, resetting 'warningEnabled'.
WIDGETS.bluetooth_notify.warningEnabled = 0;
setTimeout('WIDGETS.bluetooth_notify.warningEnabled = 1;', 30000); // don't buzz for the next 30 seconds.
var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet;
if(!quiet && WIDGETS.bluetooth_notify.readBuzzOnConnect()){
Bangle.buzz(700, 1); // buzz on connection resume
}
}
WIDGETS.bluetooth_notify.draw();
this.warningEnabled = 0;
setTimeout('WIDGETS.bluetooth_notify.warningEnabled = 1;', this.nextBuzz); // don't buzz for the next X seconds.
},
disconnect: function() {
if(WIDGETS.bluetooth_notify.warningEnabled == 1){
E.showMessage(/*LANG*/ 'Connection\nlost.', 'Bluetooth');
setTimeout(()=>{WIDGETS.bluetooth_notify.redrawCurrentApp();}, 3000); // clear message - this will reload the widget, resetting 'warningEnabled'.
WIDGETS.bluetooth_notify.warningEnabled = 0;
setTimeout('WIDGETS.bluetooth_notify.warningEnabled = 1;', 30000); // don't buzz for the next 30 seconds.
var quiet = (require('Storage').readJSON('setting.json',1)||{}).quiet;
if(!quiet && WIDGETS.bluetooth_notify.readBuzzOnLoss()){
Bangle.buzz(700, 1); // buzz on connection loss
}
}
WIDGETS.bluetooth_notify.draw();
var quiet = (require('Storage').readJSON('setting.json', 1) || {}).quiet;
if (!quiet && (connect ? this.buzzOnConnect : this.buzzOnLoss)) {
Bangle.buzz(700, 1); // buzz on connection resume or loss
}
}
}
};
NRF.on('connect', WIDGETS.bluetooth_notify.connect);
NRF.on('disconnect', WIDGETS.bluetooth_notify.disconnect);
});
// clear variables
settings = undefined;
widWidth = undefined;
// setup bluetooth connection events
NRF.on('connect', (addr) => WIDGETS.bluetooth_notify.onNRF(addr));
NRF.on('disconnect', () => WIDGETS.bluetooth_notify.onNRF());
})()

View File

@ -1,2 +0,0 @@
0.01: New Widget!
0.02: Fix calling null on draw

View File

@ -1,15 +0,0 @@
# Twenties
Follow the [20-20-20 rule](https://www.aoa.org/AOA/Images/Patients/Eye%20Conditions/20-20-20-rule.pdf) with discrete reminders. Your BangleJS will buzz every 20 minutes for you to look away from your screen, and then buzz 20 seconds later to look back. Additionally, alternate between standing and sitting every 20 minutes to be standing for [more than 30 minutes](https://uwaterloo.ca/kinesiology-health-sciences/how-long-should-you-stand-rather-sit-your-work-station) per hour.
## Usage
Download this widget and, as long as your watch-face supports widgets, it will automatically run in the background.
## Features
Vibrate to remind you to stand up and look away for healthy living.
## Creator
[@splch](https://github.com/splch/)

View File

@ -1,24 +0,0 @@
// WIDGETS = {}; // <-- for development only
/* run widgets in their own function scope so
they don't interfere with currently-running apps */
(() => {
const move = 20 * 60 * 1000; // 20 minutes
const look = 20 * 1000; // 20 seconds
buzz = _ => {
Bangle.buzz().then(_ => {
setTimeout(Bangle.buzz, look);
});
};
// add widget
WIDGETS.twenties = {
buzz: buzz,
draw: _ => { return null; },
};
setInterval(WIDGETS.twenties.buzz, move); // buzz to stand / sit
})();
// Bangle.drawWidgets(); // <-- for development only

View File

@ -2,9 +2,14 @@ var exports = {};
/* Module that allows for loading of clock 'info' displays
that can be scrolled through on the clock face.
`load()` returns an array of menu items:
`load()` returns an array of menu objects, where each object contains a list of menu items:
* 'name' : text to display and identify menu object (e.g. weather)
* 'img' : a 24x24px image
* 'items' : menu items such as temperature, humidity, wind etc.
* 'item.name' : friendly name
Note that each item is an object with:
* 'item.name' : friendly name to identify an item (e.g. temperature)
* 'item.get' : function that resolves with:
{
'text' : the text to display for this item
@ -13,21 +18,25 @@ that can be scrolled through on the clock face.
* 'item.show' : called when item should be shown. Enables updates. Call BEFORE 'get'
* 'item.hide' : called when item should be hidden. Disables updates.
* .on('redraw', ...) : event that is called when 'get' should be called again (only after 'item.show')
* 'item.run' : (optional) called if the info screen is tapped - can perform some action
* 'item.run' : (optional) called if the info screen is tapped - can perform some action. Return true if the caller should feedback the user.
See the bottom of this file for example usage...
example.clkinfo.js :
(function() {
return [
{ name : "Example",
get : () => ({ text : "Bangle.js",
img : atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==") }),
show : () => {},
hide : () => {}
}
];
return {
name: "Bangle",
img: atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==") }),
items: [
{ name : "Item1",
get : () => ({ text : "TextOfItem1",
img : atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==") }),
show : () => {},
hide : () => {}
}
]
};
}) // must not have a semi-colon!
*/
@ -38,65 +47,72 @@ exports.load = function() {
var hrm = "--";
var alt = "--";
// callbacks (needed for easy removal of listeners)
function batteryUpdateHandler() { items[0].emit("redraw"); }
function stepUpdateHandler() { items[1].emit("redraw"); }
function hrmUpdateHandler() { items[2].emit("redraw"); }
function batteryUpdateHandler() { bangleItems[0].emit("redraw"); }
function stepUpdateHandler() { bangleItems[1].emit("redraw"); }
function hrmUpdateHandler() { bangleItems[2].emit("redraw"); }
function altUpdateHandler() {
Bangle.getPressure().then(data=>{
if (!data) return;
alt = Math.round(data.altitude) + "m";
items[3].emit("redraw");
bangleItems[3].emit("redraw");
});
}
// actual items
var items = [
// actual menu
var menu = [{
name: "Bangle",
img: atob("GBiBAf8B//4B//4B//4B//4A//x4//n+f/P/P+fPn+fPn+fP3+/Px+/Px+fn3+fzn+f/n/P/P/n+f/x4//4A//4B//4B//4B//8B/w=="),
items: [
{ name : "Battery",
get : () => ({
text : E.getBattery() + "%",
img : atob(Bangle.isCharging() ? "GBiBAAABgAADwAAHwAAPgACfAAHOAAPkBgHwDwP4Hwf8Pg/+fB//OD//kD//wD//4D//8D//4B//QB/+AD/8AH/4APnwAHAAACAAAA==" : "GBiBAAAAAAAAAAAAAAAAAAAAAD//+P///IAAAr//Ar//Ar//A7//A7//A7//A7//Ar//AoAAAv///D//+AAAAAAAAAAAAAAAAAAAAA==") }),
show : function() {
this.interval = setInterval(()=>this.emit('redraw'), 60000);
Bangle.on("charging", batteryUpdateHandler);
},
hide : function() {
clearInterval(this.interval);
delete this.interval;
Bangle.removeListener("charging", batteryUpdateHandler);
},
show : function() { this.interval = setInterval(()=>this.emit('redraw'), 60000); Bangle.on("charging", batteryUpdateHandler); batteryUpdateHandler(); },
hide : function() { clearInterval(this.interval); delete this.interval; Bangle.removeListener("charging", batteryUpdateHandler); },
},
{ name : "Steps", get : () => ({
text : Bangle.getHealthStatus("day").steps,
img : atob("GBiBAAcAAA+AAA/AAA/AAB/AAB/gAA/g4A/h8A/j8A/D8A/D+AfH+AAH8AHn8APj8APj8AHj4AHg4AADAAAHwAAHwAAHgAAHgAADAA==") }),
show : function() { Bangle.on("step", stepUpdateHandler); },
show : function() { Bangle.on("step", stepUpdateHandler); stepUpdateHandler(); },
hide : function() { Bangle.removeListener("step", stepUpdateHandler); },
},
{ name : "HRM", get : () => ({
text : Math.round(Bangle.getHealthStatus("last").bpm) + " bpm",
img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAADAAADAAAHAAAHjAAHjgAPngH9n/n82/gA+AAA8AAA8AAAcAAAYAAAYAAAAAAAAAAAAAAAAAAAAAA==") }),
show : function() { Bangle.setHRMPower(1,"clkinfo"); Bangle.on("HRM", hrmUpdateHandler); hrm = Math.round(Bangle.getHealthStatus("last").bpm); },
show : function() { Bangle.setHRMPower(1,"clkinfo"); Bangle.on("HRM", hrmUpdateHandler); hrm = Math.round(Bangle.getHealthStatus("last").bpm); hrmUpdateHandler(); },
hide : function() { Bangle.setHRMPower(0,"clkinfo"); Bangle.removeListener("HRM", hrmUpdateHandler); hrm = "--"; },
}
];
if (Bangle.getPressure) // Altimeter may not exist
items.push({ name : "Altitude", get : () => ({
],
}];
var bangleItems = menu[0].items;
if (Bangle.getPressure){ // Altimeter may not exist
bangleItems.push({ name : "Altitude", get : () => ({
text : alt,
img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAAACAAAGAAAPAAEZgAOwwAPwQAZgYAwAMBgAGBAACDAADGAABv///////wAAAAAAAAAAAAAAAAAAAA==") }),
show : function() { this.interval = setInterval(altUpdateHandler, 60000); alt = "--"; altUpdateHandler(); },
hide : function() { clearInterval(this.interval); delete this.interval; },
hide : function() { clearInterval(this.interval); delete this.interval; },
});
// now load extra data from a third party files
}
// In case there exists already a menu object b with the same name as the next
// object a, we append the items. Otherwise we add the new object a to the list.
require("Storage").list(/clkinfo.js$/).forEach(fn => {
items = items.concat(eval(require("Storage").read(fn))());
var a = eval(require("Storage").read(fn))();
var b = menu.find(x => x.name === a.name)
if(b) b.items = b.items.concat(a.items);
else menu = menu.concat(a);
});
// return it all!
return items;
return menu;
};
// Code for testing
/*
g.clear();
var items = exports.load(); // or require("clock_info").load()
var menu = exports.load(); // or require("clock_info").load()
var itemsFirstMenu = menu[0].items;
items.forEach((itm,i) => {
var y = i*24;
console.log("Starting", itm.name);