Merge branch 'master' of github.com:nxdefiant/BangleApps

master
Erik Andresen 2022-05-04 22:37:51 +02:00
commit 54f2017c23
69 changed files with 829 additions and 359 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ tests/Layout/bin/tmp.*
tests/Layout/testresult.bmp tests/Layout/testresult.bmp
apps.local.json apps.local.json
_site _site
.jekyll-cache

View File

@ -226,10 +226,8 @@ and which gives information about the app for the Launcher.
"name":"Short Name", // for Bangle.js menu "name":"Short Name", // for Bangle.js menu
"icon":"*myappid", // for Bangle.js menu "icon":"*myappid", // for Bangle.js menu
"src":"-myappid", // source file "src":"-myappid", // source file
"type":"widget/clock/app/bootloader", // optional, default "app" "type":"widget/clock/app/bootloader/...", // optional, default "app"
// if this is 'widget' then it's not displayed in the menu // see 'type' in 'metadata.json format' below for more options/info
// if it's 'clock' then it'll be loaded by default at boot time
// if this is 'bootloader' then it's code that is run at boot time, but is not in a menu
"version":"1.23", "version":"1.23",
// added by BangleApps loader on upload based on metadata.json // added by BangleApps loader on upload based on metadata.json
"files:"file1,file2,file3", "files:"file1,file2,file3",
@ -252,17 +250,23 @@ and which gives information about the app for the Launcher.
"version": "0v01", // the version of this app "version": "0v01", // the version of this app
"description": "...", // long description (can contain markdown) "description": "...", // long description (can contain markdown)
"icon": "icon.png", // icon in apps/ "icon": "icon.png", // icon in apps/
"screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app "screenshots" : [ { "url":"screenshot.png" } ], // optional screenshot for app
"type":"...", // optional(if app) - "type":"...", // optional(if app) -
// 'app' - an application // 'app' - an application
// 'clock' - a clock - required for clocks to automatically start // 'clock' - a clock - required for clocks to automatically start
// 'widget' - a widget // 'widget' - a widget
// 'launch' - replacement launcher app // 'bootloader' - an app that at startup (app.boot.js) but doesn't have a launcher entry for 'app.js'
// 'bootloader' - code that runs at startup only
// 'RAM' - code that runs and doesn't upload anything to storage // 'RAM' - code that runs and doesn't upload anything to storage
// 'launch' - replacement 'Launcher'
// 'textinput' - provides a 'textinput' library that allows text to be input on the Bangle
// 'scheduler' - provides 'sched' library and boot code for scheduling alarms/timers
// (currently only 'sched' app)
// 'notify' - provides 'notify' library for showing notifications
// 'locale' - provides 'locale' library for language-specific date/distance/etc
// (a version of 'locale' is included in the firmware)
"tags": "", // comma separated tag list for searching "tags": "", // comma separated tag list for searching
"supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2 "supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2
"dependencies" : { "notify":"type" } // optional, app 'types' we depend on "dependencies" : { "notify":"type" } // optional, app 'types' we depend on (see "type" above)
"dependencies" : { "messages":"app" } // optional, depend on a specific app ID "dependencies" : { "messages":"app" } // optional, depend on a specific app ID
// for instance this will use notify/notifyfs is they exist, or will pull in 'notify' // for instance this will use notify/notifyfs is they exist, or will pull in 'notify'
"readme": "README.md", // if supplied, a link to a markdown-style text file "readme": "README.md", // if supplied, a link to a markdown-style text file

View File

@ -1,2 +1,4 @@
0.01: New App! 0.01: New App!
0.02: Fix the settings bug and some tweaking 0.02: Fix the settings bug and some tweaking
0.03: Do not alarm while charging
0.04: Obey system quiet mode

View File

@ -1,13 +1,14 @@
# Activity reminder # Activity reminder
A reminder to take short walks for the ones with a sedentary lifestyle. A reminder to take short walks for the ones with a sedentary lifestyle.
The alert will popup only if you didn't take your short walk yet The alert will popup only if you didn't take your short walk yet.
Different settings can be personalized: Different settings can be personalized:
- Enable : Enable/Disable the app - Enable : Enable/Disable the app
- Start hour: Hour to start the reminder - Start hour: Hour to start the reminder
- End hour: Hour to end the reminder - End hour: Hour to end the reminder
- Max inactivity: Maximum inactivity time to allow before the alert. From 15 to 60 min - Max inactivity: Maximum inactivity time to allow before the alert. From 15 to 60 min
- Dismiss delay: Delay added before the next alert if the alert is dismissed. From 5 to 15 min - Dismiss delay: Delay added before the next alert if the alert is dismissed. From 5 to 60 min
Notice: If Dissmiss delay > Max inactivity then it will be equal Max inactivity
- Min steps: Minimal amount of steps to count as an activity - Min steps: Minimal amount of steps to count as an activity

View File

@ -14,7 +14,10 @@ function drawAlert(){
load(); load();
}); });
// Obey system quiet mode:
if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) {
Bangle.buzz(400); Bangle.buzz(400);
}
setTimeout(load, 20000); setTimeout(load, 20000);
} }

View File

