diff --git a/.gitignore b/.gitignore index fce2efb1a..231851dd6 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ tests/Layout/bin/tmp.* tests/Layout/testresult.bmp apps.local.json _site +.jekyll-cache diff --git a/README.md b/README.md index 78dd1b492..ee555cad2 100644 --- a/README.md +++ b/README.md @@ -186,7 +186,7 @@ The widget example is available in [`apps/_example_widget`](apps/_example_widget Widgets are just small bits of code that run whenever an app that supports them calls `Bangle.loadWidgets()`. If they want to display something in the 24px high -widget bar at the top of the screen they can add themselves to the global +widget bar at the top of the screen they can add themselves to the global `WIDGETS` array with: ``` @@ -226,10 +226,8 @@ and which gives information about the app for the Launcher. "name":"Short Name", // for Bangle.js menu "icon":"*myappid", // for Bangle.js menu "src":"-myappid", // source file - "type":"widget/clock/app/bootloader", // optional, default "app" - // if this is 'widget' then it's not displayed in the menu - // 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 + "type":"widget/clock/app/bootloader/...", // optional, default "app" + // see 'type' in 'metadata.json format' below for more options/info "version":"1.23", // added by BangleApps loader on upload based on metadata.json "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 "description": "...", // long description (can contain markdown) "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) - // 'app' - an application // 'clock' - a clock - required for clocks to automatically start // 'widget' - a widget - // 'launch' - replacement launcher app - // 'bootloader' - code that runs at startup only + // 'bootloader' - an app that at startup (app.boot.js) but doesn't have a launcher entry for 'app.js' // '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 "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 // 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 @@ -415,7 +419,7 @@ Example `settings.js` // make sure to enclose the function in parentheses (function(back) { let settings = require('Storage').readJSON('myappid.json',1)||{}; - if (typeof settings.monkeys !== "number") settings.monkeys = 12; // default value + if (typeof settings.monkeys !== "number") settings.monkeys = 12; // default value function save(key, value) { settings[key] = value; require('Storage').write('myappid.json', settings); diff --git a/apps/activityreminder/ChangeLog b/apps/activityreminder/ChangeLog index 53e29a66d..ef387d098 100644 --- a/apps/activityreminder/ChangeLog +++ b/apps/activityreminder/ChangeLog @@ -1,2 +1,4 @@ 0.01: New App! 0.02: Fix the settings bug and some tweaking +0.03: Do not alarm while charging +0.04: Obey system quiet mode diff --git a/apps/activityreminder/README.md b/apps/activityreminder/README.md index 1e643fb54..e8be66f3c 100644 --- a/apps/activityreminder/README.md +++ b/apps/activityreminder/README.md @@ -1,13 +1,14 @@ # Activity reminder 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: - Enable : Enable/Disable the app - Start hour: Hour to start the reminder - End hour: Hour to end the reminder - 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 diff --git a/apps/activityreminder/app.js b/apps/activityreminder/app.js index 310dc10b0..811399ca6 100644 --- a/apps/activityreminder/app.js +++ b/apps/activityreminder/app.js @@ -13,8 +13,11 @@ function drawAlert(){ } load(); }); - - Bangle.buzz(400); + + // Obey system quiet mode: + if (!(require('Storage').readJSON('setting.json',1)||{}).quiet) { + Bangle.buzz(400); + } setTimeout(load, 20000); } diff --git a/apps/activityreminder/boot.js b/apps/activityreminder/boot.js index 0f89bf543..09aa9d757 100644 --- a/apps/activityreminder/boot.js +++ b/apps/activityreminder/boot.js @@ -1,4 +1,5 @@ function run(){ + if (Bangle.isCharging()) return; var now = new Date(); var h = now.getHours(); if(h >= activityreminder.startHour && h < activityreminder.endHour){ diff --git a/apps/activityreminder/metadata.json b/apps/activityreminder/metadata.json index eba5de105..89b0fbc0b 100644 --- a/apps/activityreminder/metadata.json +++ b/apps/activityreminder/metadata.json @@ -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.02", + "version":"0.04", "icon": "app.png", "type": "app", "tags": "tool,activity", diff --git a/apps/activityreminder/settings.js b/apps/activityreminder/settings.js index 9b9a0ecd8..e4f44efc6 100644 --- a/apps/activityreminder/settings.js +++ b/apps/activityreminder/settings.js @@ -43,7 +43,7 @@ }, 'Dismiss delay': { value: settings.dismissDelayMin, - min: 5, max: 15, + min: 5, max: 60, onchange: v => { settings.dismissDelayMin = v; require("activityreminder").writeSettings(settings); diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index 9daf7bcbf..ca1417b5b 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -25,3 +25,4 @@ 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 0.25: Fix redrawing selected Alarm/Timer entry inside edit submenu +0.26: Add support for Monday as first day of the week (#1780) diff --git a/apps/alarm/app.js b/apps/alarm/app.js index 90a62afc5..cf46823d6 100644 --- a/apps/alarm/app.js +++ b/apps/alarm/app.js @@ -2,10 +2,14 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); // 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() { - let time = new Date(); + var time = new Date(); return ( time.getHours() * 3600000 + time.getMinutes() * 60000 + @@ -14,6 +18,9 @@ function getCurrentTime() { } 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").reload(); } @@ -23,30 +30,32 @@ function showMainMenu() { // Alarm img "\0"+atob("FBSBAABgA4YcMPDGP8Zn/mx/48//PP/zD/8A//AP/wD/8A//AP/wH/+D//w//8AAAADwAAYA") const menu = { '': { 'title': /*LANG*/'Alarms&Timers' }, - /*LANG*/'< Back' : ()=>{load();}, - /*LANG*/'New Alarm': ()=>editAlarm(-1), - /*LANG*/'New Timer': ()=>editTimer(-1) + /*LANG*/'< Back': () => { load(); }, + /*LANG*/'New Alarm': () => editAlarm(-1), + /*LANG*/'New Timer': () => editTimer(-1) }; - alarms.forEach((alarm,idx)=>{ - var type,txt; // a leading space is currently required (JS error in Espruino 2v12) + alarms.forEach((alarm, idx) => { + alarm.dow = handleFirstDayOfWeek(alarm.dow, firstDayOfWeek); + + var type, txt; // a leading space is currently required (JS error in Espruino 2v12) if (alarm.timer) { type = /*LANG*/"Timer"; - txt = " "+require("sched").formatTime(alarm.timer); + txt = " " + require("sched").formatTime(alarm.timer); } else { type = /*LANG*/"Alarm"; - txt = " "+require("sched").formatTime(alarm.t); + txt = " " + require("sched").formatTime(alarm.t); } - if (alarm.rp) txt += "\0"+atob("FBaBAAABgAAcAAHn//////wAHsABzAAYwAAMAADAAAAAAwAAMAADGAAzgAN4AD//////54AAOAABgAA="); + if (alarm.rp) txt += "\0" + atob("FBaBAAABgAAcAAHn//////wAHsABzAAYwAAMAADAAAAAAwAAMAADGAAzgAN4AD//////54AAOAABgAA="); // rename duplicate alarms - if (menu[type+txt]) { + if (menu[type + txt]) { var n = 2; - while (menu[type+" "+n+txt]) n++; - txt = type+" "+n+txt; - } else txt = type+txt; + while (menu[type + " " + n + txt]) n++; + txt = type + " " + n + txt; + } else txt = type + txt; // add to menu menu[txt] = { - value : "\0"+atob(alarm.on?"EhKBAH//v/////////////5//x//j//H+eP+Mf/A//h//z//////////3//g":"EhKBAH//v//8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA///3//g"), - onchange : function() { + value: "\0" + atob(alarm.on ? "EhKBAH//v/////////////5//x//j//H+eP+Mf/A//h//z//////////3//g" : "EhKBAH//v//8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA///3//g"), + onchange: function () { setTimeout(alarm.timer ? editTimer : editAlarm, 10, idx, alarm); } }; @@ -69,25 +78,28 @@ function showMainMenu() { function editDOW(dow, onchange) { const menu = { '': { '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 }); - menu[dayOfWeek] = { - value: !!(dow&(1< { + menu[day] = { + value: !!(dow & (1 << (i + firstDayOfWeek))), format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", - onchange: v => v ? dow |= 1< v ? (dow |= 1 << (i + firstDayOfWeek)) : (dow &= ~(1 << (i + firstDayOfWeek))) }; - })(i); + }); + E.showMenu(menu); } function editAlarm(alarmIndex, alarm) { - let newAlarm = alarmIndex < 0; - let a = require("sched").newDefaultAlarm(); + var newAlarm = alarmIndex < 0; + var a = require("sched").newDefaultAlarm(); + a.dow = handleFirstDayOfWeek(a.dow, firstDayOfWeek); + if (!newAlarm) Object.assign(a, alarms[alarmIndex]); - if (alarm) Object.assign(a,alarm); - let t = require("sched").decodeTime(a.t); + if (alarm) Object.assign(a, alarm); + var t = require("sched").decodeTime(a.t); const menu = { '': { 'title': /*LANG*/'Alarm' }, @@ -96,17 +108,17 @@ function editAlarm(alarmIndex, alarm) { showMainMenu(); }, /*LANG*/'Hours': { - value: t.hrs, min : 0, max : 23, wrap : true, - onchange: v => t.hrs=v + value: t.hrs, min: 0, max: 23, wrap: true, + onchange: v => t.hrs = v }, /*LANG*/'Minutes': { - value: t.mins, min : 0, max : 59, wrap : true, - onchange: v => t.mins=v + value: t.mins, min: 0, max: 59, wrap: true, + onchange: v => t.mins = v }, /*LANG*/'Enabled': { value: a.on, format: v => v ? /*LANG*/"On" : /*LANG*/"Off", - onchange: v=>a.on=v + onchange: v => a.on = v }, /*LANG*/'Repeat': { value: a.rp, @@ -114,14 +126,14 @@ function editAlarm(alarmIndex, alarm) { onchange: v => a.rp = v }, /*LANG*/'Days': { - value: "SMTWTFS".split("").map((d,n)=>a.dow&(1< setTimeout(editDOW, 100, a.dow, d => { a.dow = d; a.t = require("sched").encodeTime(t); editAlarm(alarmIndex, a); }) }, - /*LANG*/'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate=v ), + /*LANG*/'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate = v), /*LANG*/'Auto Snooze': { value: a.as, format: v => v ? /*LANG*/"Yes" : /*LANG*/"No", @@ -156,11 +168,11 @@ function saveAlarm(newAlarm, alarmIndex, a, t) { } function editTimer(alarmIndex, alarm) { - let newAlarm = alarmIndex < 0; - let a = require("sched").newDefaultTimer(); + var newAlarm = alarmIndex < 0; + var a = require("sched").newDefaultTimer(); if (!newAlarm) Object.assign(a, alarms[alarmIndex]); - if (alarm) Object.assign(a,alarm); - let t = require("sched").decodeTime(a.timer); + if (alarm) Object.assign(a, alarm); + var t = require("sched").decodeTime(a.timer); const menu = { '': { 'title': /*LANG*/'Timer' }, @@ -169,26 +181,26 @@ function editTimer(alarmIndex, alarm) { showMainMenu(); }, /*LANG*/'Hours': { - value: t.hrs, min : 0, max : 23, wrap : true, - onchange: v => t.hrs=v + value: t.hrs, min: 0, max: 23, wrap: true, + onchange: v => t.hrs = v }, /*LANG*/'Minutes': { - value: t.mins, min : 0, max : 59, wrap : true, - onchange: v => t.mins=v + value: t.mins, min: 0, max: 59, wrap: true, + onchange: v => t.mins = v }, /*LANG*/'Enabled': { value: a.on, format: v => v ? /*LANG*/"On" : /*LANG*/"Off", onchange: v => a.on = v }, - /*LANG*/'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate=v ), + /*LANG*/'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate = v), }; menu[/*LANG*/"Cancel"] = () => showMainMenu(); if (!newAlarm) { - menu[/*LANG*/"Delete"] = function() { - alarms.splice(alarmIndex,1); + menu[/*LANG*/"Delete"] = function () { + alarms.splice(alarmIndex, 1); saveAndReload(); showMainMenu(); }; @@ -210,6 +222,26 @@ function saveTimer(newAlarm, alarmIndex, a, t) { 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) { E.showPrompt(/*LANG*/"Are you sure?", { title: on ? /*LANG*/"Enable All" : /*LANG*/"Disable All" diff --git a/apps/alarm/metadata.json b/apps/alarm/metadata.json index 33312beb6..c062b030d 100644 --- a/apps/alarm/metadata.json +++ b/apps/alarm/metadata.json @@ -2,7 +2,7 @@ "id": "alarm", "name": "Alarms & Timers", "shortName": "Alarms", - "version": "0.25", + "version": "0.26", "description": "Set alarms and timers on your Bangle", "icon": "app.png", "tags": "tool,alarm,widget", diff --git a/apps/hrmaccevents/metadata.json b/apps/hrmaccevents/metadata.json index de59dceac..7207f685a 100644 --- a/apps/hrmaccevents/metadata.json +++ b/apps/hrmaccevents/metadata.json @@ -3,7 +3,7 @@ "name": "HRM Accelerometer event recorder", "shortName": "HRM ACC recorder", "version": "0.01", - "type": "ram", + "type": "RAM", "description": "Record HRM and accelerometer events in high resolution to CSV files in your browser", "icon": "app.png", "tags": "debug", diff --git a/apps/ios/boot.js b/apps/ios/boot.js index a3b23a79d..5ea7550eb 100644 --- a/apps/ios/boot.js +++ b/apps/ios/boot.js @@ -115,7 +115,23 @@ E.on('notify',msg=>{ // could also use NRF.ancsGetAppInfo(msg.appId) here }; 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)); //if (appNames[msg.appId]) msg.a diff --git a/apps/kbtouch/ChangeLog b/apps/kbtouch/ChangeLog index 5560f00bc..17e824c00 100644 --- a/apps/kbtouch/ChangeLog +++ b/apps/kbtouch/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Introduced settings to customize the layout and functionality of the keyboard. diff --git a/apps/kbtouch/README.md b/apps/kbtouch/README.md index 513ba9239..6bd0337a8 100644 --- a/apps/kbtouch/README.md +++ b/apps/kbtouch/README.md @@ -2,6 +2,17 @@ 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 In your app's metadata, add: diff --git a/apps/kbtouch/lib.js b/apps/kbtouch/lib.js index 3dfdce00c..db90440b9 100644 --- a/apps/kbtouch/lib.js +++ b/apps/kbtouch/lib.js @@ -69,13 +69,24 @@ var KEYEXTRA = [ String.fromCharCode(27,91,53,126), // 0x84 page up 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 const R = Bangle.appRect; var kbx = 0, kby = 0, kbdx = 0, kbdy = 0, kbShift = false, flashToggle = false; -const PX=12, PY=16, DRAGSCALE=24; -var xoff = 3, yoff = g.getHeight()-PY*4; +const PX=12, PY=16, DRAGSCALE=settings.speedScaling; +var xoff = 3, yoff = g.getHeight()-PY*(4+5*settings.offsetKeyboard); function draw() { + "ram"; var map = kbShift ? KEYMAPUPPER : KEYMAPLOWER; //g.drawImage(KEYIMG,0,yoff); g.reset().setFont("6x8:2"); @@ -88,9 +99,9 @@ function draw() { g.drawString(map[1],xoff,yoff+PY); g.drawString(map[2],xoff,yoff+PY*2); g.drawString(map[3],xoff,yoff+PY*3); - var l = g.setFont("6x8:4").wrapString(text+(flashToggle?"_":" "), R.w-8); - if (l.length>2) l=l.slice(-2); - g.drawString(l.join("\n"),R.x+4,R.y+4); + var l = g.setFont(settings.textSize ? "6x8:4":"6x8:2").wrapString(text+(flashToggle?"_":" "), R.w-8); + 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 +82*settings.offsetKeyboard); g.flip(); } @@ -104,24 +115,49 @@ function draw() { return new Promise((resolve,reject) => { Bangle.setUI({mode:"custom", drag:e=>{ - kbdx += e.dx; - kbdy += e.dy; - var dx = Math.round(kbdx/DRAGSCALE), dy = Math.round(kbdy/DRAGSCALE); - kbdx -= dx*DRAGSCALE; - kbdy -= dy*DRAGSCALE; - if (dx || dy) { - kbx = (kbx+dx+15)%15; - kby = (kby+dy+4)%4; + 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; + kbdy += e.dy; + var dx = Math.round(kbdx/DRAGSCALE), dy = Math.round(kbdy/DRAGSCALE); + kbdx -= dx*DRAGSCALE; + kbdy -= dy*DRAGSCALE; + if (dx || dy) { + if (settings.loopAround) { + kbx = (kbx+dx+15)%15; + kby = (kby+dy+4)%4; + } else { + kbx = Math.max(Math.min((kbx+dx),13),0); + kby = Math.max(Math.min((kby+dy),3),0); + } + } + } + draw(); + + if (!e.b && e.y>Bangle.appRect.y && 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(); } },touch:()=>{ - 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(); + 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:()=>{ clearInterval(flashInterval); Bangle.setUI(); diff --git a/apps/kbtouch/metadata.json b/apps/kbtouch/metadata.json index da8b6c3c6..f6d6d5228 100644 --- a/apps/kbtouch/metadata.json +++ b/apps/kbtouch/metadata.json @@ -1,6 +1,6 @@ { "id": "kbtouch", "name": "Touch keyboard", - "version":"0.01", + "version":"0.02", "description": "A library for text input via onscreen keyboard", "icon": "app.png", "type":"textinput", @@ -9,6 +9,7 @@ "screenshots": [{"url":"screenshot.png"}], "readme": "README.md", "storage": [ - {"name":"textinput","url":"lib.js"} + {"name":"textinput","url":"lib.js"}, + {"name":"kbtouch.settings.js","url":"settings.js"} ] } diff --git a/apps/kbtouch/settings.js b/apps/kbtouch/settings.js new file mode 100644 index 000000000..871cc5d32 --- /dev/null +++ b/apps/kbtouch/settings.js @@ -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); +}) diff --git a/apps/menusmall/metadata.json b/apps/menusmall/metadata.json index aafb7da28..51ab825bd 100644 --- a/apps/menusmall/metadata.json +++ b/apps/menusmall/metadata.json @@ -4,7 +4,7 @@ "version": "0.02", "description": "Replace Bangle.js 2's menus with a version that contains smaller text", "icon": "app.png", - "type": "boot", + "type": "bootloader", "tags": "system", "supports": ["BANGLEJS2"], "storage": [ diff --git a/apps/menuwheel/metadata.json b/apps/menuwheel/metadata.json index 1ad042344..5f49b640c 100644 --- a/apps/menuwheel/metadata.json +++ b/apps/menuwheel/metadata.json @@ -9,7 +9,7 @@ {"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"} ], - "type": "boot", + "type": "bootloader", "tags": "system", "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ diff --git a/apps/mmind/mmind.info b/apps/mmind/mmind.info index 2e79822b1..b4b822508 100644 --- a/apps/mmind/mmind.info +++ b/apps/mmind/mmind.info @@ -5,7 +5,7 @@ "icon": "mmind.png", "version":"0.01", "description": "This is the classic game for masterminds", - "type": "game", + "type": "app", "tags": "mastermind, game, classic", "readme":"README.md", "supports": ["BANGLEJS2"], diff --git a/apps/mysticdock/metadata.json b/apps/mysticdock/metadata.json index 54ebedd93..2775b0b72 100644 --- a/apps/mysticdock/metadata.json +++ b/apps/mysticdock/metadata.json @@ -4,7 +4,7 @@ "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.", "icon": "mystic-dock.png", - "type": "dock", + "type": "app", "tags": "dock", "supports": ["BANGLEJS"], "readme": "README.md", diff --git a/apps/openwind/ChangeLog b/apps/openwind/ChangeLog index 5560f00bc..1e5f791b2 100644 --- a/apps/openwind/ChangeLog +++ b/apps/openwind/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Fix true wind computation, add swipe gesture to pause GPS diff --git a/apps/openwind/README.md b/apps/openwind/README.md index 1df7ea158..c03ec1401 100644 --- a/apps/openwind/README.md +++ b/apps/openwind/README.md @@ -14,7 +14,9 @@ additionally displayed in red. In this mode, the speed over ground in knots is ## 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 * Mounting angle: mounting relative to the boat of the wind instrument (in degrees) diff --git a/apps/openwind/app.js b/apps/openwind/app.js index b1c8fea4b..db67804f3 100644 --- a/apps/openwind/app.js +++ b/apps/openwind/app.js @@ -1,16 +1,20 @@ OW_CHAR_UUID = '0000cc91-0000-1000-8000-00805f9b34fb'; require("Font7x11Numeric7Seg").add(Graphics); -gatt = {}; -cx = g.getWidth()/2; -cy = 24+(g.getHeight()-24)/2; -w = (g.getWidth()-24)/2; - -gps_course = { spd: 0 }; +var gatt = {}; +var cx = g.getWidth()/2; +var cy = 24+(g.getHeight()-24)/2; +var w = (g.getWidth()-24)/2; +var y1 = 24; +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) || {}; -i = 0; -hullpoly = []; +var pause_gps = false; + +var i = 0; +var hullpoly = []; 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++] = cy - y*w*0.7; @@ -22,21 +26,22 @@ for (y=1; y>=-1; y-=0.1) { function wind_updated(ev) { 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; -// console.log(awa, aws); + //console.log(awa, aws); if (gps_course.spd > 0) { - wv = { // wind vector (in fixed reference frame) - lon: Math.sin(Math.PI*(gps_course.course+awa)/180)*aws, - lat: Math.cos(Math.PI*(gps_course.course+awa)/180)*aws + wv = { // wind vector (in "earth" reference frame) + vlon: Math.sin(Math.PI*(gps_course.course+(awa+180))/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 }; - tws = Math.sqrt(Math.pow(twv.lon,2)+Math.pow(twv.lat, 2)); - twa = Math.atan2(twv.lat, twv.lon)*180/Math.PI-gps_course.course; + twv = { vlon: wv.vlon+gps_course.vlon, vlat: wv.vlat+gps_course.vlat }; + tws = Math.sqrt(Math.pow(twv.vlon,2)+Math.pow(twv.vlat, 2)); + twa = 180+Math.atan2(twv.vlon, twv.vlat)*180/Math.PI-gps_course.course; if (twa<0) twa += 360; if (twa>360) twa -=360; } - else { + else { tws = -1; twa = 0; } @@ -57,34 +62,37 @@ function draw_compass(awa, aws, twa, tws) { 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.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); g.setColor(0, 1, 0).setFont("7x11Numeric7Seg",w*0.06); g.setFontAlign(0, 0, 0).drawString(aws.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') { - 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); + if (!pause_gps) { + if (tws>0) g.setColor(1, 0, 0).drawString(tws.toFixed(1), cx, cy+0.32*w); + if (settings.truewind && gps_course.spd!=-1) { + 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); + } } + if (pause_gps) g.setColor("#f00").drawImage(atob("DAwBEAKARAKQE4DwHkPqPRGKAEAA"),g.getWidth()-15, g.getHeight()-15); } function parseDevice(d) { device = d; console.log("Found device"); - device.gatt.connect().then(function(ga) { - console.log("Connected"); - gatt = ga; - return ga.getPrimaryService("cc90"); -}).then(function(s) { - return s.getCharacteristic("cc91"); -}).then(function(c) { - c.on('characteristicvaluechanged', (event)=>wind_updated(event)); - return c.startNotifications(); -}).then(function() { - console.log("Done!"); -}).catch(function(e) { - console.log("ERROR"+e); -});} + device.gatt.connect().then(function(ga) { + console.log("Connected"); + gatt = ga; + return ga.getPrimaryService("cc90"); + }).then(function(s) { + return s.getCharacteristic("cc91"); + }).then(function(c) { + c.on('characteristicvaluechanged', (event)=>wind_updated(event)); + return c.startNotifications(); + }).then(function() { + console.log("Done!"); + }).catch(function(e) { + console.log("ERROR"+e); + });} function connection_setup() { NRF.setScan(); @@ -96,8 +104,10 @@ if (settings.truewind) { 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 gps_course = - { lon: Math.sin(Math.PI*fix.course/180)*fix.speed/1.852, - lat: Math.cos(Math.PI*fix.course/180)*fix.speed/1.852, + { vlon: Math.sin(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, course: fix.course }; @@ -107,6 +117,20 @@ if (settings.truewind) { 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.drawWidgets(); draw_compass(0, 0, 0, 0); diff --git a/apps/openwind/metadata.json b/apps/openwind/metadata.json index 9229f7f25..43961cc44 100644 --- a/apps/openwind/metadata.json +++ b/apps/openwind/metadata.json @@ -1,7 +1,7 @@ { "id": "openwind", "name": "OpenWind", "shortName":"OpenWind", - "version":"0.01", + "version":"0.02", "description": "OpenWind", "icon": "openwind.png", "readme": "README.md", diff --git a/apps/promenu/metadata.json b/apps/promenu/metadata.json index 443809004..e0124467a 100644 --- a/apps/promenu/metadata.json +++ b/apps/promenu/metadata.json @@ -4,7 +4,7 @@ "version": "0.02", "description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.", "icon": "icon.png", - "type": "boot", + "type": "bootloader", "tags": "system", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", diff --git a/apps/quicklaunch/ChangeLog b/apps/quicklaunch/ChangeLog index ec66c5568..ae1d4a848 100644 --- a/apps/quicklaunch/ChangeLog +++ b/apps/quicklaunch/ChangeLog @@ -1 +1,2 @@ 0.01: Initial version +0.02: Moved settings from launcher to settings->apps menu diff --git a/apps/quicklaunch/app-icon.js b/apps/quicklaunch/app-icon.js deleted file mode 100644 index 14ae94823..000000000 --- a/apps/quicklaunch/app-icon.js +++ /dev/null @@ -1 +0,0 @@ -require("heatshrink").decompress(atob("kMigILIgPAAYMD/ADBwcGhkAwM5wcA/+2//Av/Rn/giFoyFggkUrFggEKlAkCiApCx+AAYNGoADBkU4AYMQj4DBvEICANkAoIPBgE2B4MAiMAH4MAwECAYNALYUgBIISCHYMYAoQWBAIMEgAYBAIMBwEDDQNgDwUf/4eBg4DCAA4")) diff --git a/apps/quicklaunch/metadata.json b/apps/quicklaunch/metadata.json index 6411d1a5f..49eafdd35 100644 --- a/apps/quicklaunch/metadata.json +++ b/apps/quicklaunch/metadata.json @@ -1,14 +1,15 @@ -{ "id": "quicklaunch", +{ + "id": "quicklaunch", "name": "Quick Launch", "icon": "app.png", - "version":"0.01", - "description": "Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice.", + "version":"0.02", + "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", "supports": ["BANGLEJS2"], "storage": [ - {"name":"quicklaunch.app.js","url":"app.js"}, - {"name":"quicklaunch.boot.js","url":"boot.js"}, - {"name":"quicklaunch.img","url":"app-icon.js","evaluate":true} + {"name":"quicklaunch.settings.js","url":"settings.js"}, + {"name":"quicklaunch.boot.js","url":"boot.js"} ], "data": [{"name":"quicklaunch.json"}] } diff --git a/apps/quicklaunch/app.js b/apps/quicklaunch/settings.js similarity index 99% rename from apps/quicklaunch/app.js rename to apps/quicklaunch/settings.js index f2b749e3e..ac4cc5805 100644 --- a/apps/quicklaunch/app.js +++ b/apps/quicklaunch/settings.js @@ -1,3 +1,4 @@ +(function(back) { 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)); @@ -118,3 +119,4 @@ apps.forEach((a)=>{ }); showMainMenu(); +}); \ No newline at end of file diff --git a/apps/scicalc/ChangeLog b/apps/scicalc/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/scicalc/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/scicalc/README.md b/apps/scicalc/README.md new file mode 100644 index 000000000..740b4216b --- /dev/null +++ b/apps/scicalc/README.md @@ -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) diff --git a/apps/scicalc/app-icon.js b/apps/scicalc/app-icon.js new file mode 100644 index 000000000..b8363e6ee --- /dev/null +++ b/apps/scicalc/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AJioAaF1wwSFzowRCQUZo4AWjIvVFy4ABF/4vXyGQAYov/R+sZFy8ZF6oAcF/4vvi4AeF/4SCjseAAMdAx8MAAYvVEAQABAx4v/R/TvvF96PUg8cAAMHd9QuCAAIv/R+rvvF96Pvd94vvR97vvF96Pvd94vvR97vsGDwuQGDouSAH4A/AGwA==")) diff --git a/apps/scicalc/app.js b/apps/scicalc/app.js new file mode 100644 index 000000000..5d914d0c5 --- /dev/null +++ b/apps/scicalc/app.js @@ -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); diff --git a/apps/scicalc/metadata.json b/apps/scicalc/metadata.json new file mode 100644 index 000000000..beda619e2 --- /dev/null +++ b/apps/scicalc/metadata.json @@ -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} + ] +} diff --git a/apps/scicalc/scicalc.png b/apps/scicalc/scicalc.png new file mode 100644 index 000000000..b5aa6ff7e Binary files /dev/null and b/apps/scicalc/scicalc.png differ diff --git a/apps/scicalc/scicalc_screenshot1.png b/apps/scicalc/scicalc_screenshot1.png new file mode 100644 index 000000000..7f9d46860 Binary files /dev/null and b/apps/scicalc/scicalc_screenshot1.png differ diff --git a/apps/scicalc/scicalc_screenshot2.png b/apps/scicalc/scicalc_screenshot2.png new file mode 100644 index 000000000..795d922e8 Binary files /dev/null and b/apps/scicalc/scicalc_screenshot2.png differ diff --git a/apps/scicalc/scicalc_screenshot3.png b/apps/scicalc/scicalc_screenshot3.png new file mode 100644 index 000000000..9319157ba Binary files /dev/null and b/apps/scicalc/scicalc_screenshot3.png differ diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index fe259827c..bfd32a130 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -46,3 +46,5 @@ 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.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 diff --git a/apps/setting/README.md b/apps/setting/README.md index 42e3939fb..451b48c06 100644 --- a/apps/setting/README.md +++ b/apps/setting/README.md @@ -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 * **Vibration** enable/disable the vibration motor * **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 -* **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. * **Theme** Adjust the colour scheme * **Utils** Utilities - including resetting settings (see below) @@ -35,11 +35,15 @@ 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. * **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 is a hint to apps and widgets that you do not want to be disturbed. +Quiet Mode is a hint to apps and widgets that you do not want to be disturbed. The exact effects depend on the app. In general the watch will not wake up by itself, but will still respond to button presses. * **Quiet Mode** diff --git a/apps/setting/metadata.json b/apps/setting/metadata.json index 750752bd7..85dddf9db 100644 --- a/apps/setting/metadata.json +++ b/apps/setting/metadata.json @@ -1,7 +1,7 @@ { "id": "setting", "name": "Settings", - "version": "0.43", + "version": "0.44", "description": "A menu for setting up Bangle.js", "icon": "settings.png", "tags": "tool,system", diff --git a/apps/setting/settings.js b/apps/setting/settings.js index afc7e23c8..9b5bdae68 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -37,18 +37,19 @@ function internalToG(u) { function resetSettings() { settings = { - ble: true, // Bluetooth enabled by default - blerepl: true, // Is REPL on Bluetooth - can Espruino IDE be used? - log: false, // Do log messages appear on screen? - quiet: 0, // quiet mode: 0: off, 1: priority only, 2: total silence - timeout: 10, // Default LCD timeout in seconds - vibrate: true, // Vibration enabled by default. App must support - beep: BANGLEJS2?true:"vib", // Beep enabled by default. App must support - timezone: 0, // Set the timezone for the device - HID: false, // BLE HID mode, off by default - clock: null, // a string for the default clock's name - "12hour" : false, // 12 or 24 hour clock? - brightness: 1, // LCD brightness from 0 to 1 + ble: true, // Bluetooth enabled by default + blerepl: true, // Is REPL on Bluetooth - can Espruino IDE be used? + log: false, // Do log messages appear on screen? + quiet: 0, // quiet mode: 0: off, 1: priority only, 2: total silence + timeout: 10, // Default LCD timeout in seconds + vibrate: true, // Vibration enabled by default. App must support + beep: BANGLEJS2 ? true : "vib", // Beep enabled by default. App must support + timezone: 0, // Set the timezone for the device + HID: false, // BLE HID mode, off by default + clock: null, // a string for the default clock's name + "12hour" : false, // 12 or 24 hour clock? + firstDayOfWeek: 0, // 0 -> Sunday (default), 1 -> Monday + brightness: 1, // LCD brightness from 0 to 1 // welcomed : undefined/true (whether welcome app should show) options: { wakeOnBTN1: true, @@ -94,7 +95,7 @@ function showSystemMenu() { /*LANG*/'LCD': ()=>showLCDMenu(), /*LANG*/'Locale': ()=>showLocaleMenu(), /*LANG*/'Select Clock': ()=>showClockMenu(), - /*LANG*/'Set Time': ()=>showSetTimeMenu() + /*LANG*/'Date & Time': ()=>showSetTimeMenu() }; return E.showMenu(mainmenu); @@ -478,6 +479,7 @@ function showLocaleMenu() { '< Back': ()=>showSystemMenu(), /*LANG*/'Time Zone': { value: settings.timezone, + format: v => (v > 0 ? "+" : "") + v, min: -11, max: 13, step: 0.5, @@ -486,13 +488,23 @@ function showLocaleMenu() { updateSettings(); } }, - /*LANG*/'Clock Style': { + /*LANG*/'Time Format': { value: !!settings["12hour"], - format: v => v ? "12hr" : "24hr", + format: v => v ? "12h" : "24h", onchange: v => { settings["12hour"] = v; 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); @@ -606,11 +618,34 @@ function showClockMenu() { function showSetTimeMenu() { d = new Date(); const timemenu = { - '': { 'title': /*LANG*/'Set Time' }, + '': { 'title': /*LANG*/'Date & Time' }, '< Back': function () { setTime(d.getTime() / 1000); 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': { value: d.getHours(), onchange: function (v) { @@ -631,28 +666,6 @@ function showSetTimeMenu() { this.value = (v+60)%60; 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); diff --git a/apps/smpltmr/ChangeLog b/apps/smpltmr/ChangeLog index 07afedd21..bf128e2fb 100644 --- a/apps/smpltmr/ChangeLog +++ b/apps/smpltmr/ChangeLog @@ -1 +1,2 @@ -0.01: Release \ No newline at end of file +0.01: Release +0.02: Rewrite with new interface \ No newline at end of file diff --git a/apps/smpltmr/README.md b/apps/smpltmr/README.md index 1296166e2..eeb48d338 100644 --- a/apps/smpltmr/README.md +++ b/apps/smpltmr/README.md @@ -1,21 +1,18 @@ # Simple Timer -A simple app to set a timer quickly. Simply tab on top/bottom/left/right -to select the minutes and tab in the middle of the screen to start/stop -the timer. Note that this timer depends on qalarm. +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. -# Overview -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. +This app uses the `sched` library, which allows the timer to continue to run in the background when this app is closed. -![](description.png) +![](screenshot_1.png) +![](screenshot_2.png) +![](screenshot_3.png) +![](screenshot_4.png) - -# Creator +# Creators [David Peer](https://github.com/peerdavid) +[Sir Indy](https://github.com/sir-indy) # Thanks to... Time icon created by CreativeCons - Flaticon \ No newline at end of file diff --git a/apps/smpltmr/app.js b/apps/smpltmr/app.js index eb01e27d0..4e95d3a30 100644 --- a/apps/smpltmr/app.js +++ b/apps/smpltmr/app.js @@ -3,122 +3,188 @@ * * Creator: David Peer * Date: 02/2022 + * + * Modified: Sir Indy + * Date: 05/2022 */ -Bangle.loadWidgets(); - - -const alarm = require("sched"); - +const Layout = require("Layout"); +const alarm = require("sched") 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 - -function isTimerEnabled(){ - var alarmObj = alarm.getAlarm(TIMER_IDX); - if(alarmObj===undefined || !alarmObj.on){ - return false; +const secondsToTime = (s) => new Object({h:Math.floor((s/3600) % 24), m:Math.floor((s/60) % 60), s:Math.floor(s % 60)}); +const clamp = (num, min, max) => Math.min(Math.max(num, min), max); +function formatTime(s) { + var t = secondsToTime(s); + 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; } -function getTimerMin(){ - var alarmObj = alarm.getAlarm(TIMER_IDX); - return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000)); +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 setTimer(minutes){ +function onTouch(button, xy) { + if (xy.y > (timePickerLayout.btnStart.y||timerLayout.btnStart.y)) { + 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 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, { - // msg : "Simple Timer", - timer : minutes*60*1000, + vibrate : ".-.-", + hidden: true, + timer : seconds * 1000 }); 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.reload(); + g.clearRect(Bangle.appRect); + timePickerLayout.render(); + updateTimePicker(); } -setWatch(_=>load(), BTN1); -function draw(){ - g.clear(1); - Bangle.drawWidgets(); - - if (interval) { - clearInterval(interval); - } - interval = undefined; - - // Write time - g.setFontAlign(0, 0, 0); - g.setFont("Vector", 32).setFontAlign(0,-1); - - var started = isTimerEnabled(); - var text = minutes + " min."; - if(started){ - var min = getTimerMin(); - text = min + " min."; - } - - var rectWidth = parseInt(g.stringWidth(text) / 2); - - if(started){ - interval = setInterval(draw, 1000); - 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(); +var timePickerLayout = new Layout({ + type:"v", c: [ + {type:undefined, height:2}, + {type:"h", c: [ + {type:"v", width:g.getWidth()/3, c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Hours"}, + {type:"img", pad:8, src:imgArrow}, + {type:"txt", font:"20%", label:"00", id:"hours", filly:1, fillx:1}, + {type:"img", pad:8, src:imgArrow, r:2} + ]}, + {type:"v", width:g.getWidth()/3, c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Minutes"}, + {type:"img", pad:8, src:imgArrow}, + {type:"txt", font:"20%", label:"00", id:"mins", filly:1, fillx:1}, + {type:"img", pad:8, src:imgArrow, r:2} + ]}, + {type:"v", width:g.getWidth()/3, c: [ + {type:"txt", font:"6x8", label:/*LANG*/"Seconds"}, + {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} + ]}, + ]}, + {type:"btn", src:imgPlay, id:"btnStart", fillx:1 } + ], filly:1 }); -g.reset(); -draw(); \ No newline at end of file +var timerLayout = new Layout({ + 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(); +} diff --git a/apps/smpltmr/description.png b/apps/smpltmr/description.png deleted file mode 100644 index 1286d1ab9..000000000 Binary files a/apps/smpltmr/description.png and /dev/null differ diff --git a/apps/smpltmr/metadata.json b/apps/smpltmr/metadata.json index 06bad962d..cb1ef6eab 100644 --- a/apps/smpltmr/metadata.json +++ b/apps/smpltmr/metadata.json @@ -2,13 +2,13 @@ "id": "smpltmr", "name": "Simple Timer", "shortName": "Simple Timer", - "version": "0.01", + "version": "0.02", "description": "A very simple app to start a timer.", "icon": "app.png", - "tags": "tool", + "tags": "tool,alarm,timer", "dependencies": {"scheduler":"type"}, "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", "storage": [ {"name":"smpltmr.app.js","url":"app.js"}, diff --git a/apps/smpltmr/screenshot.png b/apps/smpltmr/screenshot.png deleted file mode 100644 index eff94475c..000000000 Binary files a/apps/smpltmr/screenshot.png and /dev/null differ diff --git a/apps/smpltmr/screenshot_1.png b/apps/smpltmr/screenshot_1.png new file mode 100644 index 000000000..54eb9d20c Binary files /dev/null and b/apps/smpltmr/screenshot_1.png differ diff --git a/apps/smpltmr/screenshot_2.png b/apps/smpltmr/screenshot_2.png index 7b5dc9a3d..fb0145f17 100644 Binary files a/apps/smpltmr/screenshot_2.png and b/apps/smpltmr/screenshot_2.png differ diff --git a/apps/smpltmr/screenshot_3.png b/apps/smpltmr/screenshot_3.png new file mode 100644 index 000000000..efa10d9c1 Binary files /dev/null and b/apps/smpltmr/screenshot_3.png differ diff --git a/apps/smpltmr/screenshot_4.png b/apps/smpltmr/screenshot_4.png new file mode 100644 index 000000000..c0f984378 Binary files /dev/null and b/apps/smpltmr/screenshot_4.png differ diff --git a/apps/swp2clk/metadata.json b/apps/swp2clk/metadata.json index aa95a6473..8b0cce2d8 100644 --- a/apps/swp2clk/metadata.json +++ b/apps/swp2clk/metadata.json @@ -5,7 +5,7 @@ "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.", "icon": "app.png", - "type": "boot", + "type": "bootloader", "tags": "tools", "supports": ["BANGLEJS2"], "readme": "README.md", diff --git a/apps/widbaroalarm/ChangeLog b/apps/widbaroalarm/ChangeLog index c12cc0d65..5786741c7 100644 --- a/apps/widbaroalarm/ChangeLog +++ b/apps/widbaroalarm/ChangeLog @@ -1,2 +1,3 @@ 0.01: Initial version 0.02: Do not warn multiple times for the same exceedance +0.03: Fix crash diff --git a/apps/widbaroalarm/metadata.json b/apps/widbaroalarm/metadata.json index 9c58a41ab..134f03623 100644 --- a/apps/widbaroalarm/metadata.json +++ b/apps/widbaroalarm/metadata.json @@ -2,7 +2,7 @@ "id": "widbaroalarm", "name": "Barometer Alarm Widget", "shortName": "Barometer Alarm", - "version": "0.02", + "version": "0.03", "description": "A widget that can alarm on when the pressure reaches defined thresholds.", "icon": "widget.png", "type": "widget", diff --git a/apps/widbaroalarm/widget.js b/apps/widbaroalarm/widget.js index 5d62156eb..2745db8ad 100644 --- a/apps/widbaroalarm/widget.js +++ b/apps/widbaroalarm/widget.js @@ -104,7 +104,7 @@ saveSetting("lastHighWarningTs", 0); } - if (!alreadyWarned) { + if (history3.length > 0 && !alreadyWarned) { // 3h change detection const drop3halarm = setting("drop3halarm"); const raise3halarm = setting("raise3halarm"); diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index 8fdb5a4d2..850b793f4 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -65,6 +65,7 @@ const APP_KEYS = [ const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite', 'supports']; const DATA_KEYS = ['name', 'wildcard', 'storageFile', 'url', 'content', 'evaluate']; 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 VALID_DUPLICATES = [ '.tfmodel', '.tfnames' ]; 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`); 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.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`); else { app.supports.forEach(dev => { @@ -135,6 +138,9 @@ apps.forEach((app,appIdx) => { Object.keys(app.dependencies).forEach(dependency => { if (!["type","app"].includes(app.dependencies[dependency])) 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 ERROR(`App ${app.id} 'dependencies' must be an object`); diff --git a/core b/core index 6fc78fc39..32d01b5b3 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 6fc78fc39531a43148ae8d515efaeff9404d1daf +Subproject commit 32d01b5b3d8e013ca0364671e2352b7b0dd48bb4 diff --git a/loader.js b/loader.js index ee7b584a2..42db3f430 100644 --- a/loader.js +++ b/loader.js @@ -25,7 +25,7 @@ DEVICEINFO = DEVICEINFO.filter(x=>x.id.startsWith("BANGLEJS")); // Set up source code URL (function() { 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]; Const.APP_SOURCECODE_URL = `https://github.com/${username}/BangleApps/tree/master/apps`; })(); diff --git a/modules/Layout.js b/modules/Layout.js index 620817673..c978c611b 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -1,18 +1,13 @@ /* 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. - Usage: - ``` var Layout = require("Layout"); var layout = new Layout( layoutObject, options ) layout.render(optionalObject); ``` - For example: - ``` var Layout = require("Layout"); var layout = new Layout( { @@ -24,23 +19,22 @@ var layout = new Layout( { g.clear(); layout.render(); ``` - - layoutObject has: - * A `type` field of: * `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` 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. - optional `scale` specifies if image should be scaled up or not * `"custom"` - a custom block where `render(layoutObj)` is called to render * `"h"` - Horizontal 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 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` and `fillx`/`filly` to be set. Not compatible with text rotation. * 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 `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 - options is an object containing: - * `lazy` - a boolean specifying whether to enable automatic lazy rendering * `btns` - array of objects containing: * `label` - the text on the button * `cb` - a callback function * `cbl` - a callback function for long presses * `back` - a callback function, passed as `back` into Bangle.setUI - 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. - Once `layout.update()` is called, the following fields are added to each object: - * `x` and `y` for the top left position * `w` and `h` for the width and height * `_w` and `_h` for the **minimum** width and height - - Other functions: - * `layout.update()` - update positions of everything if contents have changed * `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.forgetLazyState()` - if lazy rendering is enabled, makes the next call to `render()` perform a full re-render - */ @@ -259,12 +244,22 @@ Layout.prototype.render = function (l) { x,y+h-5, x,y+4 ], 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.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)); - else g.setFont("6x8",2).setFontAlign(0,0,l.r).drawString(l.label,l.x+l.w/2,l.y+l.h/2); + if (l.src) g.setBgColor(bg).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)} + ); + 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){ - 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){ l.render(l); },"h":function(l) { l.c.forEach(render); }, @@ -365,7 +360,9 @@ Layout.prototype.update = function() { l._w = m.width; l._h = m.height; } }, "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._w = 20 + m.width; }, "img": function(l) { diff --git a/modules/date_utils.js b/modules/date_utils.js index da0ed24d9..7239d4f1f 100644 --- a/modules/date_utils.js +++ b/modules/date_utils.js @@ -1,39 +1,65 @@ -/* Utility functions that use the 'locale' module so can produce text -in the currently selected language. */ +// module "date_utils" +// +// 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" - short==1 -> "Sun" -*/ -exports.getDOW = (dow, short) => require("locale").dow({getDay:()=>dow},short); - -/** Return the month (1=January) - 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; +/** + * @param {int} i The index of the day of the week (0 = Sunday) + * @param {int} abbreviated + * @returns The localized name of the i-th day of the week + */ +exports.dow = (i, abbreviated) => { + var dow = require("locale").dow(new Date(((i || 0) + 3.5) * 86400000), abbreviated).slice(0, (abbreviated == 2) ? 1 : 100); + return abbreviated == 2 ? dow.toUpperCase() : dow; } -/** Return all 12 months as an array ["January","February",...] - short==0/undefined -> ["January",... - short==1 -> ["Jan",... -*/ -exports.getMonths = (short) => { +/** + * @param {int} firstDayOfWeek 0/undefined -> Sunday, + * 1 -> Monday + * @param {int} abbreviated + * @returns All 7 days of the week (localized) as an array + */ +exports.dows = (firstDayOfWeek, abbreviated) => { + var dows = []; 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 = []; - for (var i=0;i<12;i++) - months.push(locale.month({getMonth:()=>i},short)); - return months; -} + var locale = require("locale"); + for (var i = 1; i <= 12; i++) { + 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; +}; diff --git a/tests/Layout/bin/runtest.sh b/tests/Layout/bin/runtest.sh index c85b3fe6c..e06dec86b 100755 --- a/tests/Layout/bin/runtest.sh +++ b/tests/Layout/bin/runtest.sh @@ -19,7 +19,7 @@ SRCBMP=$SRCDIR/`basename $SRCJS .js`.bmp echo "TEST $SRCJS ($SRCBMP)" 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 cat $SRCJS >> $TESTJS || exit 1 echo 'layout.render()' >> $TESTJS diff --git a/tests/Layout/tests/buttons_1_bangle1.js b/tests/Layout/tests/buttons_1_bangle1.js index fb6fb29fa..481f09df3 100644 --- a/tests/Layout/tests/buttons_1_bangle1.js +++ b/tests/Layout/tests/buttons_1_bangle1.js @@ -1,6 +1,7 @@ var BTN2 = 1, BTN3=2; process.env = process.env;process.env.HWVERSION=1; 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: [ {type:"txt", font:"6x8", label:"A test"}, diff --git a/tests/Layout/tests/buttons_3_bangle1.js b/tests/Layout/tests/buttons_3_bangle1.js index c8346f449..2d5fbea9d 100644 --- a/tests/Layout/tests/buttons_3_bangle1.js +++ b/tests/Layout/tests/buttons_3_bangle1.js @@ -1,6 +1,7 @@ var BTN2 = 1, BTN3=2; process.env = process.env;process.env.HWVERSION=1; 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: [ {type:"txt", font:"6x8", label:"A test"}, diff --git a/tests/Layout/tests/buttons_osd_bangle1.js b/tests/Layout/tests/buttons_osd_bangle1.js index 108cb62b0..55656ef33 100644 --- a/tests/Layout/tests/buttons_osd_bangle1.js +++ b/tests/Layout/tests/buttons_osd_bangle1.js @@ -1,6 +1,7 @@ var BTN2 = 1, BTN3=2; process.env = process.env;process.env.HWVERSION=1; 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 the side buttons into 'soft' buttons and then use the physical diff --git a/tests/Layout/tests/padding.bmp b/tests/Layout/tests/padding.bmp index 84ae4dc1b..506bb014e 100644 Binary files a/tests/Layout/tests/padding.bmp and b/tests/Layout/tests/padding.bmp differ diff --git a/tests/Layout/tests/padding_with_fill.bmp b/tests/Layout/tests/padding_with_fill.bmp index 9f82ed09f..92eccace2 100644 Binary files a/tests/Layout/tests/padding_with_fill.bmp and b/tests/Layout/tests/padding_with_fill.bmp differ