@ -1,4 +1,5 @@
function run(){ function run(){
if (Bangle.isCharging()) return;
var now = new Date(); var now = new Date();
var h = now.getHours(); var h = now.getHours();
if(h >= activityreminder.startHour && h < activityreminder.endHour){ if(h >= activityreminder.startHour && h < activityreminder.endHour){

View File

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

View File

@ -43,7 +43,7 @@
}, },
'Dismiss delay': { 'Dismiss delay': {
value: settings.dismissDelayMin, value: settings.dismissDelayMin,
min: 5, max: 15, min: 5, max: 60,
onchange: v => { onchange: v => {
settings.dismissDelayMin = v; settings.dismissDelayMin = v;
require("activityreminder").writeSettings(settings); require("activityreminder").writeSettings(settings);

View File

@ -25,3 +25,4 @@
0.24: Automatically save the alarm/timer when the user returns to the main menu using the back arrow 0.24: Automatically save the alarm/timer when the user returns to the main menu using the back arrow
Add "Enable All", "Disable All" and "Remove All" actions Add "Enable All", "Disable All" and "Remove All" actions
0.25: Fix redrawing selected Alarm/Timer entry inside edit submenu 0.25: Fix redrawing selected Alarm/Timer entry inside edit submenu
0.26: Add support for Monday as first day of the week (#1780)

View File

@ -2,10 +2,14 @@ Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
// An array of alarm objects (see sched/README.md) // An array of alarm objects (see sched/README.md)
let alarms = require("sched").getAlarms(); var alarms = require("sched").getAlarms();
// 0 = Sunday
// 1 = Monday
var firstDayOfWeek = (require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0;
function getCurrentTime() { function getCurrentTime() {
let time = new Date(); var time = new Date();
return ( return (
time.getHours() * 3600000 + time.getHours() * 3600000 +
time.getMinutes() * 60000 + time.getMinutes() * 60000 +
@ -14,6 +18,9 @@ function getCurrentTime() {
} }
function saveAndReload() { function saveAndReload() {
// Before saving revert the dow to the standard format
alarms.forEach(a => a.dow = handleFirstDayOfWeek(a.dow, firstDayOfWeek));
require("sched").setAlarms(alarms); require("sched").setAlarms(alarms);
require("sched").reload(); require("sched").reload();
} }
@ -28,6 +35,8 @@ function showMainMenu() {
/*LANG*/'New Timer': () => editTimer(-1) /*LANG*/'New Timer': () => editTimer(-1)
}; };
alarms.forEach((alarm, idx) => { alarms.forEach((alarm, idx) => {
alarm.dow = handleFirstDayOfWeek(alarm.dow, firstDayOfWeek);
var type, txt; // a leading space is currently required (JS error in Espruino 2v12) var type, txt; // a leading space is currently required (JS error in Espruino 2v12)
if (alarm.timer) { if (alarm.timer) {
type = /*LANG*/"Timer"; type = /*LANG*/"Timer";
@ -71,23 +80,26 @@ function editDOW(dow, onchange) {
'': { 'title': /*LANG*/'Days of Week' }, '': { 'title': /*LANG*/'Days of Week' },
/*LANG*/'< Back': () => onchange(dow) /*LANG*/'< Back': () => onchange(dow)
}; };
for (let i = 0; i < 7; i++) (i => {
let dayOfWeek = require("locale").dow({ getDay: () => i }); require("date_utils").dows(firstDayOfWeek).forEach((day, i) => {
menu[dayOfWeek] = { menu[day] = {
value: !!(dow&(1<<i)), value: !!(dow & (1 << (i + firstDayOfWeek))),
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: v => v ? dow |= 1<<i : dow &= ~(1<<i), onchange: v => v ? (dow |= 1 << (i + firstDayOfWeek)) : (dow &= ~(1 << (i + firstDayOfWeek)))
}; };
})(i); });
E.showMenu(menu); E.showMenu(menu);
} }
function editAlarm(alarmIndex, alarm) { function editAlarm(alarmIndex, alarm) {
let newAlarm = alarmIndex < 0; var newAlarm = alarmIndex < 0;
let a = require("sched").newDefaultAlarm(); var a = require("sched").newDefaultAlarm();
a.dow = handleFirstDayOfWeek(a.dow, firstDayOfWeek);
if (!newAlarm) Object.assign(a, alarms[alarmIndex]); if (!newAlarm) Object.assign(a, alarms[alarmIndex]);
if (alarm) Object.assign(a, alarm); if (alarm) Object.assign(a, alarm);
let t = require("sched").decodeTime(a.t); var t = require("sched").decodeTime(a.t);
const menu = { const menu = {
'': { 'title': /*LANG*/'Alarm' }, '': { 'title': /*LANG*/'Alarm' },
@ -114,7 +126,7 @@ function editAlarm(alarmIndex, alarm) {
onchange: v => a.rp = v onchange: v => a.rp = v
}, },
/*LANG*/'Days': { /*LANG*/'Days': {
value: "SMTWTFS".split("").map((d,n)=>a.dow&(1<<n)?d:".").join(""), value: decodeDOW(a.dow),
onchange: () => setTimeout(editDOW, 100, a.dow, d => { onchange: () => setTimeout(editDOW, 100, a.dow, d => {
a.dow = d; a.dow = d;
a.t = require("sched").encodeTime(t); a.t = require("sched").encodeTime(t);
@ -156,11 +168,11 @@ function saveAlarm(newAlarm, alarmIndex, a, t) {
} }
function editTimer(alarmIndex, alarm) { function editTimer(alarmIndex, alarm) {
let newAlarm = alarmIndex < 0; var newAlarm = alarmIndex < 0;
let a = require("sched").newDefaultTimer(); var a = require("sched").newDefaultTimer();
if (!newAlarm) Object.assign(a, alarms[alarmIndex]); if (!newAlarm) Object.assign(a, alarms[alarmIndex]);
if (alarm) Object.assign(a, alarm); if (alarm) Object.assign(a, alarm);
let t = require("sched").decodeTime(a.timer); var t = require("sched").decodeTime(a.timer);
const menu = { const menu = {
'': { 'title': /*LANG*/'Timer' }, '': { 'title': /*LANG*/'Timer' },
@ -210,6 +222,26 @@ function saveTimer(newAlarm, alarmIndex, a, t) {
saveAndReload(); saveAndReload();
} }
function handleFirstDayOfWeek(dow, firstDayOfWeek) {
if (firstDayOfWeek == 1) {
if ((dow & 1) == 1) {
// By default 1 = Sunday.
// Here the week starts on Monday and Sunday is ON so move Sunday to 128.
dow += 127;
} else if ((dow & 128) == 128) {
dow -= 127;
}
}
return dow;
}
function decodeDOW(dow) {
return require("date_utils")
.dows(firstDayOfWeek, 2)
.map((day, index) => dow & (1 << (index + firstDayOfWeek)) ? day : "_")
.join("");
}
function enableAll(on) { function enableAll(on) {
E.showPrompt(/*LANG*/"Are you sure?", { E.showPrompt(/*LANG*/"Are you sure?", {
title: on ? /*LANG*/"Enable All" : /*LANG*/"Disable All" title: on ? /*LANG*/"Enable All" : /*LANG*/"Disable All"

View File

@ -2,7 +2,7 @@
"id": "alarm", "id": "alarm",
"name": "Alarms & Timers", "name": "Alarms & Timers",
"shortName": "Alarms", "shortName": "Alarms",
"version": "0.25", "version": "0.26",
"description": "Set alarms and timers on your Bangle", "description": "Set alarms and timers on your Bangle",
"icon": "app.png", "icon": "app.png",
"tags": "tool,alarm,widget", "tags": "tool,alarm,widget",

View File

@ -3,7 +3,7 @@
"name": "HRM Accelerometer event recorder", "name": "HRM Accelerometer event recorder",
"shortName": "HRM ACC recorder", "shortName": "HRM ACC recorder",
"version": "0.01", "version": "0.01",
"type": "ram", "type": "RAM",
"description": "Record HRM and accelerometer events in high resolution to CSV files in your browser", "description": "Record HRM and accelerometer events in high resolution to CSV files in your browser",
"icon": "app.png", "icon": "app.png",
"tags": "debug", "tags": "debug",

View File

@ -115,7 +115,23 @@ E.on('notify',msg=>{
// could also use NRF.ancsGetAppInfo(msg.appId) here // could also use NRF.ancsGetAppInfo(msg.appId) here
}; };
var unicodeRemap = { var unicodeRemap = {
'2019':"'" '2019':"'",
'260':"A",
'261':"a",
'262':"C",
'263':"c",
'280':"E",
'281':"e",
'321':"L",
'322':"l",
'323':"N",
'324':"n",
'346':"S",
'347':"s",
'377':"Z",
'378':"z",
'379':"Z",
'380':"z",
}; };
var replacer = ""; //(n)=>print('Unknown unicode '+n.toString(16)); var replacer = ""; //(n)=>print('Unknown unicode '+n.toString(16));
//if (appNames[msg.appId]) msg.a //if (appNames[msg.appId]) msg.a

View File

@ -1 +1,2 @@
0.01: New App! 0.01: New App!
0.02: Introduced settings to customize the layout and functionality of the keyboard.

View File

@ -2,6 +2,17 @@
A library that provides an on-screen keyboard for text input. A library that provides an on-screen keyboard for text input.
## Settings
Text size - small or big text font. Default=Big. Suggested=Small.
Offset keyboard - display the keyboard on top, making it faster to see what character you have selected. Default=No. Suggested=Yes.
Loop around - should the keyboard highlight loop around when going past the edges? Default=Yes. Suggested=No.
One-to-one input and release to select - should the input correspond directly to discrete areas on the screen, instead of being handled by scaled relative changes in position on swipes? Default=No. Suggested=Yes.
Speed scaling - how much should a swipe move the highligt on the keyboard? Higher number corresponds to slower movement. Not applicable if using one-to-one input. Default=24. Suggested=15.
## Usage ## Usage
In your app's metadata, add: In your app's metadata, add:

View File

@ -69,13 +69,24 @@ var KEYEXTRA = [
String.fromCharCode(27,91,53,126), // 0x84 page up String.fromCharCode(27,91,53,126), // 0x84 page up
String.fromCharCode(27,91,54,126), // 0x85 page down String.fromCharCode(27,91,54,126), // 0x85 page down
]; ];
var settings = Object.assign({
// default values
textSize: 1,
offsetKeyboard: 0,
loopAround: 1,
oneToOne: 0,
speedScaling: 24
}, require('Storage').readJSON("kbtouch.settings.json", true) || {});
// state // state
const R = Bangle.appRect; const R = Bangle.appRect;
var kbx = 0, kby = 0, kbdx = 0, kbdy = 0, kbShift = false, flashToggle = false; var kbx = 0, kby = 0, kbdx = 0, kbdy = 0, kbShift = false, flashToggle = false;
const PX=12, PY=16, DRAGSCALE=24; const PX=12, PY=16, DRAGSCALE=settings.speedScaling;
var xoff = 3, yoff = g.getHeight()-PY*4; var xoff = 3, yoff = g.getHeight()-PY*(4+5*settings.offsetKeyboard);
function draw() { function draw() {
"ram";
var map = kbShift ? KEYMAPUPPER : KEYMAPLOWER; var map = kbShift ? KEYMAPUPPER : KEYMAPLOWER;
//g.drawImage(KEYIMG,0,yoff); //g.drawImage(KEYIMG,0,yoff);
g.reset().setFont("6x8:2"); g.reset().setFont("6x8:2");
@ -88,9 +99,9 @@ function draw() {
g.drawString(map[1],xoff,yoff+PY); g.drawString(map[1],xoff,yoff+PY);
g.drawString(map[2],xoff,yoff+PY*2); g.drawString(map[2],xoff,yoff+PY*2);
g.drawString(map[3],xoff,yoff+PY*3); g.drawString(map[3],xoff,yoff+PY*3);
var l = g.setFont("6x8:4").wrapString(text+(flashToggle?"_":" "), R.w-8); var l = g.setFont(settings.textSize ? "6x8:4":"6x8:2").wrapString(text+(flashToggle?"_":" "), R.w-8);
if (l.length>2) l=l.slice(-2); if (l.length>2+2*settings.textSize) l=l.slice(-(2+2*settings.textSize));
g.drawString(l.join("\n"),R.x+4,R.y+4); g.drawString(l.join("\n"),R.x+4,R.y+4 +82*settings.offsetKeyboard);
g.flip(); g.flip();
} }
@ -104,17 +115,31 @@ function draw() {
return new Promise((resolve,reject) => { return new Promise((resolve,reject) => {
Bangle.setUI({mode:"custom", drag:e=>{ Bangle.setUI({mode:"custom", drag:e=>{
if (settings.oneToOne) {
kbx = Math.max(Math.min(Math.floor((e.x-16) / (6*2)) , 13) , 0);
kby = Math.max(Math.min(Math.floor((e.y-120) / (8*2)) , 3) , 0);
//print(e.y, kby, e.x, kbx);
}
if (!settings.oneToOne) {
kbdx += e.dx; kbdx += e.dx;
kbdy += e.dy; kbdy += e.dy;
var dx = Math.round(kbdx/DRAGSCALE), dy = Math.round(kbdy/DRAGSCALE); var dx = Math.round(kbdx/DRAGSCALE), dy = Math.round(kbdy/DRAGSCALE);
kbdx -= dx*DRAGSCALE; kbdx -= dx*DRAGSCALE;
kbdy -= dy*DRAGSCALE; kbdy -= dy*DRAGSCALE;
if (dx || dy) { if (dx || dy) {
if (settings.loopAround) {
kbx = (kbx+dx+15)%15; kbx = (kbx+dx+15)%15;
kby = (kby+dy+4)%4; kby = (kby+dy+4)%4;
draw(); } else {
kbx = Math.max(Math.min((kbx+dx),13),0);
kby = Math.max(Math.min((kby+dy),3),0);
} }
},touch:()=>{ }
}
draw();
if (!e.b && e.y>Bangle.appRect.y && settings.oneToOne /*&& settings.releaseToSelect*/) {
var map = kbShift ? KEYMAPUPPER : KEYMAPLOWER; var map = kbShift ? KEYMAPUPPER : KEYMAPLOWER;
var ch = map[kby][kbx]; var ch = map[kby][kbx];
if (ch=="\2") kbShift=!kbShift; if (ch=="\2") kbShift=!kbShift;
@ -122,6 +147,17 @@ function draw() {
else text += ch; else text += ch;
Bangle.buzz(20); Bangle.buzz(20);
draw(); draw();
}
},touch:()=>{
if ( !settings.oneToOne /*|| !settings.releaseToSelect*/) {
var map = kbShift ? KEYMAPUPPER : KEYMAPLOWER;
var ch = map[kby][kbx];
if (ch=="\2") kbShift=!kbShift;
else if (ch=="\b") text = text.slice(0,-1);
else text += ch;
Bangle.buzz(20);
draw();
}
},back:()=>{ },back:()=>{
clearInterval(flashInterval); clearInterval(flashInterval);
Bangle.setUI(); Bangle.setUI();

View File

@ -1,6 +1,6 @@
{ "id": "kbtouch", { "id": "kbtouch",
"name": "Touch keyboard", "name": "Touch keyboard",
"version":"0.01", "version":"0.02",
"description": "A library for text input via onscreen keyboard", "description": "A library for text input via onscreen keyboard",
"icon": "app.png", "icon": "app.png",
"type":"textinput", "type":"textinput",
@ -9,6 +9,7 @@
"screenshots": [{"url":"screenshot.png"}], "screenshots": [{"url":"screenshot.png"}],
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{"name":"textinput","url":"lib.js"} {"name":"textinput","url":"lib.js"},
{"name":"kbtouch.settings.js","url":"settings.js"}
] ]
} }

59
apps/kbtouch/settings.js Normal file
View File

@ -0,0 +1,59 @@
(function(back) {
function settings() {
let settings = require('Storage').readJSON("kbtouch.settings.json", true) || {};
if (settings.textSize===undefined) settings.textSize=1;
if (settings.offsetKeyboard===undefined) settings.offsetKeyboard=0;
if (settings.loopAround===undefined) settings.loopAround=1;
if (settings.oneToOne===undefined) settings.oneToOne=0;
if (settings.speedScaling===undefined) settings.speedScaling=24;
return settings;
}
function updateSetting(setting, value) {
let settings = require('Storage').readJSON("kbtouch.settings.json", true) || {};
settings[setting] = value;
require('Storage').writeJSON("kbtouch.settings.json", settings);
}
var mainmenu = {
"" : { "title" : /*LANG*/"Touch Keyboard" },
"< Back" : back,
/*LANG*/'Text size': {
value: settings().textSize,
min: 0, max: 1,
format: v => [/*LANG*/"Small",/*LANG*/"Big"][v],
onchange: v => updateSetting("textSize", v)
},
/*LANG*/'Offset keyboard': {
value: settings().offsetKeyboard,
min: 0, max: 1,
format: v => [/*LANG*/"No",/*LANG*/"Yes"][v],
onchange: v => updateSetting("offsetKeyboard", v)
},
/*LANG*/'Loop around': {
value: settings().loopAround,
min: 0, max: 1,
format: v => [/*LANG*/"No",/*LANG*/"Yes"][v],
onchange: v => updateSetting("loopAround", v)
},
/*LANG*/'One-to-one input and release to select': {
value: settings().oneToOne,
min: 0, max: 1,
format: v => [/*LANG*/"No",/*LANG*/"Yes"][v],
onchange: v => updateSetting("oneToOne", v)
},
/*LANG*/'Speed scaling': {
value: settings().speedScaling,
min: 1, max: 24, step : 1,
format: v => v,
onchange: v => updateSetting("speedScaling", v)
}
///*LANG*/'Release to select': {
// value: 1|settings().fontSize,
// min: 0, max: 1,
// format: v => [/*LANG*/"No",/*LANG*/"Yes"][v],
// onchange: v => updateSetting("releaseToSelect", v)
//}
};
E.showMenu(mainmenu);
})

View File

@ -4,7 +4,7 @@
"version": "0.02", "version": "0.02",
"description": "Replace Bangle.js 2's menus with a version that contains smaller text", "description": "Replace Bangle.js 2's menus with a version that contains smaller text",
"icon": "app.png", "icon": "app.png",
"type": "boot", "type": "bootloader",
"tags": "system", "tags": "system",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"storage": [ "storage": [

View File

@ -9,7 +9,7 @@
{"url":"screenshot_b1_dark.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_light.png"}, {"url":"screenshot_b1_dark.png"},{"url":"screenshot_b1_edit.png"},{"url":"screenshot_b1_light.png"},
{"url":"screenshot_b2_dark.png"},{"url":"screenshot_b2_edit.png"},{"url":"screenshot_b2_light.png"} {"url":"screenshot_b2_dark.png"},{"url":"screenshot_b2_edit.png"},{"url":"screenshot_b2_light.png"}
], ],
"type": "boot", "type": "bootloader",
"tags": "system", "tags": "system",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"storage": [ "storage": [

View File

@ -5,7 +5,7 @@
"icon": "mmind.png", "icon": "mmind.png",
"version":"0.01", "version":"0.01",
"description": "This is the classic game for masterminds", "description": "This is the classic game for masterminds",
"type": "game", "type": "app",
"tags": "mastermind, game, classic", "tags": "mastermind, game, classic",
"readme":"README.md", "readme":"README.md",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],

View File

@ -4,7 +4,7 @@
"version": "0.01", "version": "0.01",
"description": "A retro-inspired dockface that displays the current time and battery charge while plugged in, and which features an interactive mode that shows the time, date, and a rotating data display line.", "description": "A retro-inspired dockface that displays the current time and battery charge while plugged in, and which features an interactive mode that shows the time, date, and a rotating data display line.",
"icon": "mystic-dock.png", "icon": "mystic-dock.png",
"type": "dock", "type": "app",
"tags": "dock", "tags": "dock",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS"],
"readme": "README.md", "readme": "README.md",

View File

@ -1 +1,2 @@
0.01: New App! 0.01: New App!
0.02: Fix true wind computation, add swipe gesture to pause GPS

View File

@ -14,7 +14,9 @@ additionally displayed in red. In this mode, the speed over ground in knots is
## Controls ## Controls
There are no controls in the main app, but there are two settings in the settings app that can be changed: In the main app, when true wind mode is enabled (see below), swiping left on the screen will temporarily disable GPS (to preserve battery); a small
red satellite symbol will appear on the bottom right. Swiping right will turn GPS back on.
The settings app provides the following two settings:
* True wind: enables or disables true wind calculations; enabling this will turn on GPS inside the app * True wind: enables or disables true wind calculations; enabling this will turn on GPS inside the app
* Mounting angle: mounting relative to the boat of the wind instrument (in degrees) * Mounting angle: mounting relative to the boat of the wind instrument (in degrees)

View File

@ -1,16 +1,20 @@
OW_CHAR_UUID = '0000cc91-0000-1000-8000-00805f9b34fb'; OW_CHAR_UUID = '0000cc91-0000-1000-8000-00805f9b34fb';
require("Font7x11Numeric7Seg").add(Graphics); require("Font7x11Numeric7Seg").add(Graphics);
gatt = {}; var gatt = {};
cx = g.getWidth()/2; var cx = g.getWidth()/2;
cy = 24+(g.getHeight()-24)/2; var cy = 24+(g.getHeight()-24)/2;
w = (g.getWidth()-24)/2; var w = (g.getWidth()-24)/2;
var y1 = 24;
gps_course = { spd: 0 }; var y2 = g.getHeight()-1;
var gps_course = { spd: 0 };
var course_marker_len = g.getWidth()/4;
var settings = require("Storage").readJSON('openwindsettings.json', 1) || {}; var settings = require("Storage").readJSON('openwindsettings.json', 1) || {};
i = 0; var pause_gps = false;
hullpoly = [];
var i = 0;
var hullpoly = [];
for (y=-1; y<=1; y+=0.1) { for (y=-1; y<=1; y+=0.1) {
hullpoly[i++] = cx - (y<0 ? 1+y*0.15 : (Math.sqrt(1-0.7*y*y)-Math.sqrt(0.3))/(1-Math.sqrt(0.3)))*w*0.3; hullpoly[i++] = cx - (y<0 ? 1+y*0.15 : (Math.sqrt(1-0.7*y*y)-Math.sqrt(0.3))/(1-Math.sqrt(0.3)))*w*0.3;
hullpoly[i++] = cy - y*w*0.7; hullpoly[i++] = cy - y*w*0.7;
@ -22,17 +26,18 @@ for (y=1; y>=-1; y-=0.1) {
function wind_updated(ev) { function wind_updated(ev) {
if (ev.target.uuid == "0xcc91") { if (ev.target.uuid == "0xcc91") {
awa = settings.mount_angle-ev.target.value.getInt16(1, true)*0.1; awa = settings.mount_angle+ev.target.value.getInt16(1, true)*0.1;
if (awa<0) awa += 360;
aws = ev.target.value.getInt16(3, true)*0.01; aws = ev.target.value.getInt16(3, true)*0.01;
//console.log(awa, aws); //console.log(awa, aws);
if (gps_course.spd > 0) { if (gps_course.spd > 0) {
wv = { // wind vector (in fixed reference frame) wv = { // wind vector (in "earth" reference frame)
lon: Math.sin(Math.PI*(gps_course.course+awa)/180)*aws, vlon: Math.sin(Math.PI*(gps_course.course+(awa+180))/180)*aws,
lat: Math.cos(Math.PI*(gps_course.course+awa)/180)*aws vlat: Math.cos(Math.PI*(gps_course.course+(awa+180))/180)*aws
}; };
twv = { lon: wv.lon+gps_course.lon, lat: wv.lat+gps_course.lat }; twv = { vlon: wv.vlon+gps_course.vlon, vlat: wv.vlat+gps_course.vlat };
tws = Math.sqrt(Math.pow(twv.lon,2)+Math.pow(twv.lat, 2)); tws = Math.sqrt(Math.pow(twv.vlon,2)+Math.pow(twv.vlat, 2));
twa = Math.atan2(twv.lat, twv.lon)*180/Math.PI-gps_course.course; twa = 180+Math.atan2(twv.vlon, twv.vlat)*180/Math.PI-gps_course.course;
if (twa<0) twa += 360; if (twa<0) twa += 360;
if (twa>360) twa -=360; if (twa>360) twa -=360;
} }
@ -57,16 +62,19 @@ function draw_compass(awa, aws, twa, tws) {
a = i*Math.PI/2+Math.PI/4; a = i*Math.PI/2+Math.PI/4;
g.drawLineAA(cx+Math.cos(a)*w*0.85, cy+Math.sin(a)*w*0.85, cx+Math.cos(a)*w*0.99, cy+Math.sin(a)*w*0.99); g.drawLineAA(cx+Math.cos(a)*w*0.85, cy+Math.sin(a)*w*0.85, cx+Math.cos(a)*w*0.99, cy+Math.sin(a)*w*0.99);
} }
g.setColor(0, 1, 0).fillCircle(cx+Math.sin(Math.PI*awa/180)*w*0.9, cy+Math.cos(Math.PI*awa/180)*w*0.9, w*0.1); g.setColor(0, 1, 0).fillCircle(cx+Math.sin(Math.PI*awa/180)*w*0.9, cy-Math.cos(Math.PI*awa/180)*w*0.9, w*0.1);
if (tws>0) g.setColor(1, 0, 0).fillCircle(cx+Math.sin(Math.PI*twa/180)*w*0.9, cy+Math.cos(Math.PI*twa/180)*w*0.9, w*0.1); if (tws>0) g.setColor(1, 0, 0).fillCircle(cx+Math.sin(Math.PI*twa/180)*w*0.9, cy+Math.cos(Math.PI*twa/180)*w*0.9, w*0.1);
g.setColor(0, 1, 0).setFont("7x11Numeric7Seg",w*0.06); g.setColor(0, 1, 0).setFont("7x11Numeric7Seg",w*0.06);
g.setFontAlign(0, 0, 0).drawString(aws.toFixed(1), cx, cy-0.32*w); g.setFontAlign(0, 0, 0).drawString(aws.toFixed(1), cx, cy-0.32*w);
if (!pause_gps) {
if (tws>0) g.setColor(1, 0, 0).drawString(tws.toFixed(1), cx, cy+0.32*w); if (tws>0) g.setColor(1, 0, 0).drawString(tws.toFixed(1), cx, cy+0.32*w);
if (settings.truewind && typeof gps_course.spd!=='undefined') { if (settings.truewind && gps_course.spd!=-1) {
spd = gps_course.spd/1.852; spd = gps_course.spd/1.852;
g.setColor(g.theme.fg).setFont("7x11Numeric7Seg", w*0.03).setFontAlign(-1, 1, 0).drawString(spd.toFixed(1), 1, g.getHeight()-1); g.setColor(g.theme.fg).setFont("7x11Numeric7Seg", w*0.03).setFontAlign(-1, 1, 0).drawString(spd.toFixed(1), 1, g.getHeight()-1);
} }
} }
if (pause_gps) g.setColor("#f00").drawImage(atob("DAwBEAKARAKQE4DwHkPqPRGKAEAA"),g.getWidth()-15, g.getHeight()-15);
}
function parseDevice(d) { function parseDevice(d) {
device = d; device = d;
@ -96,8 +104,10 @@ if (settings.truewind) {
Bangle.on('GPS',function(fix) { Bangle.on('GPS',function(fix) {
if (fix.fix && fix.satellites>3 && fix.speed>2) { // only uses fixes w/ more than 3 sats and speed > 2kph if (fix.fix && fix.satellites>3 && fix.speed>2) { // only uses fixes w/ more than 3 sats and speed > 2kph
gps_course = gps_course =
{ lon: Math.sin(Math.PI*fix.course/180)*fix.speed/1.852, { vlon: Math.sin(Math.PI*fix.course/180)*fix.speed/1.852,
lat: Math.cos(Math.PI*fix.course/180)*fix.speed/1.852, vlat: Math.cos(Math.PI*fix.course/180)*fix.speed/1.852,
lat: fix.lat,
lon: fix.lon,
spd: fix.speed, spd: fix.speed,
course: fix.course course: fix.course
}; };
@ -107,6 +117,20 @@ if (settings.truewind) {
Bangle.setGPSPower(1, "app"); Bangle.setGPSPower(1, "app");
} }
if (settings.truewind) {
Bangle.on("swipe", (d)=>{
if (d==-1 && !pause_gps) {
pause_gps = true;
Bangle.setGPSPower(0);
draw_compass(0, 0, 0, 0);
}
else if (d==1 && pause_gps) {
pause_gps = false;
Bangle.setGPSPower(1, "app");
draw_compass(0, 0, 0, 0);
}
});
}
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
draw_compass(0, 0, 0, 0); draw_compass(0, 0, 0, 0);

View File

@ -1,7 +1,7 @@
{ "id": "openwind", { "id": "openwind",
"name": "OpenWind", "name": "OpenWind",
"shortName":"OpenWind", "shortName":"OpenWind",
"version":"0.01", "version":"0.02",
"description": "OpenWind", "description": "OpenWind",
"icon": "openwind.png", "icon": "openwind.png",
"readme": "README.md", "readme": "README.md",

View File

@ -4,7 +4,7 @@
"version": "0.02", "version": "0.02",
"description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.", "description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.",
"icon": "icon.png", "icon": "icon.png",
"type": "boot", "type": "bootloader",
"tags": "system", "tags": "system",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md", "readme": "README.md",

View File

@ -1 +1,2 @@
0.01: Initial version 0.01: Initial version
0.02: Moved settings from launcher to settings->apps menu

View File

@ -1 +0,0 @@
require("heatshrink").decompress(atob("kMigILIgPAAYMD/ADBwcGhkAwM5wcA/+2//Av/Rn/giFoyFggkUrFggEKlAkCiApCx+AAYNGoADBkU4AYMQj4DBvEICANkAoIPBgE2B4MAiMAH4MAwECAYNALYUgBIISCHYMYAoQWBAIMEgAYBAIMBwEDDQNgDwUf/4eBg4DCAA4"))

View File

@ -1,14 +1,15 @@
{ "id": "quicklaunch", {
"id": "quicklaunch",
"name": "Quick Launch", "name": "Quick Launch",
"icon": "app.png", "icon": "app.png",
"version":"0.01", "version":"0.02",
"description": "Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice.", "description": "Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice. Configurations can be accessed through Settings->Apps.",
"type": "bootloader",
"tags": "tools, system", "tags": "tools, system",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"storage": [ "storage": [
{"name":"quicklaunch.app.js","url":"app.js"}, {"name":"quicklaunch.settings.js","url":"settings.js"},
{"name":"quicklaunch.boot.js","url":"boot.js"}, {"name":"quicklaunch.boot.js","url":"boot.js"}
{"name":"quicklaunch.img","url":"app-icon.js","evaluate":true}
], ],
"data": [{"name":"quicklaunch.json"}] "data": [{"name":"quicklaunch.json"}]
} }

View File

@ -1,3 +1,4 @@
(function(back) {
var settings = Object.assign(require("Storage").readJSON("quicklaunch.json", true) || {}); var settings = Object.assign(require("Storage").readJSON("quicklaunch.json", true) || {});
var apps = require("Storage").list(/\.info$/).map(app=>{var a=require("Storage").readJSON(app,1);return a&&{name:a.name,type:a.type,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="launch" || app.type=="clock" || !app.type)); var apps = require("Storage").list(/\.info$/).map(app=>{var a=require("Storage").readJSON(app,1);return a&&{name:a.name,type:a.type,sortorder:a.sortorder,src:a.src};}).filter(app=>app && (app.type=="app" || app.type=="launch" || app.type=="clock" || !app.type));
@ -118,3 +119,4 @@ apps.forEach((a)=>{
}); });
showMainMenu(); showMainMenu();
});

1
apps/scicalc/ChangeLog Normal file
View File

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

24
apps/scicalc/README.md Normal file
View File

@ -0,0 +1,24 @@
# SciCalc
Simple scientific calculator. I needed one, so I wrote a basic one, no design frills. Input expressions are slightly post processed and then evaluated
by the JS interpreter.
## Usage
Buttons are arranged on 3 separate screens, swiping left or right switches between them. Swiping down has the same effect as hitting the "=" button.
## Features
The calculator supports the following operations:
* basic arithmetic: +, -, *, /, ^ (raise to a power), +/- (invert sign), 1/x (inverse), use of parentheses
* trigonometric fucntions: sin, cos, tan, asin, acos, atan
* exponential exp, natural logarithm log, pow function (this one takes 2 comma separated arguments)
* Pi is provided as a constant
* a memory button "M" stores or recalls the last result (after hitting the "=" button or swiping down)
![](scicalc_screenshot1.png)
![](scicalc_screenshot2.png)
![](scicalc_screenshot3.png)

1
apps/scicalc/app-icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AJioAaF1wwSFzowRCQUZo4AWjIvVFy4ABF/4vXyGQAYov/R+sZFy8ZF6oAcF/4vvi4AeF/4SCjseAAMdAx8MAAYvVEAQABAx4v/R/TvvF96PUg8cAAMHd9QuCAAIv/R+rvvF96Pvd94vvR97vvF96Pvd94vvR97vsGDwuQGDouSAH4A/AGwA=="))

113
apps/scicalc/app.js Normal file
View File

@ -0,0 +1,113 @@
const W = g.getWidth();
const H = g.getHeight();
const dispH = H/5;
const butH = H-dispH;
const buttons = [[['7', '8', '9'],
['4', '5', '6'],
['1', '2', '3'],
['E', '0', '.']],
[['<', 'M', 'C'],
['+', '-', '*'],
['/', '(', ')'],
['^', ',', '=']],
[['Sin', 'Cos', 'Tan'],
['Asi', 'Aco', 'Ata'],
['Pi', '1/x', '+/-'],
['Log', 'Exp', 'Pow']
]];
var curPage = 0;
var inputStr = '';
var memory = '';
var qResult = false;
function drawPage (p) {
g.clearRect(0, dispH, W-1, H-1);
g.setFont('Vector', butH/5).setFontAlign(0, 0, 0).setColor(g.theme.fg);
for (x=0; x<3; ++x)
for (y=0; y<4; ++y)
g.drawString(buttons[p][y][x], (x+0.5)*W/3, dispH+(y+0.7)*butH/4);
g.setColor(0.5, 0.5, 0.5);
for (x=1; x<3; ++x) g.drawLine(x*W/3, dispH+0.2*butH/4-2, x*W/3, H-1);
for (y=1; y<4; ++y) g.drawLine(0, dispH+(y+0.2)*butH/4, W-1, dispH+(y+0.2)*butH/4);
g.setColor(g.theme.fg).drawLine(0, dispH+0.2*butH/4-2, W-1, dispH+0.2*butH/4-2);
}
function updateDisp(s, len) {
var fh = butH/5;
if (s.toString().length>len) s = s.toString().substr(0,len);
g.setFont("Vector", butH/5).setColor(g.theme.fg).setFontAlign(1, 0, 0);
while (g.stringWidth(s) > W-1) {
fh /= 1.05;
g.setFont("Vector", fh);
}
g.clearRect(0, 0, W-1, dispH-1).drawString(s, W-2, dispH/2);
g.setColor(g.theme.fg).drawLine(0, dispH+0.2*butH/4-2, W-1, dispH+0.2*butH/4-2);
}
function processInp (s) {
var idx = s.indexOf("^");
if (idx > 0) s = "Math.pow(" + s.slice(0,idx) + "," + s.slice(idx+1, s.length) + ")";
['Sin', 'Cos', 'Tan', 'Asin', 'Acos', 'Atan', 'Log', 'Exp', 'Pow'].forEach((x) => {
var i = s.indexOf(x);
while (i>-1) {
s = s.slice(0,i)+"Math."+s.slice(i,i+1).toLowerCase()+s.slice(i+1, s.length);
i = s.indexOf(x, i+6);
}
});
idx = s.indexOf('Pi');
if (idx>-1) s = s.slice(0,idx) + "Math.PI" + s.slice(idx+2, s.length);
idx = 0;
s.split('').forEach((x)=>{ if (x=='(') idx++; if (x==')') idx-- });
s += ')'.repeat(idx);
return s;
}
function compute() {
var res;
console.log(processInp(inputStr));
try { res = eval(processInp(inputStr)); }
catch(e) { res = "error"; }
inputStr = res;
qResult = true;
updateDisp(inputStr, 19);
}
function touchHandler(e, d) {
var x = Math.floor(d.x/(W/3));
var y = Math.floor((d.y-dispH-0.2*butH/4)/(butH/4));
var c = buttons[curPage][y][x];
if (c=="=") { // do the computation
compute();
return;
}
else if (c=="<" && inputStr.length>0) inputStr = inputStr.slice(0, -1); // delete last character
else if (c=='M' && qResult) memory = inputStr;
else if (c=='M') inputStr += memory;
else if (c=="C") inputStr = ''; // clear
else {
if ("Sin Cos Tan Log Exp Pow".indexOf(c)>-1 && c!='E') c += "(";
if ("Asi Aco Ata".indexOf(c)>-1) c += "n(";
if (c=='1/x') { inputStr = "1/("+inputStr+")"; compute(); return; }
if (c=='+/-') { inputStr = "-("+inputStr+")"; compute(); return; }
if (qResult && "+-*/^".indexOf(c)==-1) inputStr = c + inputStr + ")";
else inputStr += c;
}
qResult = false;
updateDisp(inputStr, 32);
}
function swipeHandler(e,d) {
curPage -= e;
if (curPage>buttons.length-1) curPage = 0;
if (curPage<0) curPage = buttons.length-1;
drawPage(curPage);
if (d==1) compute();
}
Bangle.on("touch", touchHandler);
Bangle.on("swipe", swipeHandler);
g.clear();
drawPage(curPage);

View File

@ -0,0 +1,16 @@
{ "id": "scicalc",
"name": "Scientific Calculator",
"shortName":"SciCalc",
"version":"0.01",
"description": "Scientific calculator",
"icon": "scicalc.png",
"screenshots" : [ { "url":"scicalc_screenshot1.png" }, { "url":"scicalc_screenshot2.png" }, { "url":"scicalc_screenshot3.png" } ],
"readme": "README.md",
"tags": "app,tool",
"allow_emulator": true,
"supports" : ["BANGLEJS2"],
"storage": [
{"name":"scicalc.app.js","url":"app.js"},
{"name":"scicalc.img","url":"app-icon.js","evaluate":true}
]
}

BIN
apps/scicalc/scicalc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -46,3 +46,5 @@
0.41: Stop users disabling all wake-up methods and locking themselves out (fix #1272) 0.41: Stop users disabling all wake-up methods and locking themselves out (fix #1272)
0.42: Fix theme customizer on new Bangle 2 firmware 0.42: Fix theme customizer on new Bangle 2 firmware
0.43: Add some Bangle 1 colours to theme customizer 0.43: Add some Bangle 1 colours to theme customizer
0.44: Add "Start Week On X" option (#1780)
UI improvements to Locale and Date & Time menu

View File

@ -7,9 +7,9 @@ This is Bangle.js's settings menu
* **Beep** most Bangle.js do not have a speaker inside, but they can use the vibration motor to beep in different pitches. You can change the behaviour here to use a Piezo speaker if one is connected * **Beep** most Bangle.js do not have a speaker inside, but they can use the vibration motor to beep in different pitches. You can change the behaviour here to use a Piezo speaker if one is connected
* **Vibration** enable/disable the vibration motor * **Vibration** enable/disable the vibration motor
* **Quiet Mode** prevent notifications/alarms from vibrating/beeping/turning the screen on - see below * **Quiet Mode** prevent notifications/alarms from vibrating/beeping/turning the screen on - see below
* **Locale** set time zone/whether the clock is 12/24 hour (for supported clocks) * **Locale** set time zone, the time format (12/24h, for supported clocks) and the first day of the week
* **Select Clock** if you have more than one clock face, select the default one * **Select Clock** if you have more than one clock face, select the default one
* **Set Time** Configure the current time - Note that this can be done much more easily by choosing 'Set Time' from the App Loader * **Date & Time** Configure the current time - Note that this can be done much more easily by choosing 'Set Time' from the App Loader
* **LCD** Configure settings about the screen. How long it stays on, how bright it is, and when it turns on - see below. * **LCD** Configure settings about the screen. How long it stays on, how bright it is, and when it turns on - see below.
* **Theme** Adjust the colour scheme * **Theme** Adjust the colour scheme
* **Utils** Utilities - including resetting settings (see below) * **Utils** Utilities - including resetting settings (see below)
@ -35,7 +35,11 @@ This is Bangle.js's settings menu
`Wake on Touch` actually uses the accelerometer, and you need to actually tap the display to wake Bangle.js. `Wake on Touch` actually uses the accelerometer, and you need to actually tap the display to wake Bangle.js.
* **Twist X** these options adjust the sensitivity of `Wake on Twist` to ensure Bangle.js wakes up with just the right amount of wrist movement. * **Twist X** these options adjust the sensitivity of `Wake on Twist` to ensure Bangle.js wakes up with just the right amount of wrist movement.
## Locale
* **Time Zone** your current Time zone. This is usually set automatically by the App Loader
* **Time Format** whether you want a 24 or 12 hour clock. However not all clocks will honour this.
* **Start Week On** start the displayed week on Sunday, or Monday. This currently only applies to the Alarm app.
## Quiet Mode ## Quiet Mode

View File

@ -1,7 +1,7 @@
{ {
"id": "setting", "id": "setting",
"name": "Settings", "name": "Settings",
"version": "0.43", "version": "0.44",
"description": "A menu for setting up Bangle.js", "description": "A menu for setting up Bangle.js",
"icon": "settings.png", "icon": "settings.png",
"tags": "tool,system", "tags": "tool,system",

View File

@ -48,6 +48,7 @@ function resetSettings() {
HID: false, // BLE HID mode, off by default HID: false, // BLE HID mode, off by default
clock: null, // a string for the default clock's name clock: null, // a string for the default clock's name
"12hour" : false, // 12 or 24 hour clock? "12hour" : false, // 12 or 24 hour clock?
firstDayOfWeek: 0, // 0 -> Sunday (default), 1 -> Monday
brightness: 1, // LCD brightness from 0 to 1 brightness: 1, // LCD brightness from 0 to 1
// welcomed : undefined/true (whether welcome app should show) // welcomed : undefined/true (whether welcome app should show)
options: { options: {
@ -94,7 +95,7 @@ function showSystemMenu() {
/*LANG*/'LCD': ()=>showLCDMenu(), /*LANG*/'LCD': ()=>showLCDMenu(),
/*LANG*/'Locale': ()=>showLocaleMenu(), /*LANG*/'Locale': ()=>showLocaleMenu(),
/*LANG*/'Select Clock': ()=>showClockMenu(), /*LANG*/'Select Clock': ()=>showClockMenu(),
/*LANG*/'Set Time': ()=>showSetTimeMenu() /*LANG*/'Date & Time': ()=>showSetTimeMenu()
}; };
return E.showMenu(mainmenu); return E.showMenu(mainmenu);
@ -478,6 +479,7 @@ function showLocaleMenu() {
'< Back': ()=>showSystemMenu(), '< Back': ()=>showSystemMenu(),
/*LANG*/'Time Zone': { /*LANG*/'Time Zone': {
value: settings.timezone, value: settings.timezone,
format: v => (v > 0 ? "+" : "") + v,
min: -11, min: -11,
max: 13, max: 13,
step: 0.5, step: 0.5,
@ -486,13 +488,23 @@ function showLocaleMenu() {
updateSettings(); updateSettings();
} }
}, },
/*LANG*/'Clock Style': { /*LANG*/'Time Format': {
value: !!settings["12hour"], value: !!settings["12hour"],
format: v => v ? "12hr" : "24hr", format: v => v ? "12h" : "24h",
onchange: v => { onchange: v => {
settings["12hour"] = v; settings["12hour"] = v;
updateSettings(); updateSettings();
} }
},
/*LANG*/'Start Week On': {
value: settings["firstDayOfWeek"] || 0,
min: 0, // Sunday
max: 1, // Monday
format: v => require("date_utils").dow(v, 1),
onchange: v => {
settings["firstDayOfWeek"] = v;
updateSettings();
},
} }
}; };
return E.showMenu(localemenu); return E.showMenu(localemenu);
@ -606,11 +618,34 @@ function showClockMenu() {
function showSetTimeMenu() { function showSetTimeMenu() {
d = new Date(); d = new Date();
const timemenu = { const timemenu = {
'': { 'title': /*LANG*/'Set Time' }, '': { 'title': /*LANG*/'Date & Time' },
'< Back': function () { '< Back': function () {
setTime(d.getTime() / 1000); setTime(d.getTime() / 1000);
showSystemMenu(); showSystemMenu();
}, },
/*LANG*/'Day': {
value: d.getDate(),
onchange: function (v) {
this.value = ((v+30)%31)+1;
d.setDate(this.value);
}
},
/*LANG*/'Month': {
value: d.getMonth() + 1,
format: v => require("date_utils").month(v),
onchange: function (v) {
this.value = ((v+11)%12)+1;
d.setMonth(this.value - 1);
}
},
/*LANG*/'Year': {
value: d.getFullYear(),
min: 2019,
max: 2100,
onchange: function (v) {
d.setFullYear(v);
}
},
/*LANG*/'Hour': { /*LANG*/'Hour': {
value: d.getHours(), value: d.getHours(),
onchange: function (v) { onchange: function (v) {
@ -631,28 +666,6 @@ function showSetTimeMenu() {
this.value = (v+60)%60; this.value = (v+60)%60;
d.setSeconds(this.value); d.setSeconds(this.value);
} }
},
/*LANG*/'Date': {
value: d.getDate(),
onchange: function (v) {
this.value = ((v+30)%31)+1;
d.setDate(this.value);
}
},
/*LANG*/'Month': {
value: d.getMonth() + 1,
onchange: function (v) {
this.value = ((v+11)%12)+1;
d.setMonth(this.value - 1);
}
},
/*LANG*/'Year': {
value: d.getFullYear(),
min: 2019,
max: 2100,
onchange: function (v) {
d.setFullYear(v);
}
} }
}; };
return E.showMenu(timemenu); return E.showMenu(timemenu);

View File

@ -1 +1,2 @@
0.01: Release 0.01: Release
0.02: Rewrite with new interface

View File

@ -1,21 +1,18 @@
# Simple Timer # Simple Timer
A simple app to set a timer quickly. Simply tab on top/bottom/left/right A simple app to set a timer quickly. Drag or tap on the up and down buttons over the hour, minute or second to set the time.
to select the minutes and tab in the middle of the screen to start/stop
the timer. Note that this timer depends on qalarm.
# Overview This app uses the `sched` library, which allows the timer to continue to run in the background when this app is closed.
If you open the app, you can simply control the timer
by clicking on top, bottom, left or right of the screen.
If you tab at the middle of the screen, the timer is
started / stopped.
![](description.png) ![](screenshot_1.png)
![](screenshot_2.png)
![](screenshot_3.png)
![](screenshot_4.png)
# Creators
# Creator
[David Peer](https://github.com/peerdavid) [David Peer](https://github.com/peerdavid)
[Sir Indy](https://github.com/sir-indy)
# Thanks to... # Thanks to...
Time icon created by <a href="https://www.flaticon.com/free-icons/time" title="time icons">CreativeCons - Flaticon</a> Time icon created by <a href="https://www.flaticon.com/free-icons/time" title="time icons">CreativeCons - Flaticon</a>

View File

@ -3,122 +3,188 @@
* *
* Creator: David Peer * Creator: David Peer
* Date: 02/2022 * Date: 02/2022
*
* Modified: Sir Indy
* Date: 05/2022
*/ */
Bangle.loadWidgets(); const Layout = require("Layout");
const alarm = require("sched")
const alarm = require("sched");
const TIMER_IDX = "smpltmr"; const TIMER_IDX = "smpltmr";
const screenWidth = g.getWidth();
const screenHeight = g.getHeight();
const cx = parseInt(screenWidth/2);
const cy = parseInt(screenHeight/2)-12;
var minutes = 5;
var interval; //used for the 1 second interval timer
const secondsToTime = (s) => new Object({h:Math.floor((s/3600) % 24), m:Math.floor((s/60) % 60), s:Math.floor(s % 60)});
function isTimerEnabled(){ const clamp = (num, min, max) => Math.min(Math.max(num, min), max);
var alarmObj = alarm.getAlarm(TIMER_IDX); function formatTime(s) {
if(alarmObj===undefined || !alarmObj.on){ var t = secondsToTime(s);
return false; if (t.h) {
return t.h + ':' + ("0" + t.m).substr(-2) + ':' + ("0" + t.s).substr(-2);
} else {
return t.m + ':' + ("0" + t.s).substr(-2);
}
} }
return true; var seconds = 5 * 60; // Default to 5 minutes
var drawTimeout;
//var timerRunning = false;
function timerRunning() {
return (alarm.getTimeToAlarm(alarm.getAlarm(TIMER_IDX)) != undefined)
}
const imgArrow = atob("CQmBAAgOBwfD47ndx+OA");
const imgPause = atob("GBiBAP+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B//+B/w==");
const imgPlay = atob("GBiBAIAAAOAAAPgAAP4AAP+AAP/gAP/4AP/+AP//gP//4P//+P///v///v//+P//4P//gP/+AP/4AP/gAP+AAP4AAPgAAOAAAIAAAA==");
function onDrag(event) {
if (!timerRunning()) {
Bangle.buzz(40, 0.3);
var diff = -Math.round(event.dy/5);
if (event.x < timePickerLayout.hours.w) {
diff *= 3600;
} else if (event.x > timePickerLayout.mins.x && event.x < timePickerLayout.secs.x) {
diff *= 60;
}
updateTimePicker(diff);
}
} }
function getTimerMin(){ function onTouch(button, xy) {
var alarmObj = alarm.getAlarm(TIMER_IDX); if (xy.y > (timePickerLayout.btnStart.y||timerLayout.btnStart.y)) {
return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000)); Bangle.buzz(40, 0.3);
onButton();
return;
}
if (!timerRunning()) {
var touchMidpoint = timePickerLayout.hours.y + timePickerLayout.hours.h/2;
var diff = 0;
Bangle.buzz(40, 0.3);
if (xy.y > 24 && xy.y < touchMidpoint - 10) {
diff = 1;
} else if (xy.y > touchMidpoint + 10 && xy.y < timePickerLayout.btnStart.y) {
diff = -1;
}
if (xy.x < timePickerLayout.hours.w) {
diff *= 3600;
} else if (xy.x > timePickerLayout.mins.x && xy.x < timePickerLayout.secs.x) {
diff *= 60;
}
updateTimePicker(diff);
} }
function setTimer(minutes){ }
function onButton() {
g.clearRect(Bangle.appRect);
if (timerRunning()) {
timerStop();
} else {
if (seconds > 0) {
timerRun();
}
}
}
function updateTimePicker(diff) {
seconds = clamp(seconds + (diff || 0), 0, 24 * 3600 - 1);
var set_time = secondsToTime(seconds);
updateLayoutField(timePickerLayout, 'hours', set_time.h);
updateLayoutField(timePickerLayout, 'mins', set_time.m);
updateLayoutField(timePickerLayout, 'secs', set_time.s);
}
function updateTimer() {
var timeToNext = alarm.getTimeToAlarm(alarm.getAlarm(TIMER_IDX));
updateLayoutField(timerLayout, 'timer', formatTime(timeToNext / 1000));
queueDraw(1000);
}
function queueDraw(millisecs) {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
updateTimer();
}, millisecs - (Date.now() % millisecs));
}
function timerRun() {
alarm.setAlarm(TIMER_IDX, { alarm.setAlarm(TIMER_IDX, {
// msg : "Simple Timer", vibrate : ".-.-",
timer : minutes*60*1000, hidden: true,
timer : seconds * 1000
}); });
alarm.reload(); alarm.reload();
g.clearRect(Bangle.appRect);
timerLayout.render();
updateTimer();
} }
function deleteTimer(){ function timerStop() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
var timeToNext = alarm.getTimeToAlarm(alarm.getAlarm(TIMER_IDX));
if (timeToNext != undefined) {
seconds = timeToNext / 1000;
}
alarm.setAlarm(TIMER_IDX, undefined); alarm.setAlarm(TIMER_IDX, undefined);
alarm.reload(); alarm.reload();
g.clearRect(Bangle.appRect);
timePickerLayout.render();
updateTimePicker();
} }
setWatch(_=>load(), BTN1); var timePickerLayout = new Layout({
function draw(){ type:"v", c: [
g.clear(1); {type:undefined, height:2},
Bangle.drawWidgets(); {type:"h", c: [
{type:"v", width:g.getWidth()/3, c: [
if (interval) { {type:"txt", font:"6x8", label:/*LANG*/"Hours"},
clearInterval(interval); {type:"img", pad:8, src:imgArrow},
} {type:"txt", font:"20%", label:"00", id:"hours", filly:1, fillx:1},
interval = undefined; {type:"img", pad:8, src:imgArrow, r:2}
]},
// Write time {type:"v", width:g.getWidth()/3, c: [
g.setFontAlign(0, 0, 0); {type:"txt", font:"6x8", label:/*LANG*/"Minutes"},
g.setFont("Vector", 32).setFontAlign(0,-1); {type:"img", pad:8, src:imgArrow},
{type:"txt", font:"20%", label:"00", id:"mins", filly:1, fillx:1},
var started = isTimerEnabled(); {type:"img", pad:8, src:imgArrow, r:2}
var text = minutes + " min."; ]},
if(started){ {type:"v", width:g.getWidth()/3, c: [
var min = getTimerMin(); {type:"txt", font:"6x8", label:/*LANG*/"Seconds"},
text = min + " min."; {type:"img", pad:8, src:imgArrow},
} {type:"txt", font:"20%", label:"00", id:"secs", filly:1, fillx:1},
{type:"img", pad:8, src:imgArrow, r:2}
var rectWidth = parseInt(g.stringWidth(text) / 2); ]},
]},
if(started){ {type:"btn", src:imgPlay, id:"btnStart", fillx:1 }
interval = setInterval(draw, 1000); ], filly:1
g.setColor("#ff0000");
} else {
g.setColor(g.theme.fg);
}
g.fillRect(cx-rectWidth-5, cy-5, cx+rectWidth, cy+30);
g.setColor(g.theme.bg);
g.drawString(text, cx, cy);
}
Bangle.on('touch', function(btn, e){
var left = parseInt(g.getWidth() * 0.25);
var right = g.getWidth() - left;
var upper = parseInt(g.getHeight() * 0.25);
var lower = g.getHeight() - upper;
var isLeft = e.x < left;
var isRight = e.x > right;
var isUpper = e.y < upper;
var isLower = e.y > lower;
var isMiddle = !isLeft && !isRight && !isUpper && !isLower;
var started = isTimerEnabled();
if(isRight && !started){
minutes += 1;
Bangle.buzz(40, 0.3);
} else if(isLeft && !started){
minutes -= 1;
Bangle.buzz(40, 0.3);
} else if(isUpper && !started){
minutes += 5;
Bangle.buzz(40, 0.3);
} else if(isLower && !started){
minutes -= 5;
Bangle.buzz(40, 0.3);
} else if(isMiddle) {
if(!started){
setTimer(minutes);
} else {
deleteTimer();
}
Bangle.buzz(80, 0.6);
}
minutes = Math.max(0, minutes);
draw();
}); });
g.reset(); var timerLayout = new Layout({
draw(); type:"v", c: [
{type:"txt", font:"22%", label:"0:00", id:"timer", fillx:1, filly:1 },
{type:"btn", src:imgPause, id:"btnStart", cb: l=>timerStop(), fillx:1 }
], filly:1
});
function updateLayoutField(layout, field, value) {
layout.clear(layout[field]);
layout[field].label = value;
layout.render(layout[field]);
}
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setUI({
mode : "custom",
touch : function(n,e) {onTouch(n,e);},
drag : function(e) {onDrag(e);},
btn : function(n) {onButton();},
});
g.clearRect(Bangle.appRect);
if (timerRunning()) {
timerLayout.render();
updateTimer();
} else {
timePickerLayout.render();
updateTimePicker();
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -2,13 +2,13 @@
"id": "smpltmr", "id": "smpltmr",
"name": "Simple Timer", "name": "Simple Timer",
"shortName": "Simple Timer", "shortName": "Simple Timer",
"version": "0.01", "version": "0.02",
"description": "A very simple app to start a timer.", "description": "A very simple app to start a timer.",
"icon": "app.png", "icon": "app.png",
"tags": "tool", "tags": "tool,alarm,timer",
"dependencies": {"scheduler":"type"}, "dependencies": {"scheduler":"type"},
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"screenshots": [{"url":"screenshot.png"}, {"url": "screenshot_2.png"}], "screenshots": [{"url":"screenshot_1.png"}, {"url": "screenshot_2.png"}, {"url": "screenshot_3.png"}, {"url": "screenshot_4.png"}],
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{"name":"smpltmr.app.js","url":"app.js"}, {"name":"smpltmr.app.js","url":"app.js"},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -5,7 +5,7 @@
"version": "0.01", "version": "0.01",
"description": "Let's you swipe from left to right on any app to return back to the clock face. Please configure in the settings app after installing to activate, since its disabled by default.", "description": "Let's you swipe from left to right on any app to return back to the clock face. Please configure in the settings app after installing to activate, since its disabled by default.",
"icon": "app.png", "icon": "app.png",
"type": "boot", "type": "bootloader",
"tags": "tools", "tags": "tools",
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"readme": "README.md", "readme": "README.md",

View File

@ -1,2 +1,3 @@
0.01: Initial version 0.01: Initial version
0.02: Do not warn multiple times for the same exceedance 0.02: Do not warn multiple times for the same exceedance
0.03: Fix crash

View File

@ -2,7 +2,7 @@
"id": "widbaroalarm", "id": "widbaroalarm",
"name": "Barometer Alarm Widget", "name": "Barometer Alarm Widget",
"shortName": "Barometer Alarm", "shortName": "Barometer Alarm",
"version": "0.02", "version": "0.03",
"description": "A widget that can alarm on when the pressure reaches defined thresholds.", "description": "A widget that can alarm on when the pressure reaches defined thresholds.",
"icon": "widget.png", "icon": "widget.png",
"type": "widget", "type": "widget",

View File

@ -104,7 +104,7 @@
saveSetting("lastHighWarningTs", 0); saveSetting("lastHighWarningTs", 0);
} }
if (!alreadyWarned) { if (history3.length > 0 && !alreadyWarned) {
// 3h change detection // 3h change detection
const drop3halarm = setting("drop3halarm"); const drop3halarm = setting("drop3halarm");
const raise3halarm = setting("raise3halarm"); const raise3halarm = setting("raise3halarm");

View File

@ -65,6 +65,7 @@ const APP_KEYS = [
const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite', 'supports']; const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite', 'supports'];
const DATA_KEYS = ['name', 'wildcard', 'storageFile', 'url', 'content', 'evaluate']; const DATA_KEYS = ['name', 'wildcard', 'storageFile', 'url', 'content', 'evaluate'];
const SUPPORTS_DEVICES = ["BANGLEJS","BANGLEJS2"]; // device IDs allowed for 'supports' const SUPPORTS_DEVICES = ["BANGLEJS","BANGLEJS2"]; // device IDs allowed for 'supports'
const METADATA_TYPES = ["app","clock","widget","bootloader","RAM","launch","textinput","scheduler","notify","locale"]; // values allowed for "type" field
const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info const FORBIDDEN_FILE_NAME_CHARS = /[,;]/; // used as separators in appid.info
const VALID_DUPLICATES = [ '.tfmodel', '.tfnames' ]; const VALID_DUPLICATES = [ '.tfmodel', '.tfnames' ];
const GRANDFATHERED_ICONS = ["s7clk", "snek", "astral", "alpinenav", "slomoclock", "arrow", "pebble", "rebble"]; const GRANDFATHERED_ICONS = ["s7clk", "snek", "astral", "alpinenav", "slomoclock", "arrow", "pebble", "rebble"];
@ -94,6 +95,8 @@ apps.forEach((app,appIdx) => {
if (!app.name) ERROR(`App ${app.id} has no name`); if (!app.name) ERROR(`App ${app.id} has no name`);
var isApp = !app.type || app.type=="app"; var isApp = !app.type || app.type=="app";
if (app.name.length>20 && !app.shortName && isApp) ERROR(`App ${app.id} has a long name, but no shortName`); if (app.name.length>20 && !app.shortName && isApp) ERROR(`App ${app.id} has a long name, but no shortName`);
if (app.type && !METADATA_TYPES.includes(app.type))
ERROR(`App ${app.id} 'type' is one one of `+METADATA_TYPES);
if (!Array.isArray(app.supports)) ERROR(`App ${app.id} has no 'supports' field or it's not an array`); if (!Array.isArray(app.supports)) ERROR(`App ${app.id} has no 'supports' field or it's not an array`);
else { else {
app.supports.forEach(dev => { app.supports.forEach(dev => {
@ -135,6 +138,9 @@ apps.forEach((app,appIdx) => {
Object.keys(app.dependencies).forEach(dependency => { Object.keys(app.dependencies).forEach(dependency => {
if (!["type","app"].includes(app.dependencies[dependency])) if (!["type","app"].includes(app.dependencies[dependency]))
ERROR(`App ${app.id} 'dependencies' must all be tagged 'type' or 'app' right now`); ERROR(`App ${app.id} 'dependencies' must all be tagged 'type' or 'app' right now`);
if (app.dependencies[dependency]=="type" && !METADATA_TYPES.includes(dependency))
ERROR(`App ${app.id} 'type' dependency must be one of `+METADATA_TYPES);
}); });
} else } else
ERROR(`App ${app.id} 'dependencies' must be an object`); ERROR(`App ${app.id} 'dependencies' must be an object`);

2
core

@ -1 +1 @@
Subproject commit 6fc78fc39531a43148ae8d515efaeff9404d1daf Subproject commit 32d01b5b3d8e013ca0364671e2352b7b0dd48bb4

View File

@ -25,7 +25,7 @@ DEVICEINFO = DEVICEINFO.filter(x=>x.id.startsWith("BANGLEJS"));
// Set up source code URL // Set up source code URL
(function() { (function() {
let username = "espruino"; let username = "espruino";
let githubMatch = window.location.href.match(/\/(\w+)\.github\.io/); let githubMatch = window.location.href.match(/\/([\w-]+)\.github\.io/);
if (githubMatch) username = githubMatch[1]; if (githubMatch) username = githubMatch[1];
Const.APP_SOURCECODE_URL = `https://github.com/${username}/BangleApps/tree/master/apps`; Const.APP_SOURCECODE_URL = `https://github.com/${username}/BangleApps/tree/master/apps`;
})(); })();

View File

@ -1,18 +1,13 @@
/* Copyright (c) 2022 Bangle.js contributors. See the file LICENSE for copying permission. */ /* Copyright (c) 2022 Bangle.js contributors. See the file LICENSE for copying permission. */
/* /*
Take a look at README.md for hints on developing with this library. Take a look at README.md for hints on developing with this library.
Usage: Usage:
``` ```
var Layout = require("Layout"); var Layout = require("Layout");
var layout = new Layout( layoutObject, options ) var layout = new Layout( layoutObject, options )
layout.render(optionalObject); layout.render(optionalObject);
``` ```
For example: For example:
``` ```
var Layout = require("Layout"); var Layout = require("Layout");
var layout = new Layout( { var layout = new Layout( {
@ -24,23 +19,22 @@ var layout = new Layout( {
g.clear(); g.clear();
layout.render(); layout.render();
``` ```
layoutObject has: layoutObject has:
* A `type` field of: * A `type` field of:
* `undefined` - blank, can be used for padding * `undefined` - blank, can be used for padding
* `"txt"` - a text label, with value `label` and `r` for text rotation. 'font' is required * `"txt"` - a text label, with value `label`. 'font' is required
* `"btn"` - a button, with value `label` and callback `cb` * `"btn"` - a button, with value `label` and callback `cb`
optional `src` specifies an image (like img) in which case label is ignored optional `src` specifies an image (like img) in which case label is ignored
Default font is `6x8`, scale 2. This can be overridden with the `font` or `scale` fields.
* `"img"` - an image where `src` is an image, or a function which is called to return an image to draw. * `"img"` - an image where `src` is an image, or a function which is called to return an image to draw.
optional `scale` specifies if image should be scaled up or not
* `"custom"` - a custom block where `render(layoutObj)` is called to render * `"custom"` - a custom block where `render(layoutObj)` is called to render
* `"h"` - Horizontal layout, `c` is an array of more `layoutObject` * `"h"` - Horizontal layout, `c` is an array of more `layoutObject`
* `"v"` - Vertical layout, `c` is an array of more `layoutObject` * `"v"` - Vertical layout, `c` is an array of more `layoutObject`
* A `id` field. If specified the object is added with this name to the * A `id` field. If specified the object is added with this name to the
returned `layout` object, so can be referenced as `layout.foo` returned `layout` object, so can be referenced as `layout.foo`
* A `font` field, eg `6x8` or `30%` to use a percentage of screen height * A `font` field, eg `6x8` or `30%` to use a percentage of screen height. Set scale with :, e.g. `6x8:2`.
* A `scale` field, eg `2` to set scale of an image
* A `r` field to set rotation of text or images (0: 0°, 1: 90°, 2: 180°, 3: 270°).
* A `wrap` field to enable line wrapping. Requires some combination of `width`/`height` * A `wrap` field to enable line wrapping. Requires some combination of `width`/`height`
and `fillx`/`filly` to be set. Not compatible with text rotation. and `fillx`/`filly` to be set. Not compatible with text rotation.
* A `col` field, eg `#f00` for red * A `col` field, eg `#f00` for red
@ -51,34 +45,25 @@ layoutObject has:
* A `fillx` int to choose if the object should fill available space in x. 0=no, 1=yes, 2=2x more space * A `fillx` int to choose if the object should fill available space in x. 0=no, 1=yes, 2=2x more space
* A `filly` int to choose if the object should fill available space in y. 0=no, 1=yes, 2=2x more space * A `filly` int to choose if the object should fill available space in y. 0=no, 1=yes, 2=2x more space
* `width` and `height` fields to optionally specify minimum size * `width` and `height` fields to optionally specify minimum size
options is an object containing: options is an object containing:
* `lazy` - a boolean specifying whether to enable automatic lazy rendering * `lazy` - a boolean specifying whether to enable automatic lazy rendering
* `btns` - array of objects containing: * `btns` - array of objects containing:
* `label` - the text on the button * `label` - the text on the button
* `cb` - a callback function * `cb` - a callback function
* `cbl` - a callback function for long presses * `cbl` - a callback function for long presses
* `back` - a callback function, passed as `back` into Bangle.setUI * `back` - a callback function, passed as `back` into Bangle.setUI
If automatic lazy rendering is enabled, calls to `layout.render()` will attempt to automatically If automatic lazy rendering is enabled, calls to `layout.render()` will attempt to automatically
determine what objects have changed or moved, clear their previous locations, and re-render just those objects. determine what objects have changed or moved, clear their previous locations, and re-render just those objects.
Once `layout.update()` is called, the following fields are added Once `layout.update()` is called, the following fields are added
to each object: to each object:
* `x` and `y` for the top left position * `x` and `y` for the top left position
* `w` and `h` for the width and height * `w` and `h` for the width and height
* `_w` and `_h` for the **minimum** width and height * `_w` and `_h` for the **minimum** width and height
Other functions: Other functions:
* `layout.update()` - update positions of everything if contents have changed * `layout.update()` - update positions of everything if contents have changed
* `layout.debug(obj)` - draw outlines for objects on screen * `layout.debug(obj)` - draw outlines for objects on screen
* `layout.clear(obj)` - clear the given object (you can also just specify `bgCol` to clear before each render) * `layout.clear(obj)` - clear the given object (you can also just specify `bgCol` to clear before each render)
* `layout.forgetLazyState()` - if lazy rendering is enabled, makes the next call to `render()` perform a full re-render * `layout.forgetLazyState()` - if lazy rendering is enabled, makes the next call to `render()` perform a full re-render
*/ */
@ -261,10 +246,20 @@ Layout.prototype.render = function (l) {
], bg = l.selected?g.theme.bgH:g.theme.bg2; ], bg = l.selected?g.theme.bgH:g.theme.bg2;
g.setColor(bg).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg2).drawPoly(poly); g.setColor(bg).fillPoly(poly).setColor(l.selected ? g.theme.fgH : g.theme.fg2).drawPoly(poly);
if (l.col!==undefined) g.setColor(l.col); if (l.col!==undefined) g.setColor(l.col);
if (l.src) g.setBgColor(bg).drawImage("function"==typeof l.src?l.src():l.src, l.x + 10 + (0|l.pad), l.y + 8 + (0|l.pad)); if (l.src) g.setBgColor(bg).drawImage(
else g.setFont("6x8",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2); "function"==typeof l.src?l.src():l.src,
l.x + l.w/2,
l.y + l.h/2,
{scale: l.scale||undefined, rotate: Math.PI*0.5*(l.r||0)}
);
else g.setFont(l.font||"6x8:2").setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2);
}, "img":function(l){ }, "img":function(l){
g.drawImage("function"==typeof l.src?l.src():l.src, l.x + (0|l.pad), l.y + (0|l.pad), l.scale?{scale:l.scale}:undefined); g.drawImage(
"function"==typeof l.src?l.src():l.src,
l.x + l.w/2,
l.y + l.h/2,
{scale: l.scale||undefined, rotate: Math.PI*0.5*(l.r||0)}
);
}, "custom":function(l){ }, "custom":function(l){
l.render(l); l.render(l);
},"h":function(l) { l.c.forEach(render); }, },"h":function(l) { l.c.forEach(render); },
@ -365,7 +360,9 @@ Layout.prototype.update = function() {
l._w = m.width; l._h = m.height; l._w = m.width; l._h = m.height;
} }
}, "btn": function(l) { }, "btn": function(l) {
var m = l.src?g.imageMetrics("function"==typeof l.src?l.src():l.src):g.setFont("6x8",2).stringMetrics(l.label); if (l.font && l.font.endsWith("%"))
l.font = "Vector"+Math.round(g.getHeight()*l.font.slice(0,-1)/100);
var m = l.src?g.imageMetrics("function"==typeof l.src?l.src():l.src):g.setFont(l.font||"6x8:2").stringMetrics(l.label);
l._h = 16 + m.height; l._h = 16 + m.height;
l._w = 20 + m.width; l._w = 20 + m.width;
}, "img": function(l) { }, "img": function(l) {

View File

@ -1,39 +1,65 @@
/* Utility functions that use the 'locale' module so can produce text // module "date_utils"
in the currently selected language. */ //
// Utility functions that use the "locale" module so can produce
// date-related text in the currently selected language.
//
// Some functions have a "firstDayOfWeek" parameter.
// Most used values are:
// - 0/undefined --> Sunday
// - 1 --> Monday
// but you can start the week from any day if you need it.
//
// Some functions have an "abbreviated" parameter.
// It supports the following 3 values:
// - 0/undefined --> get the full value, without abbreviation (eg.: "Monday", "January", etc.)
// - 1 --> get the short value (eg.: "Mon", "Jan", etc.)
// - 2 --> get only the first char (eg.: "M", "J", etc.)
//
/** Return the day of the week (0=Sunday) /**
short==0/undefined -> "Sunday" * @param {int} i The index of the day of the week (0 = Sunday)
short==1 -> "Sun" * @param {int} abbreviated
* @returns The localized name of the i-th day of the week
*/ */
exports.getDOW = (dow, short) => require("locale").dow({getDay:()=>dow},short); exports.dow = (i, abbreviated) => {
var dow = require("locale").dow(new Date(((i || 0) + 3.5) * 86400000), abbreviated).slice(0, (abbreviated == 2) ? 1 : 100);
/** Return the month (1=January) return abbreviated == 2 ? dow.toUpperCase() : dow;
short==0/undefined -> "January"
short==1 -> "Jan"
*/
exports.getMonth = (month, short) => require("locale").month({getMonth:()=>month-1},short);
/** Return all 7 days of the week as an array ["Sunday","Monday",...].
short==0/undefined -> ["Sunday",...
short==1 -> ["Sun",...
short==2 -> ["S",...
*/
exports.getDOWs = (short) => {
var locale = require("locale");
var days = [];
for (var i=0;i<7;i++)
days.push(locale.dow({getDay:()=>i},short).slice(0,(short==2)?1:100));
return days;
} }
/** Return all 12 months as an array ["January","February",...] /**
short==0/undefined -> ["January",... * @param {int} firstDayOfWeek 0/undefined -> Sunday,
short==1 -> ["Jan",... * 1 -> Monday
* @param {int} abbreviated
* @returns All 7 days of the week (localized) as an array
*/ */
exports.getMonths = (short) => { exports.dows = (firstDayOfWeek, abbreviated) => {
var dows = [];
var locale = require("locale"); var locale = require("locale");
for (var i = 0; i < 7; i++) {
dows.push(exports.dow(i + (firstDayOfWeek || 0), abbreviated))
}
return abbreviated == 2 ? dows.map(dow => dow.toUpperCase()) : dows;
};
/**
* @param {int} i The index of the month (1 = January)
* @param {int} abbreviated
* @returns The localized name of the i-th month
*/
exports.month = (i, abbreviated) => {
var month = require("locale").month(new Date((i - 0.5) * 2628000000), abbreviated).slice(0, (abbreviated == 2) ? 1 : 100);
return abbreviated == 2 ? month.toUpperCase() : month;
}
/**
* @param {int} abbreviated
* @returns All 12 months (localized) as an array
*/
exports.months = (abbreviated) => {
var months = []; var months = [];
for (var i=0;i<12;i++) var locale = require("locale");
months.push(locale.month({getMonth:()=>i},short)); for (var i = 1; i <= 12; i++) {
return months; months.push(locale.month(new Date((i - 0.5) * 2628000000), abbreviated).slice(0, (abbreviated == 2) ? 1 : 100));
} }
return abbreviated == 2 ? months.map(month => month.toUpperCase()) : months;
};

View File

@ -19,7 +19,7 @@ SRCBMP=$SRCDIR/`basename $SRCJS .js`.bmp
echo "TEST $SRCJS ($SRCBMP)" echo "TEST $SRCJS ($SRCBMP)"
cat ../../modules/Layout.js > $TESTJS cat ../../modules/Layout.js > $TESTJS
echo 'Bangle = { setUI : function(){} };BTN1=0;process.env = process.env;process.env.HWVERSION=2;' >> $TESTJS echo 'Bangle = { setUI : function(){}, appRect:{x:0,y:0,w:176,h:176,x2:175,y2:175} };BTN1=0;process.env = process.env;process.env.HWVERSION=2;' >> $TESTJS
echo 'g = Graphics.createArrayBuffer(176,176,4);' >> $TESTJS echo 'g = Graphics.createArrayBuffer(176,176,4);' >> $TESTJS
cat $SRCJS >> $TESTJS || exit 1 cat $SRCJS >> $TESTJS || exit 1
echo 'layout.render()' >> $TESTJS echo 'layout.render()' >> $TESTJS

View File

@ -1,6 +1,7 @@
var BTN2 = 1, BTN3=2; var BTN2 = 1, BTN3=2;
process.env = process.env;process.env.HWVERSION=1; process.env = process.env;process.env.HWVERSION=1;
g = Graphics.createArrayBuffer(240,240,4); g = Graphics.createArrayBuffer(240,240,4);
Bangle.appRect = {x:0,y:0,w:240,h:240,x2:239,y2:239};
var layout = new Layout({ type: "v", c: [ var layout = new Layout({ type: "v", c: [
{type:"txt", font:"6x8", label:"A test"}, {type:"txt", font:"6x8", label:"A test"},

View File

@ -1,6 +1,7 @@
var BTN2 = 1, BTN3=2; var BTN2 = 1, BTN3=2;
process.env = process.env;process.env.HWVERSION=1; process.env = process.env;process.env.HWVERSION=1;
g = Graphics.createArrayBuffer(240,240,4); g = Graphics.createArrayBuffer(240,240,4);
Bangle.appRect = {x:0,y:0,w:240,h:240,x2:239,y2:239};
var layout = new Layout({ type: "v", c: [ var layout = new Layout({ type: "v", c: [
{type:"txt", font:"6x8", label:"A test"}, {type:"txt", font:"6x8", label:"A test"},

View File

@ -1,6 +1,7 @@
var BTN2 = 1, BTN3=2; var BTN2 = 1, BTN3=2;
process.env = process.env;process.env.HWVERSION=1; process.env = process.env;process.env.HWVERSION=1;
g = Graphics.createArrayBuffer(240,240,4); g = Graphics.createArrayBuffer(240,240,4);
Bangle.appRect = {x:0,y:0,w:240,h:240,x2:239,y2:239};
/* When displaying OSD buttons on Bangle.js 1 we should turn /* When displaying OSD buttons on Bangle.js 1 we should turn
the side buttons into 'soft' buttons and then use the physical the side buttons into 'soft' buttons and then use the physical

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB