diff --git a/.gitignore b/.gitignore index f4588ac6f..7687a770a 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ _site .owncloudsync.log Desktop.ini .sync_*.db* +*.swp diff --git a/README.md b/README.md index 1058787bb..fed13a358 100644 --- a/README.md +++ b/README.md @@ -282,8 +282,11 @@ and which gives information about the app for the Launcher. "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' - "dependencies" : { "messageicons":"module" } // optional, depend on a specific library to be used with 'require' + "dependencies" : { "messageicons":"module" } // optional, depend on a specific library to be used with 'require' - see provides_modules + "dependencies" : { "message":"widget" } // optional, depend on a specific type of widget - see provides_widgets "provides_modules" : ["messageicons"] // optional, this app provides a module that can be used with 'require' + "provides_widgets" : ["battery"] // optional, this app provides a type of widget - 'alarm/battery/bluetooth/pedometer/message' + "default" : true, // set if an app is the default implementer of something (a widget/module/etc) "readme": "README.md", // if supplied, a link to a markdown-style text file // that contains more information about this app (usage, etc) // A 'Read more...' link will be added under the app diff --git a/apps/about/ChangeLog b/apps/about/ChangeLog index ffe9de081..e236e4b34 100644 --- a/apps/about/ChangeLog +++ b/apps/about/ChangeLog @@ -11,3 +11,4 @@ 0.11: Bangle.js2: New pixels, btn1 to exit 0.12: Actual pixels as of 29th Nov 2021 0.13: Bangle.js 2: Use setUI to add software back button +0.14: Add automatic translation of more strings diff --git a/apps/about/app-bangle1.js b/apps/about/app-bangle1.js index 28a292376..dd94c1e84 100644 --- a/apps/about/app-bangle1.js +++ b/apps/about/app-bangle1.js @@ -11,8 +11,8 @@ g.drawString("BANGLEJS.COM",120,y-4); } else { y=-(4+h); // small screen, start right at top } -g.drawString("Powered by Espruino",0,y+=4+h); -g.drawString("Version "+ENV.VERSION,0,y+=h); +g.drawString(/*LANG*/"Powered by Espruino",0,y+=4+h); +g.drawString(/*LANG*/"Version "+ENV.VERSION,0,y+=h); g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h); function getVersion(name,file) { var j = s.readJSON(file,1); @@ -24,9 +24,9 @@ getVersion("Launcher","launch.info"); getVersion("Settings","setting.info"); y+=h; -g.drawString(MEM.total+" JS Variables available",0,y+=h); -g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h); -if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",0,y+=h); +g.drawString(MEM.total+/*LANG*/" JS Variables available",0,y+=h); +g.drawString("Storage: "+(require("Storage").getFree()>>10)+/*LANG*/"k free",0,y+=h); +if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+/*LANG*/"k total",0,y+=h); if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h); g.setFontAlign(0,-1); g.flip(); diff --git a/apps/about/app-bangle2.js b/apps/about/app-bangle2.js index 471b0670f..ccffd183f 100644 --- a/apps/about/app-bangle2.js +++ b/apps/about/app-bangle2.js @@ -35,17 +35,17 @@ function drawInfo() { g.setFont("4x6").setFontAlign(0,0).drawString("BANGLEJS.COM",W-30,56); var h=8, y = 24-h; g.setFont("6x8").setFontAlign(-1,-1); - g.drawString("Powered by Espruino",0,y+=4+h); - g.drawString("Version "+ENV.VERSION,0,y+=h); + g.drawString(/*LANG*/"Powered by Espruino",0,y+=4+h); + g.drawString(/*LANG*/"Version "+ENV.VERSION,0,y+=h); g.drawString("Commit "+ENV.GIT_COMMIT,0,y+=h); getVersion("Bootloader","boot.info"); getVersion("Launcher","launch.info"); getVersion("Settings","setting.info"); - g.drawString(MEM.total+" JS Vars",0,y+=h); - g.drawString("Storage: "+(require("Storage").getFree()>>10)+"k free",0,y+=h); - if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+"k total",0,y+=h); + g.drawString(MEM.total+/*LANG*/" JS Vars",0,y+=h); + g.drawString("Storage: "+(require("Storage").getFree()>>10)+/*LANG*/"k free",0,y+=h); + if (ENV.STORAGE) g.drawString(" "+(ENV.STORAGE>>10)+/*LANG*/"k total",0,y+=h); if (ENV.SPIFLASH) g.drawString("SPI Flash: "+(ENV.SPIFLASH>>10)+"k",0,y+=h); imageTop = y+h; imgScroll = imgHeight-imageTop; diff --git a/apps/about/metadata.json b/apps/about/metadata.json index 648576576..52cd37b7d 100644 --- a/apps/about/metadata.json +++ b/apps/about/metadata.json @@ -1,7 +1,7 @@ { "id": "about", "name": "About", - "version": "0.13", + "version": "0.14", "description": "Bangle.js About page - showing software version, stats, and a collaborative mural from the Bangle.js KickStarter backers", "icon": "app.png", "tags": "tool,system", diff --git a/apps/activepedom/README.md b/apps/activepedom/README.md index ac32a1dd6..06ad280ee 100644 --- a/apps/activepedom/README.md +++ b/apps/activepedom/README.md @@ -1,6 +1,11 @@ # Active Pedometer + Pedometer that filters out arm movement and displays a step goal progress. +**Note:** Since creation of this app, Bangle.js's step counting algorithm has +improved significantly - and as a result the algorithm in this app (which + runs *on top* of Bangle.js's algorithm) may no longer be accurate. + I changed the step counting algorithm completely. Now every step is counted when in status 'active', if the time difference between two steps is not too short or too long. To get in 'active' mode, you have to reach the step threshold before the active timer runs out. @@ -9,6 +14,7 @@ When you reach the step threshold, the steps needed to reach the threshold are c Steps are saved to a datafile every 5 minutes. You can watch a graph using the app. ## Screenshots + * 600 steps ![](600.png) @@ -70,4 +76,4 @@ Steps are saved to a datafile every 5 minutes. You can watch a graph using the a ## Requests -If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/ \ No newline at end of file +If you have any feature requests, please post in this forum thread: http://forum.espruino.com/conversations/345754/ diff --git a/apps/activepedom/metadata.json b/apps/activepedom/metadata.json index 4deb7006d..81bafb573 100644 --- a/apps/activepedom/metadata.json +++ b/apps/activepedom/metadata.json @@ -3,7 +3,7 @@ "name": "Active Pedometer", "shortName": "Active Pedometer", "version": "0.09", - "description": "Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph.", + "description": "(NOT RECOMMENDED) Pedometer that filters out arm movement and displays a step goal progress. Steps are saved to a daily file and can be viewed as graph. The `Health` app now provides step logging and graphs.", "icon": "app.png", "tags": "outdoors,widget", "supports": ["BANGLEJS"], diff --git a/apps/agenda/ChangeLog b/apps/agenda/ChangeLog index 0a7916810..77e11c92e 100644 --- a/apps/agenda/ChangeLog +++ b/apps/agenda/ChangeLog @@ -4,4 +4,8 @@ 0.04: Added awareness of allDay field 0.05: Displaying calendar colour and name 0.06: Added clkinfo for clocks. -0.07: Clkinfo improvements. \ No newline at end of file +0.07: Clkinfo improvements. +0.08: Fix error in clkinfo (didn't require Storage & locale) + Fix clkinfo icon +0.09: Ensure Agenda supplies an image for clkinfo items +0.10: Update clock_info to avoid a redraw diff --git a/apps/agenda/agenda.clkinfo.js b/apps/agenda/agenda.clkinfo.js index 6c2ddb3da..7c89446a2 100644 --- a/apps/agenda/agenda.clkinfo.js +++ b/apps/agenda/agenda.clkinfo.js @@ -1,29 +1,29 @@ (function() { - var agendaItems = { - name: "Agenda", - img: atob("GBiBAf////////85z/AAAPAAAPgAAP////AAAPAAAPAAAPAAAOAAAeAAAeAAAcAAA8AAAoAABgAADP//+P//8PAAAPAAAPgAAf///w=="), - items: [] - }; + var agendaItems = { + name: "Agenda", + img: atob("GBiBAAAAAAAAAADGMA///w///wf//wAAAA///w///w///w///x///h///h///j///D///X//+f//8wAABwAADw///w///wf//gAAAA=="), + items: [] + }; + var locale = require("locale"); + var now = new Date(); + var agenda = require("Storage").readJSON("android.calendar.json") + .filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000) + .sort((a,b)=>a.timestamp - b.timestamp); - var now = new Date(); - var agenda = storage.readJSON("android.calendar.json") - .filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000) - .sort((a,b)=>a.timestamp - b.timestamp); + agenda.forEach((entry, i) => { - agenda.forEach((entry, i) => { + var title = entry.title.slice(0,12); + var date = new Date(entry.timestamp*1000); + var dateStr = locale.date(date).replace(/\d\d\d\d/,""); + dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : ""; - var title = entry.title.slice(0,18); - var date = new Date(entry.timestamp*1000); - var dateStr = locale.date(date).replace(/\d\d\d\d/,""); - dateStr += entry.durationInSeconds < 86400 ? "/ " + locale.time(date,1) : ""; + agendaItems.items.push({ + name: "Agenda "+i, + get: () => ({ text: title + "\n" + dateStr, img: agendaItems.img }), + show: function() {}, + hide: function () {} + }); + }); - agendaItems.items.push({ - name: null, - get: () => ({ text: title + "\n" + dateStr, img: null}), - show: function() { agendaItems.items[i].emit("redraw"); }, - hide: function () {} - }); - }); - - return agendaItems; -}) \ No newline at end of file + return agendaItems; +}) diff --git a/apps/agenda/metadata.json b/apps/agenda/metadata.json index 4436e143c..8253b36bc 100644 --- a/apps/agenda/metadata.json +++ b/apps/agenda/metadata.json @@ -1,7 +1,7 @@ { "id": "agenda", "name": "Agenda", - "version": "0.07", + "version": "0.10", "description": "Simple agenda", "icon": "agenda.png", "screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}], diff --git a/apps/aiclock/ChangeLog b/apps/aiclock/ChangeLog index 31c55aef1..fb5aed3e3 100644 --- a/apps/aiclock/ChangeLog +++ b/apps/aiclock/ChangeLog @@ -1,3 +1,5 @@ 0.01: New app! 0.02: Design improvements and fixes. -0.03: Indicate battery level through line occurrence. \ No newline at end of file +0.03: Indicate battery level through line occurrence. +0.04: Use widget_utils module. +0.05: Support for clkinfo. \ No newline at end of file diff --git a/apps/aiclock/README.md b/apps/aiclock/README.md index 9e23de3a6..31dd5aa29 100644 --- a/apps/aiclock/README.md +++ b/apps/aiclock/README.md @@ -10,7 +10,9 @@ The original output of stable diffusion is shown here: My implementation is shown below. Note that horizontal lines occur randomly, but the probability is correlated with the battery level. So if your screen contains only -a few lines its time to charge your bangle again ;) +a few lines its time to charge your bangle again ;) Also note that the upper text +implementes the clkinfo module and can be configured via touch left/right/up/down. +Touch at the center to trigger the selected action. ![](impl.png) diff --git a/apps/aiclock/aiclock.app.js b/apps/aiclock/aiclock.app.js index dbd053f2c..b5bb30b9d 100644 --- a/apps/aiclock/aiclock.app.js +++ b/apps/aiclock/aiclock.app.js @@ -1,6 +1,14 @@ -/** +/************************************************ * AI Clock */ + const storage = require('Storage'); + const clock_info = require("clock_info"); + + + + /************************************************ + * Assets + */ require("Font7x11Numeric7Seg").add(Graphics); Graphics.prototype.setFontGochiHand = function(scale) { // Actual height 27 (29 - 3) @@ -13,7 +21,7 @@ Graphics.prototype.setFontGochiHand = function(scale) { return this; } -/* +/************************************************ * Set some important constants such as width, height and center */ var W = g.getWidth(),R=W/2; @@ -21,6 +29,120 @@ var H = g.getHeight(); var cx = W/2; var cy = H/2; var drawTimeout; +var lock_input = false; + + +/************************************************ + * SETTINGS + */ +const SETTINGS_FILE = "aiclock.setting.json"; +let settings = { + menuPosX: 0, + menuPosY: 0, +}; +let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; +for (const key in saved_settings) { + settings[key] = saved_settings[key] +} + + +/************************************************ + * Menu + */ +function getDate(){ + var date = new Date(); + return ("0"+date.getDate()).substr(-2) + "/" + ("0"+(date.getMonth()+1)).substr(-2) +} + + +// Custom clockItems menu - therefore, its added here and not in a clkinfo.js file. +var clockItems = { + name: getDate(), + img: null, + items: [ + { name: "Week", + get: () => ({ text: "Week " + weekOfYear(), img: null}), + show: function() { clockItems.items[0].emit("redraw"); }, + hide: function () {} + }, + ] + }; + +function weekOfYear() { + var date = new Date(); + date.setHours(0, 0, 0, 0); + // Thursday in current week decides the year. + date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); + // January 4 is always in week 1. + var week1 = new Date(date.getFullYear(), 0, 4); + // Adjust to Thursday in week 1 and count number of weeks from date to week1. + return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 + - 3 + (week1.getDay() + 6) % 7) / 7); +} + + + +// Load menu +var menu = clock_info.load(); +menu = menu.concat(clockItems); + + + // Ensure that our settings are still in range (e.g. app uninstall). Otherwise reset the position it. + if(settings.menuPosX >= menu.length || settings.menuPosY > menu[settings.menuPosX].items.length ){ + settings.menuPosX = 0; + settings.menuPosY = 0; + } + + // Set draw functions for each item + menu.forEach((menuItm, x) => { + menuItm.items.forEach((item, y) => { + function drawItem() { + // For the clock, we have a special case, as we don't wanna redraw + // immediately when something changes. Instead, we update data each minute + // to save some battery etc. Therefore, we hide (and disable the listener) + // immedeately after redraw... + item.hide(); + + // After drawing the item, we enable inputs again... + lock_input = false; + + var info = item.get(); + drawMenuItem(info.text, info.img); + } + + item.on('redraw', drawItem); + }) + }); + + + function canRunMenuItem(){ + if(settings.menuPosY == 0){ + return false; + } + + var menuEntry = menu[settings.menuPosX]; + var item = menuEntry.items[settings.menuPosY-1]; + return item.run !== undefined; + } + + + function runMenuItem(){ + if(settings.menuPosY == 0){ + return; + } + + var menuEntry = menu[settings.menuPosX]; + var item = menuEntry.items[settings.menuPosY-1]; + try{ + var ret = item.run(); + if(ret){ + Bangle.buzz(300, 0.6); + } + } catch (ex) { + // Simply ignore it... + } + } + /* * Based on the great multi clock from https://github.com/jeffmer/BangleApps/ @@ -76,7 +198,50 @@ function toAngle(a){ return a } + +function drawMenuItem(text, image){ + if(text == null){ + drawTime(); + return + } + // image = atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA=="); + + text = String(text); + + g.reset().setBgColor("#fff").setColor("#000"); + g.setFontAlign(0,0); + g.setFont("Vector", 20); + + var imgWidth = image == null ? 0 : 24; + var strWidth = g.stringWidth(text); + var strHeight = text.split('\n').length > 1 ? 40 : Math.max(24, imgWidth+2); + var w = imgWidth + strWidth; + + g.clearRect(cx-w/2-8, 40-strHeight/2-1, cx+w/2+4, 40+strHeight/2) + + // Draw right line as designed by stable diffusion + g.drawLine(cx+w/2+5, 40-strHeight/2-1, cx+w/2+5, 40+strHeight/2); + g.drawLine(cx+w/2+6, 40-strHeight/2-1, cx+w/2+6, 40+strHeight/2); + g.drawLine(cx+w/2+7, 40-strHeight/2-1, cx+w/2+7, 40+strHeight/2); + + // And finally the text + g.drawString(text, cx+imgWidth/2, 42); + g.drawString(text, cx+1+imgWidth/2, 41); + + if(image != null) { + var scale = image.width ? imgWidth / image.width : 1; + g.drawImage(image, W/2 + -strWidth/2-4 - parseInt(imgWidth/2), 41-12, {scale: scale}); + } + + drawTime(); +} + + function drawTime(){ + // Draw digital time first + drawDigits(); + + // And now the analog time var drawHourHand = g.drawRotRect.bind(g,8,12,R-38); var drawMinuteHand = g.drawRotRect.bind(g,6,12,R-12 ); @@ -90,13 +255,6 @@ function drawTime(){ h += date.getMinutes()/60.0; h = parseInt(h*360/12); - // Draw minute and hour bg - g.setColor(g.theme.bg); - drawHourHand(toAngle(h-3)); - drawHourHand(toAngle(h+3)); - drawMinuteHand(toAngle(m-2)); - drawMinuteHand(toAngle(m+3)); - // Draw minute and hour fg g.setColor(g.theme.fg); drawHourHand(h); @@ -104,28 +262,6 @@ function drawTime(){ } - -function drawDate(){ - var date = new Date(); - g.setFontAlign(0,0); - g.setFontGochiHand(); - - var text = ("0"+date.getDate()).substr(-2) + "/" + ("0"+(date.getMonth()+1)).substr(-2); - var w = g.stringWidth(text); - g.setColor(g.theme.bg); - g.fillRect(cx-w/2-4, 20, cx+w/2+4, 40+12); - - g.setColor(g.theme.fg); - // Draw right line as designed by stable diffusion - g.drawLine(cx+w/2+5, 20, cx+w/2+5, 40+12); - g.drawLine(cx+w/2+6, 20, cx+w/2+6, 40+12); - g.drawLine(cx+w/2+7, 20, cx+w/2+7, 40+12); - - // And finally the text - g.drawString(text, cx, 40); -} - - function drawDigits(){ var date = new Date(); @@ -156,20 +292,35 @@ function drawDigits(){ } +function drawDate(){ + var menuEntry = menu[settings.menuPosX]; + + // The first entry is the overview... + if(settings.menuPosY == 0){ + drawMenuItem(menuEntry.name, menuEntry.img); + return; + } + + // Draw item if needed + lock_input = true; + var item = menuEntry.items[settings.menuPosY-1]; + item.show(); +} + + + + function draw(){ // Queue draw in one minute queueDraw(); - g.reset(); g.clearRect(0, 0, g.getWidth(), g.getHeight()); - g.setColor(1,1,1); + drawBackground(); drawDate(); - drawDigits(); - drawTime(); drawCircle(Bangle.isLocked()); } @@ -190,6 +341,68 @@ Bangle.on('lock', function(isLocked) { drawCircle(isLocked); }); +Bangle.on('touch', function(btn, e){ + var left = parseInt(g.getWidth() * 0.22); + var right = g.getWidth() - left; + var upper = parseInt(g.getHeight() * 0.22); + var lower = g.getHeight() - upper; + + var is_upper = e.y < upper; + var is_lower = e.y > lower; + var is_left = e.x < left && !is_upper && !is_lower; + var is_right = e.x > right && !is_upper && !is_lower; + var is_center = !is_upper && !is_lower && !is_left && !is_right; + + if(lock_input){ + return; + } + + if(is_lower){ + Bangle.buzz(40, 0.6); + settings.menuPosY = (settings.menuPosY+1) % (menu[settings.menuPosX].items.length+1); + + draw(); + } + + if(is_upper){ + Bangle.buzz(40, 0.6); + settings.menuPosY = settings.menuPosY-1; + settings.menuPosY = settings.menuPosY < 0 ? menu[settings.menuPosX].items.length : settings.menuPosY; + + draw(); + } + + if(is_right){ + Bangle.buzz(40, 0.6); + settings.menuPosX = (settings.menuPosX+1) % menu.length; + settings.menuPosY = 0; + draw(); + } + + if(is_left){ + Bangle.buzz(40, 0.6); + settings.menuPosY = 0; + settings.menuPosX = settings.menuPosX-1; + settings.menuPosX = settings.menuPosX < 0 ? menu.length-1 : settings.menuPosX; + draw(); + } + + if(is_center){ + if(canRunMenuItem()){ + runMenuItem(); + } + } +}); + + +E.on("kill", function(){ + try{ + storage.write(SETTINGS_FILE, settings); + } catch(ex){ + // If this fails, we still kill the app... + } +}); + /* * Some helpers @@ -203,7 +416,6 @@ function queueDraw() { } - /* * Lets start widgets, listen for btn etc. */ @@ -215,7 +427,7 @@ Bangle.loadWidgets(); * so we will blank out the draw() functions of each widget and change the * area to the top bar doesn't get cleared. */ -for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";} +require('widget_utils').hide(); // Clear the screen once, at startup and draw clock g.setTheme({bg:"#fff",fg:"#000",dark:false}).clear(); diff --git a/apps/aiclock/impl.png b/apps/aiclock/impl.png index 92374b680..8a9e43e2d 100644 Binary files a/apps/aiclock/impl.png and b/apps/aiclock/impl.png differ diff --git a/apps/aiclock/impl_2.png b/apps/aiclock/impl_2.png new file mode 100644 index 000000000..be3519a4b Binary files /dev/null and b/apps/aiclock/impl_2.png differ diff --git a/apps/aiclock/impl_3.png b/apps/aiclock/impl_3.png new file mode 100644 index 000000000..c2a036d14 Binary files /dev/null and b/apps/aiclock/impl_3.png differ diff --git a/apps/aiclock/metadata.json b/apps/aiclock/metadata.json index 2124b1b7e..1dcda427f 100644 --- a/apps/aiclock/metadata.json +++ b/apps/aiclock/metadata.json @@ -3,7 +3,7 @@ "name": "AI Clock", "shortName":"AI Clock", "icon": "aiclock.png", - "version":"0.03", + "version":"0.05", "readme": "README.md", "supports": ["BANGLEJS2"], "description": "A watch face that was designed by an AI (stable diffusion) and implemented by a human.", @@ -11,7 +11,9 @@ "tags": "clock", "screenshots": [ {"url":"orig.png"}, - {"url":"impl.png"} + {"url":"impl.png"}, + {"url":"impl_2.png"}, + {"url":"impl_3.png"} ], "storage": [ {"name":"aiclock.app.js","url":"aiclock.app.js"}, diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index 52ee8bf9c..9994d33d9 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -34,4 +34,6 @@ 0.32: Fix wrong hidden filter Add option for auto-delete a timer after it expires 0.33: Allow hiding timers&alarms - +0.34: Add "Confirm" option to alarm/timer edit menus +0.35: Add automatic translation of more strings +0.36: alarm widget moved out of app diff --git a/apps/alarm/app.js b/apps/alarm/app.js index ed5aa608a..1414c0b90 100644 --- a/apps/alarm/app.js +++ b/apps/alarm/app.js @@ -128,7 +128,12 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex) { value: alarm.hidden || false, onchange: v => alarm.hidden = v }, - /*LANG*/"Cancel": () => showMainMenu() + /*LANG*/"Cancel": () => showMainMenu(), + /*LANG*/"Confirm": () => { + prepareAlarmForSave(alarm, alarmIndex, time); + saveAndReload(); + showMainMenu(); + } }; if (!isNew) { @@ -178,7 +183,7 @@ function decodeDOW(alarm) { .map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_") .join("") .toLowerCase() - : "Once" + : /*LANG*/"Once" } function showEditRepeatMenu(repeat, dow, dowChangeCallback) { @@ -293,7 +298,12 @@ function showEditTimerMenu(selectedTimer, timerIndex) { onchange: v => timer.hidden = v }, /*LANG*/"Vibrate": require("buzz_menu").pattern(timer.vibrate, v => timer.vibrate = v), - /*LANG*/"Cancel": () => showMainMenu() + /*LANG*/"Cancel": () => showMainMenu(), + /*LANG*/"Confirm": () => { + prepareTimerForSave(timer, timerIndex, time); + saveAndReload(); + showMainMenu(); + } }; if (!isNew) { diff --git a/apps/alarm/metadata.json b/apps/alarm/metadata.json index 31dd58ece..dbf090774 100644 --- a/apps/alarm/metadata.json +++ b/apps/alarm/metadata.json @@ -2,17 +2,16 @@ "id": "alarm", "name": "Alarms & Timers", "shortName": "Alarms", - "version": "0.33", + "version": "0.36", "description": "Set alarms and timers on your Bangle", "icon": "app.png", - "tags": "tool,alarm,widget", + "tags": "tool,alarm", "supports": [ "BANGLEJS", "BANGLEJS2" ], "readme": "README.md", - "dependencies": { "scheduler":"type" }, + "dependencies": { "scheduler":"type", "alarm":"widget" }, "storage": [ { "name": "alarm.app.js", "url": "app.js" }, - { "name": "alarm.img", "url": "app-icon.js", "evaluate": true }, - { "name": "alarm.wid.js", "url": "widget.js" } + { "name": "alarm.img", "url": "app-icon.js", "evaluate": true } ], "screenshots": [ { "url": "screenshot-1.png" }, diff --git a/apps/android/ChangeLog b/apps/android/ChangeLog index a65326941..86dbdb649 100644 --- a/apps/android/ChangeLog +++ b/apps/android/ChangeLog @@ -14,3 +14,7 @@ 0.14: Fix timeout of http function not being cleaned up 0.15: Allow method/body/headers to be specified for `http` (needs Gadgetbridge 0.68.0b or later) 0.16: Bangle.http now fails immediately if there is no Bluetooth connection (fix #2152) +0.17: Now kick off Calendar sync as soon as connected to Gadgetbridge +0.18: Use new message library + If connected to Gadgetbridge, allow GPS forwarding from phone (Gadgetbridge code still not merged) +0.19: Add automatic translation for a couple of strings. diff --git a/apps/android/README.md b/apps/android/README.md index f9ab73699..c76e6e528 100644 --- a/apps/android/README.md +++ b/apps/android/README.md @@ -20,6 +20,8 @@ It contains: of Gadgetbridge - making your phone make noise so you can find it. * `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js keep any messages it has received, or should it delete them? +* `Overwrite GPS` - when GPS is requested by an app, this doesn't use Bangle.js's GPS +but instead asks Gadgetbridge on the phone to use the phone's GPS * `Messages` - launches the messages app, showing a list of messages ## How it works diff --git a/apps/android/boot.js b/apps/android/boot.js index fb2b10e0d..e1e5b028b 100644 --- a/apps/android/boot.js +++ b/apps/android/boot.js @@ -57,7 +57,7 @@ t:event.cmd=="incoming"?"add":"remove", id:"call", src:"Phone", positive:true, negative:true, - title:event.name||"Call", body:"Incoming call\n"+event.number}); + title:event.name||/*LANG*/"Call", body:/*LANG*/"Incoming call\n"+event.number}); require("messages").pushMessage(event); }, "alarm" : function() { @@ -105,7 +105,7 @@ "calendar-" : function() { var cal = require("Storage").readJSON("android.calendar.json",true); //if any of those happen we are out of sync! - if (!cal || !Array.isArray(cal)) return; + if (!cal || !Array.isArray(cal)) cal = []; cal = cal.filter(e=>e.id!=event.id); require("Storage").writeJSON("android.calendar.json", cal); }, @@ -126,6 +126,18 @@ request.j(event.err); //r = reJect function else request.r(event); //r = resolve function + }, + "gps": function() { + const settings = require("Storage").readJSON("android.settings.json",1)||{}; + if (!settings.overwriteGps) return; + delete event.t; + event.satellites = NaN; + event.course = NaN; + event.fix = 1; + Bangle.emit('gps', event); + }, + "is_gps_active": function() { + gbSend({ t: "gps_power", status: Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0 }); } }; var h = HANDLERS[event.t]; @@ -136,7 +148,7 @@ Bangle.http = (url,options)=>{ options = options||{}; if (!NRF.getSecurityStatus().connected) - return Promise.reject("Not connected to Bluetooth"); + return Promise.reject(/*LANG*/"Not connected to Bluetooth"); if (Bangle.httpRequest === undefined) Bangle.httpRequest={}; if (options.id === undefined) { @@ -166,7 +178,10 @@ // Battery monitor function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); } - NRF.on("connect", () => setTimeout(sendBattery, 2000)); + NRF.on("connect", () => setTimeout(function() { + sendBattery(); + GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process + }, 2000)); Bangle.on("charging", sendBattery); if (!settings.keep) NRF.on("disconnect", () => require("messages").clearAll()); // remove all messages on disconnect @@ -186,6 +201,30 @@ if (isFinite(msg.id)) return gbSend({ t: "notify", n:response?"OPEN":"DISMISS", id: msg.id }); // error/warn here? }; + // GPS overwrite logic + if (settings.overwriteGps) { // if the overwrite option is set../ + // Save current logic + const originalSetGpsPower = Bangle.setGPSPower; + // Replace set GPS power logic to suppress activation of gps (and instead request it from the phone) + Bangle.setGPSPower = (isOn, appID) => { + // if not connected, use old logic + if (!NRF.getSecurityStatus().connected) return originalSetGpsPower(isOn, appID); + // Emulate old GPS power logic + if (!Bangle._PWR) Bangle._PWR={}; + if (!Bangle._PWR.GPS) Bangle._PWR.GPS=[]; + if (!appID) appID="?"; + if (isOn && !Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.push(appID); + if (!isOn && Bangle._PWR.GPS.includes(appID)) Bangle._PWR.GPS.splice(Bangle._PWR.GPS.indexOf(appID),1); + let pwr = Bangle._PWR.GPS.length>0; + gbSend({ t: "gps_power", status: pwr }); + return pwr; + } + // Replace check if the GPS is on to check the _PWR variable + Bangle.isGPSOn = () => { + return Bangle._PWR && Bangle._PWR.GPS && Bangle._PWR.GPS.length>0; + } + } + // remove settings object so it's not taking up RAM delete settings; })(); diff --git a/apps/android/metadata.json b/apps/android/metadata.json index ab340340c..d5a45edb7 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,11 +2,11 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.16", + "version": "0.19", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "icon": "app.png", "tags": "tool,system,messages,notifications,gadgetbridge", - "dependencies": {"messages":"app"}, + "dependencies": {"messages":"module"}, "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ diff --git a/apps/android/settings.js b/apps/android/settings.js index c7c34a76f..3e04e0f9d 100644 --- a/apps/android/settings.js +++ b/apps/android/settings.js @@ -1,4 +1,7 @@ (function(back) { + + + function gb(j) { Bluetooth.println(JSON.stringify(j)); } @@ -23,7 +26,17 @@ updateSettings(); } }, - /*LANG*/"Messages" : ()=>load("messages.app.js"), + /*LANG*/"Overwrite GPS" : { + value : !!settings.overwriteGps, + onchange: newValue => { + if (newValue) { + Bangle.setGPSPower(false, 'android'); + } + settings.overwriteGps = newValue; + updateSettings(); + } + }, + /*LANG*/"Messages" : ()=>require("message").openGUI(), }; E.showMenu(mainmenu); }) diff --git a/apps/astrocalc/ChangeLog b/apps/astrocalc/ChangeLog index 60ef5da0a..11b2d7177 100644 --- a/apps/astrocalc/ChangeLog +++ b/apps/astrocalc/ChangeLog @@ -1,2 +1,4 @@ 0.01: Create astrocalc app 0.02: Store last GPS lock, can be used instead of waiting for new GPS on start +0.03: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps +0.04: Compatibility with Bangle.js 2, get location from My Location diff --git a/apps/astrocalc/astrocalc-app.js b/apps/astrocalc/astrocalc-app.js index 4e7aa0b40..6629842cf 100644 --- a/apps/astrocalc/astrocalc-app.js +++ b/apps/astrocalc/astrocalc-app.js @@ -9,10 +9,9 @@ * Calculate the Sun and Moon positions based on watch GPS and display graphically */ -const SunCalc = require("suncalc.js"); +const SunCalc = require("suncalc"); // from modules folder const storage = require("Storage"); -const LAST_GPS_FILE = "astrocalc.gps.json"; -let lastGPS = (storage.readJSON(LAST_GPS_FILE, 1) || null); +const BANGLEJS2 = process.env.HWVERSION == 2; // check for bangle 2 function drawMoon(phase, x, y) { const moonImgFiles = [ @@ -73,7 +72,7 @@ function drawTitle(key) { */ function drawPoint(angle, radius, color) { const pRad = Math.PI / 180; - const faceWidth = 80; // watch face radius + const faceWidth = g.getWidth()/3; // watch face radius const centerPx = g.getWidth() / 2; const a = angle * pRad; @@ -141,6 +140,7 @@ function drawData(title, obj, startX, startY) { function drawMoonPositionPage(gps, title) { const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon); + const moonColor = g.theme.dark ? {r: 1, g: 1, b: 1} : {r: 0, g: 0, b: 0}; const pageData = { Azimuth: pos.azimuth.toFixed(2), @@ -150,59 +150,61 @@ function drawMoonPositionPage(gps, title) { }; const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI); - drawData(title, pageData, null, 80); + drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20); drawPoints(); - drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 1}); + drawPoint(azimuthDegrees, 8, moonColor); let m = setWatch(() => { let m = moonIndexPageMenu(gps); - }, BTN3, {repeat: false, edge: "falling"}); + }, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"}); } function drawMoonIlluminationPage(gps, title) { const phaseNames = [ - "New Moon", "Waxing Crescent", "First Quarter", "Waxing Gibbous", - "Full Moon", "Waning Gibbous", "Last Quater", "Waning Crescent", + /*LANG*/"New Moon", /*LANG*/"Waxing Crescent", /*LANG*/"First Quarter", /*LANG*/"Waxing Gibbous", + /*LANG*/"Full Moon", /*LANG*/"Waning Gibbous", /*LANG*/"Last Quater", /*LANG*/"Waning Crescent", ]; const phase = SunCalc.getMoonIllumination(new Date()); + const phaseIdx = Math.round(phase.phase*8); const pageData = { - Phase: phaseNames[phase.phase], + Phase: phaseNames[phaseIdx], }; drawData(title, pageData, null, 35); - drawMoon(phase.phase, g.getWidth() / 2, g.getHeight() / 2); + drawMoon(phaseIdx, g.getWidth() / 2, g.getHeight() / 2); let m = setWatch(() => { let m = moonIndexPageMenu(gps); - }, BTN3, {repease: false, edge: "falling"}); + }, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"}); } function drawMoonTimesPage(gps, title) { const times = SunCalc.getMoonTimes(new Date(), gps.lat, gps.lon); + const moonColor = g.theme.dark ? {r: 1, g: 1, b: 1} : {r: 0, g: 0, b: 0}; const pageData = { Rise: dateToTimeString(times.rise), Set: dateToTimeString(times.set), }; - drawData(title, pageData, null, 105); + drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5); drawPoints(); // Draw the moon rise position const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon); const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI); - drawPoint(riseAzimuthDegrees, 8, {r: 1, g: 1, b: 1}); + drawPoint(riseAzimuthDegrees, 8, moonColor); // Draw the moon set position const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon); const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI); - drawPoint(setAzimuthDegrees, 8, {r: 1, g: 1, b: 1}); + drawPoint(setAzimuthDegrees, 8, moonColor); let m = setWatch(() => { let m = moonIndexPageMenu(gps); - }, BTN3, {repease: false, edge: "falling"}); + }, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"}); } function drawSunShowPage(gps, key, date) { @@ -224,7 +226,7 @@ function drawSunShowPage(gps, key, date) { Degrees: azimuthDegrees }; - drawData(key, pageData, null, 85); + drawData(key, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5); drawPoints(); @@ -233,7 +235,7 @@ function drawSunShowPage(gps, key, date) { m = setWatch(() => { m = sunIndexPageMenu(gps); - }, BTN3, {repeat: false, edge: "falling"}); + }, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"}); return null; } @@ -273,15 +275,15 @@ function moonIndexPageMenu(gps) { }, "Times": () => { m = E.showMenu(); - drawMoonTimesPage(gps, "Times"); + drawMoonTimesPage(gps, /*LANG*/"Times"); }, "Position": () => { m = E.showMenu(); - drawMoonPositionPage(gps, "Position"); + drawMoonPositionPage(gps, /*LANG*/"Position"); }, "Illumination": () => { m = E.showMenu(); - drawMoonIlluminationPage(gps, "Illumination"); + drawMoonIlluminationPage(gps, /*LANG*/"Illumination"); }, "< Back": () => m = indexPageMenu(gps), }; @@ -292,15 +294,15 @@ function moonIndexPageMenu(gps) { function indexPageMenu(gps) { const menu = { "": { - "title": "Select", + "title": /*LANG*/"Select", }, - "Sun": () => { + /*LANG*/"Sun": () => { m = sunIndexPageMenu(gps); }, - "Moon": () => { + /*LANG*/"Moon": () => { m = moonIndexPageMenu(gps); }, - "< Exit": () => { load(); } + "< Back": () => { load(); } }; return E.showMenu(menu); @@ -310,79 +312,10 @@ function getCenterStringX(str) { return (g.getWidth() - g.stringWidth(str)) / 2; } -/** - * GPS wait page, shows GPS locating animation until it gets a lock, then moves to the Sun page - */ -function drawGPSWaitPage() { - const img = require("heatshrink").decompress(atob("mEwxH+AH4A/AH4AW43GF1wwsFwYwqFwowoFw4wmFxIwdE5YAPF/4vM5nN6YAE5vMF8YtHGIgvhFpQxKF7AuOGA4vXFyAwGF63MFyIABF6xeWMC4UDLwvNGpAJG5gwSdhIIDRBLyWCIgcJHAgJJDoouQF4vMQoICBBJoeGFx6GGACIfHL6YvaX6gvZeCIdFc4gAFXogvGFxgwFDwovQCAguOGAnMMBxeG5guTGAggGGAwNKFySREcA3N5vM5gDBdpQvXEY4AKXqovGGCKbFF7AwPZQwvZGJgtGF7vGdQItG5gSIF7gASF/44WEzgwRF0wwHF1AwFF1QwDF1gvwAH4A/AFAA==")); - const str1 = "Astrocalc v0.02"; - const str2 = "Locating GPS"; - const str3 = "Please wait..."; - - g.clear(); - g.drawImage(img, 100, 50); - g.setFont("6x8", 1); - g.drawString(str1, getCenterStringX(str1), 105); - g.drawString(str2, getCenterStringX(str2), 140); - g.drawString(str3, getCenterStringX(str3), 155); - - if (lastGPS) { - lastGPS = JSON.parse(lastGPS); - lastGPS.time = new Date(); - - const str4 = "Press Button 3 to use last GPS"; - g.setColor("#d32e29"); - g.fillRect(0, 190, g.getWidth(), 215); - g.setColor("#ffffff"); - g.drawString(str4, getCenterStringX(str4), 200); - - setWatch(() => { - clearWatch(); - Bangle.setGPSPower(0); - m = indexPageMenu(lastGPS); - }, BTN3, {repeat: false}); - } - - g.flip(); - - const DEBUG = false; - if (DEBUG) { - clearWatch(); - - const gps = { - "lat": 56.45783133333, - "lon": -3.02188583333, - "alt": 75.3, - "speed": 0.070376, - "course": NaN, - "time":new Date(), - "satellites": 4, - "fix": 1 - }; - - m = indexPageMenu(gps); - - return; - } - - Bangle.on('GPS', (gps) => { - if (gps.fix === 0) return; - clearWatch(); - - if (isNaN(gps.course)) gps.course = 0; - require("Storage").writeJSON(LAST_GPS_FILE, JSON.stringify(gps)); - Bangle.setGPSPower(0); - Bangle.buzz(); - Bangle.setLCDPower(true); - - m = indexPageMenu(gps); - }); -} - function init() { - Bangle.setGPSPower(1); - drawGPSWaitPage(); + let location = require("Storage").readJSON("mylocation.json",1)||{"lat":51.5072,"lon":0.1276,"location":"London"}; + indexPageMenu(location); } let m; -init(); \ No newline at end of file +init(); diff --git a/apps/astrocalc/metadata.json b/apps/astrocalc/metadata.json index 384c7fa1e..653c097da 100644 --- a/apps/astrocalc/metadata.json +++ b/apps/astrocalc/metadata.json @@ -1,15 +1,15 @@ { "id": "astrocalc", "name": "Astrocalc", - "version": "0.02", - "description": "Calculates interesting information on the sun and moon cycles for the current day based on your location.", + "version": "0.04", + "description": "Calculates interesting information on the sun like sunset and sunrise and moon cycles for the current day based on your location from MyLocation app", "icon": "astrocalc.png", - "tags": "app,sun,moon,cycles,tool,outdoors", - "supports": ["BANGLEJS"], + "tags": "app,sun,moon,cycles,tool", + "supports": ["BANGLEJS", "BANGLEJS2"], "allow_emulator": true, + "dependencies": {"mylocation":"app"}, "storage": [ {"name":"astrocalc.app.js","url":"astrocalc-app.js"}, - {"name":"suncalc.js","url":"suncalc.js"}, {"name":"astrocalc.img","url":"astrocalc-icon.js","evaluate":true}, {"name":"first-quarter.img","url":"first-quarter-icon.js","evaluate":true}, {"name":"last-quarter.img","url":"last-quarter-icon.js","evaluate":true}, diff --git a/apps/astrocalc/suncalc.js b/apps/astrocalc/suncalc.js deleted file mode 100644 index e2beaedca..000000000 --- a/apps/astrocalc/suncalc.js +++ /dev/null @@ -1,328 +0,0 @@ -/* - (c) 2011-2015, Vladimir Agafonkin - SunCalc is a JavaScript library for calculating sun/moon position and light phases. - https://github.com/mourner/suncalc -*/ - -(function () { 'use strict'; - - // shortcuts for easier to read formulas - - var PI = Math.PI, - sin = Math.sin, - cos = Math.cos, - tan = Math.tan, - asin = Math.asin, - atan = Math.atan2, - acos = Math.acos, - rad = PI / 180; - - // sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas - - - // date/time constants and conversions - - var dayMs = 1000 * 60 * 60 * 24, - J1970 = 2440588, - J2000 = 2451545; - - function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; } - function fromJulian(j) { return (j + 0.5 - J1970) * dayMs; } - function toDays(date) { return toJulian(date) - J2000; } - - - // general calculations for position - - var e = rad * 23.4397; // obliquity of the Earth - - function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); } - function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); } - - function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); } - function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); } - - function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; } - - function astroRefraction(h) { - if (h < 0) // the following formula works for positive altitudes only. - h = 0; // if h = -0.08901179 a div/0 would occur. - - // formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. - // 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad: - return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179)); - } - - // general sun calculations - - function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); } - - function eclipticLongitude(M) { - - var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center - P = rad * 102.9372; // perihelion of the Earth - - return M + C + P + PI; - } - - function sunCoords(d) { - - var M = solarMeanAnomaly(d), - L = eclipticLongitude(M); - - return { - dec: declination(L, 0), - ra: rightAscension(L, 0) - }; - } - - - var SunCalc = {}; - - - // calculates sun position for a given date and latitude/longitude - - SunCalc.getPosition = function (date, lat, lng) { - - var lw = rad * -lng, - phi = rad * lat, - d = toDays(date), - - c = sunCoords(d), - H = siderealTime(d, lw) - c.ra; - - return { - azimuth: azimuth(H, phi, c.dec), - altitude: altitude(H, phi, c.dec) - }; - }; - - - // sun times configuration (angle, morning name, evening name) - - var times = SunCalc.times = [ - [-0.833, 'sunrise', 'sunset' ], - [ -0.3, 'sunriseEnd', 'sunsetStart' ], - [ -6, 'dawn', 'dusk' ], - [ -12, 'nauticalDawn', 'nauticalDusk'], - [ -18, 'nightEnd', 'night' ], - [ 6, 'goldenHourEnd', 'goldenHour' ] - ]; - - // adds a custom time to the times config - - SunCalc.addTime = function (angle, riseName, setName) { - times.push([angle, riseName, setName]); - }; - - - // calculations for sun times - - var J0 = 0.0009; - - function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); } - - function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; } - function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); } - - function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); } - function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; } - - // returns set time for the given sun altitude - function getSetJ(h, lw, phi, dec, n, M, L) { - - var w = hourAngle(h, phi, dec), - a = approxTransit(w, lw, n); - return solarTransitJ(a, M, L); - } - - - // calculates sun times for a given date, latitude/longitude, and, optionally, - // the observer height (in meters) relative to the horizon - - SunCalc.getTimes = function (date, lat, lng, height) { - - height = height || 0; - - var lw = rad * -lng, - phi = rad * lat, - - dh = observerAngle(height), - - d = toDays(date), - n = julianCycle(d, lw), - ds = approxTransit(0, lw, n), - - M = solarMeanAnomaly(ds), - L = eclipticLongitude(M), - dec = declination(L, 0), - - Jnoon = solarTransitJ(ds, M, L), - - i, len, time, h0, Jset, Jrise; - - - var result = { - solarNoon: new Date(fromJulian(Jnoon)), - nadir: new Date(fromJulian(Jnoon - 0.5)) - }; - - for (i = 0, len = times.length; i < len; i += 1) { - time = times[i]; - h0 = (time[0] + dh) * rad; - - Jset = getSetJ(h0, lw, phi, dec, n, M, L); - Jrise = Jnoon - (Jset - Jnoon); - - result[time[1]] = new Date(fromJulian(Jrise) - (dayMs / 2)); - result[time[2]] = new Date(fromJulian(Jset) + (dayMs / 2)); - } - - return result; - }; - - - // moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas - - function moonCoords(d) { // geocentric ecliptic coordinates of the moon - - var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude - M = rad * (134.963 + 13.064993 * d), // mean anomaly - F = rad * (93.272 + 13.229350 * d), // mean distance - - l = L + rad * 6.289 * sin(M), // longitude - b = rad * 5.128 * sin(F), // latitude - dt = 385001 - 20905 * cos(M); // distance to the moon in km - - return { - ra: rightAscension(l, b), - dec: declination(l, b), - dist: dt - }; - } - - SunCalc.getMoonPosition = function (date, lat, lng) { - - var lw = rad * -lng, - phi = rad * lat, - d = toDays(date), - - c = moonCoords(d), - H = siderealTime(d, lw) - c.ra, - h = altitude(H, phi, c.dec), - // formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. - pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H)); - - h = h + astroRefraction(h); // altitude correction for refraction - - return { - azimuth: azimuth(H, phi, c.dec), - altitude: h, - distance: c.dist, - parallacticAngle: pa - }; - }; - - - // calculations for illumination parameters of the moon, - // based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and - // Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. - - // Function updated from gist: https://gist.github.com/endel/dfe6bb2fbe679781948c - - SunCalc.getMoonIllumination = function (date) { - let month = date.getMonth(); - let year = date.getFullYear(); - let day = date.getDate(); - - let c = 0; - let e = 0; - let jd = 0; - let b = 0; - - if (month < 3) { - year--; - month += 12; - } - - ++month; - c = 365.25 * year; - e = 30.6 * month; - jd = c + e + day - 694039.09; // jd is total days elapsed - jd /= 29.5305882; // divide by the moon cycle - b = parseInt(jd); // int(jd) -> b, take integer part of jd - jd -= b; // subtract integer part to leave fractional part of original jd - b = Math.round(jd * 8); // scale fraction from 0-8 and round - - if (b >= 8) b = 0; // 0 and 8 are the same so turn 8 into 0 - - return {phase: b}; - }; - - - function hoursLater(date, h) { - return new Date(date.valueOf() + h * dayMs / 24); - } - - // calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article - - SunCalc.getMoonTimes = function (date, lat, lng, inUTC) { - var t = date; - if (inUTC) t.setUTCHours(0, 0, 0, 0); - else t.setHours(0, 0, 0, 0); - - var hc = 0.133 * rad, - h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc, - h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx; - - // go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set) - for (var i = 1; i <= 24; i += 2) { - h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc; - h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc; - - a = (h0 + h2) / 2 - h1; - b = (h2 - h0) / 2; - xe = -b / (2 * a); - ye = (a * xe + b) * xe + h1; - d = b * b - 4 * a * h1; - roots = 0; - - if (d >= 0) { - dx = Math.sqrt(d) / (Math.abs(a) * 2); - x1 = xe - dx; - x2 = xe + dx; - if (Math.abs(x1) <= 1) roots++; - if (Math.abs(x2) <= 1) roots++; - if (x1 < -1) x1 = x2; - } - - if (roots === 1) { - if (h0 < 0) rise = i + x1; - else set = i + x1; - - } else if (roots === 2) { - rise = i + (ye < 0 ? x2 : x1); - set = i + (ye < 0 ? x1 : x2); - } - - if (rise && set) break; - - h0 = h2; - } - - var result = {}; - - if (rise) result.rise = hoursLater(t, rise); - if (set) result.set = hoursLater(t, set); - - if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true; - - return result; - }; - - - // export as Node module / AMD module / browser variable - if (typeof exports === 'object' && typeof module !== 'undefined') module.exports = SunCalc; - else if (typeof define === 'function' && define.amd) define(SunCalc); - else global.SunCalc = SunCalc; - -}()); diff --git a/apps/barclock/ChangeLog b/apps/barclock/ChangeLog index a00ae9325..88f4eaf00 100644 --- a/apps/barclock/ChangeLog +++ b/apps/barclock/ChangeLog @@ -12,4 +12,5 @@ 0.12: Add settings to hide date,widgets 0.13: Add font setting 0.14: Use ClockFace_menu.addItems -0.15: Add Power saving option \ No newline at end of file +0.15: Add Power saving option +0.16: Support Fast Loading diff --git a/apps/barclock/clock-bar.js b/apps/barclock/clock-bar.js index 5a7dfc8c0..f2499189b 100644 --- a/apps/barclock/clock-bar.js +++ b/apps/barclock/clock-bar.js @@ -1,124 +1,128 @@ /* jshint esversion: 6 */ -/** - * A simple digital clock showing seconds as a bar - **/ +{ + /** + * A simple digital clock showing seconds as a bar + **/ // Check settings for what type our clock should be -let locale = require("locale"); -{ // add some more info to locale - let date = new Date(); - date.setFullYear(1111); - date.setMonth(1, 3); // februari: months are zero-indexed - const localized = locale.date(date, true); - locale.dayFirst = /3.*2/.test(localized); - locale.hasMeridian = (locale.meridian(date)!==""); -} - -let barW = 0, prevX = 0; -function renderBar(l) { - "ram"; - if (l) prevX = 0; // called from Layout: drawing area was cleared - else l = clock.layout.bar; - let x2 = l.x+barW; - if (clock.powerSave && Bangle.isLocked()) x2 = 0; // hide bar - if (x2===prevX) return; // nothing to do - if (x2===0) x2--; // don't leave 1px line - if (x212) { - date12.setHours(hours-12); - } - return locale.time(date12, true); -} -function ampmText(date) { - return (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : ""; -} -function dateText(date) { - const dayName = locale.dow(date, true), - month = locale.month(date, true), - day = date.getDate(); - const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}`; - return `${dayName} ${dayMonth}`; -} -const ClockFace = require("ClockFace"), - clock = new ClockFace({ - precision: 1, - settingsFile: "barclock.settings.json", - init: function() { - const Layout = require("Layout"); - this.layout = new Layout({ - type: "v", c: [ - { - type: "h", c: [ - {id: "time", label: "88:88", type: "txt", font: "6x8:5", col: g.theme.fg, bgCol: g.theme.bg}, // updated below - {id: "ampm", label: " ", type: "txt", font: "6x8:2", col: g.theme.fg, bgCol: g.theme.bg}, - ], - }, - {id: "bar", type: "custom", fillx: 1, height: 6, col: g.theme.fg2, render: renderBar}, - this.showDate ? {height: 40} : {}, - this.showDate ? {id: "date", type: "txt", font: "10%", valign: 1} : {}, - ], - }, {lazy: true}); - // adjustments based on screen size and whether we display am/pm - let thickness; // bar thickness, same as time font "pixel block" size - if (this.is12Hour && locale.hasMeridian) { - // Maximum font size = ( - ) / (5chars * 6px) - thickness = Math.floor((Bangle.appRect.w-24)/(5*6)); - } else { - this.layout.ampm.label = ""; - thickness = Math.floor(Bangle.appRect.w/(5*6)); - } - let bar = this.layout.bar; - bar.height = thickness+1; - if (this.font===1) { // vector - const B2 = process.env.HWVERSION>1; + let barW = 0, prevX = 0; + const renderBar = function (l) { + "ram"; + if (l) prevX = 0; // called from Layout: drawing area was cleared + else l = clock.layout.bar; + let x2 = l.x+barW; + if (clock.powerSave && Bangle.isLocked()) x2 = 0; // hide bar + if (x2===prevX) return; // nothing to do + if (x2===0) x2--; // don't leave 1px line + if (x212) { + date12.setHours(hours-12); + } + return locale.time(date12, true); + } + const ampmText = date => (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : ""; + const dateText = date => { + const dayName = locale.dow(date, true), + month = locale.month(date, true), + day = date.getDate(); + const dayMonth = locale.dayFirst ? `${day} ${month}` : `${month} ${day}`; + return `${dayName} ${dayMonth}`; + }; + + const ClockFace = require("ClockFace"), + clock = new ClockFace({ + precision: 1, + settingsFile: "barclock.settings.json", + init: function() { + const Layout = require("Layout"); + this.layout = new Layout({ + type: "v", c: [ + { + type: "h", c: [ + {id: "time", label: "88:88", type: "txt", font: "6x8:5", col: g.theme.fg, bgCol: g.theme.bg}, // updated below + {id: "ampm", label: " ", type: "txt", font: "6x8:2", col: g.theme.fg, bgCol: g.theme.bg}, + ], + }, + {id: "bar", type: "custom", fillx: 1, height: 6, col: g.theme.fg2, render: renderBar}, + this.showDate ? {height: 40} : {}, + this.showDate ? {id: "date", type: "txt", font: "10%", valign: 1} : {}, + ], + }, {lazy: true}); + // adjustments based on screen size and whether we display am/pm + let thickness; // bar thickness, same as time font "pixel block" size if (this.is12Hour && locale.hasMeridian) { - this.layout.time.font = "Vector:"+(B2 ? 50 : 60); - this.layout.ampm.font = "Vector:"+(B2 ? 20 : 40); + // Maximum font size = ( - ) / (5chars * 6px) + thickness = Math.floor((Bangle.appRect.w-24)/(5*6)); } else { - this.layout.time.font = "Vector:"+(B2 ? 60 : 80); + this.layout.ampm.label = ""; + thickness = Math.floor(Bangle.appRect.w/(5*6)); } - } else { - this.layout.time.font = "6x8:"+thickness; - } - this.layout.update(); - bar.y2 = bar.y+bar.height-1; - }, - update: function(date, c) { - "ram"; - if (c.m) this.layout.time.label = timeText(date); - if (c.h) this.layout.ampm.label = ampmText(date); - if (c.d && this.showDate) this.layout.date.label = dateText(date); - if (c.m) this.layout.render(); - if (c.s) { - barW = Math.round(date.getSeconds()/60*this.layout.bar.w); - renderBar(); - } - }, - resume: function() { - prevX = 0; // force redraw of bar - this.layout.forgetLazyState(); - }, - }); + let bar = this.layout.bar; + bar.height = thickness+1; + if (this.font===1) { // vector + const B2 = process.env.HWVERSION>1; + if (this.is12Hour && locale.hasMeridian) { + this.layout.time.font = "Vector:"+(B2 ? 50 : 60); + this.layout.ampm.font = "Vector:"+(B2 ? 20 : 40); + } else { + this.layout.time.font = "Vector:"+(B2 ? 60 : 80); + } + } else { + this.layout.time.font = "6x8:"+thickness; + } + this.layout.update(); + bar.y2 = bar.y+bar.height-1; + }, + update: function(date, c) { + "ram"; + if (c.m) this.layout.time.label = timeText(date); + if (c.h) this.layout.ampm.label = ampmText(date); + if (c.d && this.showDate) this.layout.date.label = dateText(date); + if (c.m) this.layout.render(); + if (c.s) { + barW = Math.round(date.getSeconds()/60*this.layout.bar.w); + renderBar(); + } + }, + resume: function() { + prevX = 0; // force redraw of bar + this.layout.forgetLazyState(); + }, + remove: function() { + if (this.onLock) Bangle.removeListener("lock", this.onLock); + }, + }); -// power saving: only update once a minute while locked, hide bar -if (clock.powerSave) { - Bangle.on("lock", lock => { - clock.precision = lock ? 60 : 1; - clock.tick(); - renderBar(); // hide/redraw bar right away - }); -} + // power saving: only update once a minute while locked, hide bar + if (clock.powerSave) { + clock.onLock = lock => { + clock.precision = lock ? 60 : 1; + clock.tick(); + renderBar(); // hide/redraw bar right away + } + Bangle.on("lock", clock.onLock); + } -clock.start(); + clock.start(); +} \ No newline at end of file diff --git a/apps/barclock/metadata.json b/apps/barclock/metadata.json index 5b783dbda..785c228b0 100644 --- a/apps/barclock/metadata.json +++ b/apps/barclock/metadata.json @@ -1,7 +1,7 @@ { "id": "barclock", "name": "Bar Clock", - "version": "0.15", + "version": "0.16", "description": "A simple digital clock showing seconds as a bar", "icon": "clock-bar.png", "screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}], diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index 0eaf517d9..780d9cc7d 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -61,3 +61,6 @@ 0.52: Ensure heading patch for pre-2v15.68 firmware applies to getCompass 0.53: Add polyfills for pre-2v15.135 firmware for Bangle.load and Bangle.showClock 0.54: Fix for invalid version comparison in polyfill +0.55: Add toLocalISOString polyfill for pre-2v15 firmwares + Only add boot info comments if settings.bootDebug was set + If settings.bootDebug is set, output timing for each section of .boot0 diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index ad58437ec..112dfeba8 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -1,16 +1,22 @@ /* This rewrites boot0.js based on current settings. If settings changed then it recalculates, but this avoids us doing a whole bunch of reconfiguration most of the time. */ +{ // execute in our own scope so we don't have to free variables... E.showMessage(/*LANG*/"Updating boot0..."); -var s = require('Storage').readJSON('setting.json',1)||{}; -var BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2 -var FWVERSION = parseFloat(process.env.VERSION.replace("v","").replace(/\.(\d\d)$/,".0$1")); -var boot = "", bootPost = ""; +let s = require('Storage').readJSON('setting.json',1)||{}; +const BANGLEJS2 = process.env.HWVERSION==2; // Is Bangle.js 2 +const FWVERSION = parseFloat(process.env.VERSION.replace("v","").replace(/\.(\d\d)$/,".0$1")); +const DEBUG = s.bootDebug; // we can set this to enable debugging output in boot0 +let boot = "", bootPost = ""; +if (DEBUG) { + boot += "var _tm=Date.now()\n"; + bootPost += "delete _tm;"; +} if (require('Storage').hash) { // new in 2v11 - helps ensure files haven't changed - var CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT); + let CRC = E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\.boot\.js/)+E.CRC32(process.env.GIT_COMMIT); boot += `if (E.CRC32(require('Storage').read('setting.json'))+require('Storage').hash(/\\.boot\\.js/)+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; } else { - var CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT); + let CRC = E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\.boot\.js/))+E.CRC32(process.env.GIT_COMMIT); boot += `if (E.CRC32(require('Storage').read('setting.json'))+E.CRC32(require('Storage').list(/\\.boot\\.js/))+E.CRC32(process.env.GIT_COMMIT)!=${CRC})`; } boot += ` { eval(require('Storage').read('bootupdate.js')); throw "Storage Updated!"}\n`; @@ -88,14 +94,25 @@ delete Bangle.showClock; if (!Bangle.showClock) boot += `Bangle.showClock = ()=>{load(".bootcde")};\n`; delete Bangle.load; if (!Bangle.load) boot += `Bangle.load = load;\n`; +let date = new Date(); +delete date.toLocalISOString; // toLocalISOString was only introduced in 2v15 +if (!date.toLocalISOString) boot += `Date.prototype.toLocalISOString = function() { + var o = this.getTimezoneOffset(); + var d = new Date(this.getTime() - o*60000); + var sign = o>0?"-":"+"; + o = Math.abs(o); + return d.toISOString().slice(0,-1)+sign+Math.floor(o/60).toString().padStart(2,0)+(o%60).toString().padStart(2,0); +};\n`; +// show timings +if (DEBUG) boot += `print(".boot0",0|(Date.now()-_tm),"ms");_tm=Date.now();\n` // ================================================== BOOT.JS // Append *.boot.js files // These could change bleServices/bleServiceOptions if needed -var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ - var getPriority = /.*\.(\d+)\.boot\.js$/; - var aPriority = a.match(getPriority); - var bPriority = b.match(getPriority); +let bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ + let getPriority = /.*\.(\d+)\.boot\.js$/; + let aPriority = a.match(getPriority); + let bPriority = b.match(getPriority); if (aPriority && bPriority){ return parseInt(aPriority[1]) - parseInt(bPriority[1]); } else if (aPriority && !bPriority){ @@ -106,14 +123,16 @@ var bootFiles = require('Storage').list(/\.boot\.js$/).sort((a,b)=>{ return a==b ? 0 : (a>b ? 1 : -1); }); // precalculate file size -var fileSize = boot.length + bootPost.length; +let fileSize = boot.length + bootPost.length; bootFiles.forEach(bootFile=>{ // match the size of data we're adding below in bootFiles.forEach - fileSize += 2+bootFile.length+1+require('Storage').read(bootFile).length+2; + if (DEBUG) fileSize += 2+bootFile.length+1; // `//${bootFile}\n` comment + fileSize += require('Storage').read(bootFile).length+2; // boot code plus ";\n" + if (DEBUG) fileSize += 48+E.toJS(bootFile).length; // `print(${E.toJS(bootFile)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n` }); // write file in chunks (so as not to use up all RAM) require('Storage').write('.boot0',boot,0,fileSize); -var fileOffset = boot.length; +let fileOffset = boot.length; bootFiles.forEach(bootFile=>{ // we add a semicolon so if the file is wrapped in (function(){ ... }() // with no semicolon we don't end up with (function(){ ... }()(function(){ ... }() @@ -122,16 +141,18 @@ bootFiles.forEach(bootFile=>{ // "//"+bootFile+"\n"+require('Storage').read(bootFile)+";\n"; // but we need to do this without ever loading everything into RAM as some // boot files seem to be getting pretty big now. - require('Storage').write('.boot0',"//"+bootFile+"\n",fileOffset); - fileOffset+=2+bootFile.length+1; - var bf = require('Storage').read(bootFile); + if (DEBUG) { + require('Storage').write('.boot0',`//${bootFile}\n`,fileOffset); + fileOffset+=2+bootFile.length+1; + } + let bf = require('Storage').read(bootFile); // we can't just write 'bf' in one go because at least in 2v13 and earlier // Espruino wants to read the whole file into RAM first, and on Bangle.js 1 // it can be too big (especially BTHRM). - var bflen = bf.length; - var bfoffset = 0; + let bflen = bf.length; + let bfoffset = 0; while (bflen) { - var bfchunk = Math.min(bflen, 2048); + let bfchunk = Math.min(bflen, 2048); require('Storage').write('.boot0',bf.substr(bfoffset, bfchunk),fileOffset); fileOffset+=bfchunk; bfoffset+=bfchunk; @@ -139,15 +160,14 @@ bootFiles.forEach(bootFile=>{ } require('Storage').write('.boot0',";\n",fileOffset); fileOffset+=2; + if (DEBUG) { + require('Storage').write('.boot0',`print(${E.toJS(bootFile)},0|(Date.now()-_tm),"ms");_tm=Date.now();\n`,fileOffset); + fileOffset += 48+E.toJS(bootFile).length + } }); require('Storage').write('.boot0',bootPost,fileOffset); - -delete boot; -delete bootPost; -delete bootFiles; -delete fileSize; -delete fileOffset; E.showMessage(/*LANG*/"Reloading..."); -eval(require('Storage').read('.boot0')); +} // .bootcde should be run automatically after if required, since // we normally get called automatically from '.boot0' +eval(require('Storage').read('.boot0')); diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json index bd39beb7f..455563a16 100644 --- a/apps/boot/metadata.json +++ b/apps/boot/metadata.json @@ -1,7 +1,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.54", + "version": "0.55", "description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings", "icon": "bootloader.png", "type": "bootloader", diff --git a/apps/bthometemp/ChangeLog b/apps/bthometemp/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/bthometemp/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/bthometemp/README.md b/apps/bthometemp/README.md new file mode 100644 index 000000000..1a8212ea4 --- /dev/null +++ b/apps/bthometemp/README.md @@ -0,0 +1,9 @@ +# BTHome Temperature and Pressure + +This app displays temperature and pressure and advertises them over bluetooth using BTHome.io standard (along with battery level) + +This can be used to integrate with [Home Assistant](https://www.home-assistant.io/), so you can use your Bangle as a wireless temperature/pressure sensor. + +More info on the standard at https://bthome.io + +And the data format used is https://bthome.io/format/ diff --git a/apps/bthometemp/app-icon.js b/apps/bthometemp/app-icon.js new file mode 100644 index 000000000..e2dff3eb9 --- /dev/null +++ b/apps/bthometemp/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4kA///1N6BIPf//1gMIwdE8sG2me+9Y/8C/2snXsoUNpdnzdt/xj/AH4AYgMRAAUQCyoYSCQNXs1muoFBFyHm1X//+qtwwPiMX1+YmczxP6uIwNFwN6yeDnGDmc504wNFwOpnGYC4OJweaGBsR9WTmYtBmc4GAOuC5ZGBt4SBAAQEBwf2JBcBiupnIuCmedxGTzVRC5cX1AuDnPZF4OKuIXLi3zIoedMgMzn9hC5uICQON5IDBxAXSznYC6RdDPQYXNO4JcB7pdCO56nBnGZ7p6DU5zXBXgSqDa5sAiPqIgOZd4c510RCxQXBi+pRQIXBxODzVxC5hIBvR1DnE505GMGAevzAvC/QuNGAfm1X//+qtwuOGAURq9ms11AoIWOGAQAEFw1EDBwWFggBCkUgAQMigUAAIIAJoABDCgIXQFwYXBCYYBDHAMCEAIkCFgcEAIIKCCoQFCkAhBAQIlCkAsBOoIXCBoIvEAwQTCAYI2BIwgXIF4YXDQwIVCC4YIBMIwfCAQRfGYBSPNC6TBFACgwBACouWAH4AiA=")) diff --git a/apps/bthometemp/app.js b/apps/bthometemp/app.js new file mode 100644 index 000000000..7b55777d1 --- /dev/null +++ b/apps/bthometemp/app.js @@ -0,0 +1,58 @@ +// history of temperature/pressure readings +var history = []; + +// When we get temperature... +function onTemperature(p) { + // Average the last 5 temperature readings + while (history.length>4) history.shift(); + history.push(p); + var avrTemp = history.reduce((i,h)=>h.temperature+i,0) / history.length; + var avrPressure = history.reduce((i,h)=>h.pressure+i,0) / history.length; + var t = require('locale').temp(avrTemp).replace("'","°"); + // Draw + var rect = Bangle.appRect; + g.reset(1).clearRect(rect.x, rect.y, rect.x2, rect.y2); + var x = (rect.x+rect.x2)/2; + var y = (rect.y+rect.y2)/2 + 10; + g.setFont("6x15").setFontAlign(0,0).drawString("Temperature:", x, y - 65); + g.setFontVector(50).setFontAlign(0,0).drawString(t, x, y-25); + g.setFont("6x15").setFontAlign(0,0).drawString("Pressure:", x, y+15 ); + g.setFont("12x20").setFontAlign(0,0).drawString(Math.round(avrPressure)+" hPa", x, y+40); + // Set Bluetooth Advertising + // https://bthome.io/format/ + var temp100 = Math.round(avrTemp*100); + var pressure100 = Math.round(avrPressure*100); + + Bangle.bleAdvert[0xFCD2] = [ 0x40, /* BTHome Device Information + bit 0: "Encryption flag" + bit 1-4: "Reserved for future use" + bit 5-7: "BTHome Version" */ + + 0x01, // Battery, 8 bit + E.getBattery(), + + 0x02, // Temperature, 16 bit + temp100&255,temp100>>8, + + 0x04, // Pressure, 16 bit + pressure100&255,(pressure100>>8)&255,pressure100>>16 + ]; + NRF.setAdvertising(Bangle.bleAdvert); +} + +// Gets the temperature in the most accurate way with pressure sensor +function drawTemperature() { + Bangle.getPressure().then(p =>{if (p) onTemperature(p);}); +} + +if (!Bangle.bleAdvert) Bangle.bleAdvert = {}; +setInterval(function() { + drawTemperature(); +}, 10000); // update every 10s +Bangle.loadWidgets(); +Bangle.setUI({ + mode : "custom", + back : function() {load();} +}); +E.showMessage("Reading temperature..."); +drawTemperature(); diff --git a/apps/bthometemp/app.png b/apps/bthometemp/app.png new file mode 100644 index 000000000..6c8eb3f14 Binary files /dev/null and b/apps/bthometemp/app.png differ diff --git a/apps/bthometemp/metadata.json b/apps/bthometemp/metadata.json new file mode 100644 index 000000000..4bfd08c31 --- /dev/null +++ b/apps/bthometemp/metadata.json @@ -0,0 +1,14 @@ +{ "id": "bthometemp", + "name": "BTHome Temperature and Pressure", + "shortName":"BTHome T", + "version":"0.01", + "description": "Displays temperature and pressure, and advertises them over bluetooth using BTHome.io standard", + "icon": "app.png", + "tags": "bthome,bluetooth,temperature", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"bthometemp.app.js","url":"app.js"}, + {"name":"bthometemp.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/bthrm/ChangeLog b/apps/bthrm/ChangeLog index 99cf0c670..000c5e3f8 100644 --- a/apps/bthrm/ChangeLog +++ b/apps/bthrm/ChangeLog @@ -40,3 +40,4 @@ 0.16: Set powerdownRequested correctly on BTHRM power on Additional logging on errors Add debug option for disabling active scanning +0.17: New GUI based on layout library diff --git a/apps/bthrm/bthrm.js b/apps/bthrm/bthrm.js index fadf2a5d8..b07e7bd37 100644 --- a/apps/bthrm/bthrm.js +++ b/apps/bthrm/bthrm.js @@ -1,5 +1,5 @@ -var intervalInt; -var intervalBt; +const BPM_FONT_SIZE="19%"; +const VALUE_TIMEOUT=3000; var BODY_LOCS = { 0: 'Other', @@ -7,46 +7,119 @@ var BODY_LOCS = { 2: 'Wrist', 3: 'Finger', 4: 'Hand', - 5: 'Ear Lobe', + 5: 'Earlobe', 6: 'Foot', +}; + +var Layout = require("Layout"); + +function border(l,c) { + g.setColor(c).drawLine(l.x+l.w*0.05, l.y-4, l.x+l.w*0.95, l.y-4); } -function clear(y){ - g.reset(); - g.clearRect(0,y,g.getWidth(),y+75); -} - -function draw(y, type, event) { - clear(y); - var px = g.getWidth()/2; - var str = event.bpm + ""; - g.reset(); - g.setFontAlign(0,0); - g.setFontVector(40).drawString(str,px,y+20); - str = "Event: " + type; - if (type === "HRM") { - str += " Confidence: " + event.confidence; - g.setFontVector(12).drawString(str,px,y+40); - str = " Source: " + (event.src ? event.src : "internal"); - g.setFontVector(12).drawString(str,px,y+50); +function getRow(id, text, additionalInfo){ + let additional = []; + let l = { + type:"h", c: [ + { + type:"v", + width: g.getWidth()*0.4, + c: [ + {type:"txt", halign:1, font:"8%", label:text, id:id+"text" }, + {type:"txt", halign:1, font:BPM_FONT_SIZE, label:"--", id:id, bgCol: g.theme.bg } + ] + },{ + type:undefined, fillx:1 + },{ + type:"v", + valign: -1, + width: g.getWidth()*0.45, + c: additional + },{ + type:undefined, width:g.getWidth()*0.05 + } + ] + }; + for (let i of additionalInfo){ + let label = {type:"txt", font:"6x8", label:i + ":" }; + let value = {type:"txt", font:"6x8", label:"--", id:id + i }; + additional.push({type:"h", halign:-1, c:[ label, {type:undefined, fillx:1}, value ]}); } - if (type === "BTHRM"){ - if (event.battery) str += " Bat: " + (event.battery ? event.battery : ""); - g.setFontVector(12).drawString(str,px,y+40); - str= ""; - if (event.location) str += "Loc: " + BODY_LOCS[event.location]; - if (event.rr && event.rr.length > 0) str += " RR: " + event.rr.join(","); - g.setFontVector(12).drawString(str,px,y+50); - str= ""; - if (event.contact) str += " Contact: " + event.contact; - if (event.energy) str += " kJoule: " + event.energy.toFixed(0); - g.setFontVector(12).drawString(str,px,y+60); + + return l; +} + +var layout = new Layout( { + type:"v", c: [ + getRow("int", "INT", ["Confidence"]), + getRow("agg", "HRM", ["Confidence", "Source"]), + getRow("bt", "BT", ["Battery","Location","Contact", "RR", "Energy"]), + { type:undefined, height:8 } //dummy to protect debug output + ] +}, { + lazy:true +}); + +var int,agg,bt; +var firstEvent = true; + +function draw(){ + if (!(int || agg || bt)) return; + + if (firstEvent) { + g.clearRect(Bangle.appRect); + firstEvent = false; + } + + let now = Date.now(); + + if (int && int.time > (now - VALUE_TIMEOUT)){ + layout.int.label = int.bpm; + if (!isNaN(int.confidence)) layout.intConfidence.label = int.confidence; + } else { + layout.int.label = "--"; + layout.intConfidence.label = "--"; + } + + if (agg && agg.time > (now - VALUE_TIMEOUT)){ + layout.agg.label = agg.bpm; + if (!isNaN(agg.confidence)) layout.aggConfidence.label = agg.confidence; + if (agg.src) layout.aggSource.label = agg.src; + } else { + layout.agg.label = "--"; + layout.aggConfidence.label = "--"; + layout.aggSource.label = "--"; + } + + if (bt && bt.time > (now - VALUE_TIMEOUT)) { + layout.bt.label = bt.bpm; + if (!isNaN(bt.battery)) layout.btBattery.label = bt.battery + "%"; + if (bt.rr) layout.btRR.label = bt.rr.join(","); + if (!isNaN(bt.location)) layout.btLocation.label = BODY_LOCS[bt.location]; + if (bt.contact !== undefined) layout.btContact.label = bt.contact ? "Yes":"No"; + if (!isNaN(bt.energy)) layout.btEnergy.label = bt.energy.toFixed(0) + "kJ"; + } else { + layout.bt.label = "--"; + layout.btBattery.label = "--"; + layout.btRR.label = "--"; + layout.btLocation.label = "--"; + layout.btContact.label = "--"; + layout.btEnergy.label = "--"; + } + + layout.update(); + layout.render(); + let first = true; + for (let c of layout.l.c){ + if (first) { + first = false; + continue; + } + if (c.type && c.type == "h") + border(c,g.theme.fg); } } -var firstEventBt = true; -var firstEventInt = true; - // This can get called for the boot code to show what's happening function showStatusInfo(txt) { @@ -57,41 +130,26 @@ function showStatusInfo(txt) { } function onBtHrm(e) { - if (firstEventBt){ - clear(24); - firstEventBt = false; - } - draw(100, "BTHRM", e); - if (e.bpm === 0){ - Bangle.buzz(100,0.2); - } - if (intervalBt){ - clearInterval(intervalBt); - } - intervalBt = setInterval(()=>{ - clear(100); - }, 2000); + bt = e; + bt.time = Date.now(); } -function onHrm(e) { - if (firstEventInt){ - clear(24); - firstEventInt = false; - } - draw(24, "HRM", e); - if (intervalInt){ - clearInterval(intervalInt); - } - intervalInt = setInterval(()=>{ - clear(24); - }, 2000); +function onInt(e) { + int = e; + int.time = Date.now(); } +function onAgg(e) { + agg = e; + agg.time = Date.now(); +} var settings = require('Storage').readJSON("bthrm.json", true) || {}; Bangle.on('BTHRM', onBtHrm); -Bangle.on('HRM', onHrm); +Bangle.on('HRM_int', onInt); +Bangle.on('HRM', onAgg); + Bangle.setHRMPower(1,'bthrm'); if (!(settings.startWithHrm)){ @@ -103,10 +161,11 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); if (Bangle.setBTHRMPower){ g.reset().setFont("6x8",2).setFontAlign(0,0); - g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 24); + g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2); + setInterval(draw, 1000); } else { g.reset().setFont("6x8",2).setFontAlign(0,0); - g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2 + 32); + g.drawString("BTHRM disabled",g.getWidth()/2,g.getHeight()/2); } E.on('kill', ()=>Bangle.setBTHRMPower(0,'bthrm')); diff --git a/apps/bthrm/lib.js b/apps/bthrm/lib.js index f5e0e1e5b..a792167ca 100644 --- a/apps/bthrm/lib.js +++ b/apps/bthrm/lib.js @@ -553,14 +553,15 @@ exports.enable = () => { if (settings.replace){ // register a listener for original HRM events and emit as HRM_int - Bangle.on("HRM", (e) => { - e.modified = true; + Bangle.on("HRM", (o) => { + let e = Object.assign({},o); log("Emitting HRM_int", e); Bangle.emit("HRM_int", e); if (fallbackActive){ // if fallback to internal HRM is active, emit as HRM_R to which everyone listens - log("Emitting HRM_R(int)", e); - Bangle.emit("HRM_R", e); + o.src = "int"; + log("Emitting HRM_R(int)", o); + Bangle.emit("HRM_R", o); } }); @@ -576,6 +577,13 @@ exports.enable = () => { if (name == "HRM") o("HRM_R", cb); else o(name, cb); })(Bangle.removeListener); + } else { + Bangle.on("HRM", (o)=>{ + o.src = "int"; + let e = Object.assign({},o); + log("Emitting HRM_int", e); + Bangle.emit("HRM_int", e); + }); } Bangle.origSetHRMPower = Bangle.setHRMPower; diff --git a/apps/bthrm/metadata.json b/apps/bthrm/metadata.json index 18c34ea33..fea274ff3 100644 --- a/apps/bthrm/metadata.json +++ b/apps/bthrm/metadata.json @@ -2,9 +2,10 @@ "id": "bthrm", "name": "Bluetooth Heart Rate Monitor", "shortName": "BT HRM", - "version": "0.16", + "version": "0.17", "description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.", "icon": "app.png", + "screenshots": [{"url":"screen.png"}], "type": "app", "tags": "health,bluetooth,hrm,bthrm", "supports": ["BANGLEJS","BANGLEJS2"], diff --git a/apps/bthrm/screen.png b/apps/bthrm/screen.png new file mode 100644 index 000000000..6b6b85227 Binary files /dev/null and b/apps/bthrm/screen.png differ diff --git a/apps/bwclk/ChangeLog b/apps/bwclk/ChangeLog index 546c83894..e3e059318 100644 --- a/apps/bwclk/ChangeLog +++ b/apps/bwclk/ChangeLog @@ -20,4 +20,5 @@ 0.20: Better handling of async data such as getPressure. 0.21: On the default menu the week of year can be shown. 0.22: Use the new clkinfo module for the menu. -0.23: Feedback of apps after run is now optional and decided by the corresponding clkinfo. \ No newline at end of file +0.23: Feedback of apps after run is now optional and decided by the corresponding clkinfo. +0.24: Update clock_info to avoid a redraw diff --git a/apps/bwclk/app.js b/apps/bwclk/app.js index 7dcca9d75..c29fdf2ef 100644 --- a/apps/bwclk/app.js +++ b/apps/bwclk/app.js @@ -93,7 +93,7 @@ var bwItems = { items: [ { name: "WeekOfYear", get: () => ({ text: "Week " + weekOfYear(), img: null}), - show: function() { bwItems.items[0].emit("redraw"); }, + show: function() {}, hide: function () {} }, ] diff --git a/apps/bwclk/metadata.json b/apps/bwclk/metadata.json index fa0f7b01f..8ef812f41 100644 --- a/apps/bwclk/metadata.json +++ b/apps/bwclk/metadata.json @@ -1,7 +1,7 @@ { "id": "bwclk", "name": "BW Clock", - "version": "0.23", + "version": "0.24", "description": "A very minimalistic clock to mainly show date and time.", "readme": "README.md", "icon": "app.png", diff --git a/apps/calclock/ChangeLog b/apps/calclock/ChangeLog index f4a0c96f5..90bcfb9d4 100644 --- a/apps/calclock/ChangeLog +++ b/apps/calclock/ChangeLog @@ -2,3 +2,5 @@ 0.02: More compact rendering & app icon 0.03: Tell clock widgets to hide. 0.04: Improve current time readability in light theme. +0.05: Show calendar colors & improved all day events. +0.06: Improved multi-line locations & titles diff --git a/apps/calclock/calclock.js b/apps/calclock/calclock.js index a55dc05f9..1f98502ef 100644 --- a/apps/calclock/calclock.js +++ b/apps/calclock/calclock.js @@ -20,41 +20,57 @@ function zp(str) { } function drawEventHeader(event, y) { - g.setFont("Vector", 24); - + var x = 0; var time = isActive(event) ? new Date() : new Date(event.timestamp * 1000); - var timeStr = zp(time.getHours()) + ":" + zp(time.getMinutes()); - g.drawString(timeStr, 5, y); - y += 24; + + //Don't need to know what time the event is at if its all day + if (isActive(event) || !event.allDay) { + g.setFont("Vector", 24); + var timeStr = zp(time.getHours()) + ":" + zp(time.getMinutes()); + g.drawString(timeStr, 0, y); + y += 3; + x = 13*timeStr.length+5; + } g.setFont("12x20", 1); + if (isActive(event)) { - g.drawString(zp(time.getDate())+". " + require("locale").month(time,1),15*timeStr.length,y-21); + g.drawString(zp(time.getDate())+". " + require("locale").month(time,1),x,y); } else { var offset = 0-time.getTimezoneOffset()/1440; var days = Math.floor((time.getTime()/1000)/86400+offset)-Math.floor(getTime()/86400+offset); - if(days > 0) { + if(days > 0 || event.allDay) { var daysStr = days===1?/*LANG*/"tomorrow":/*LANG*/"in "+days+/*LANG*/" days"; - g.drawString(daysStr,15*timeStr.length,y-21); + g.drawString(daysStr,x,y); } } + y += 21; return y; } function drawEventBody(event, y) { g.setFont("12x20", 1); - var lines = g.wrapString(event.title, g.getWidth()-10); + var lines = g.wrapString(event.title, g.getWidth()-15); + var yStart = y; if (lines.length > 2) { lines = lines.slice(0,2); - lines[1] = lines[1].slice(0,-3)+"..."; + lines[1] += "..."; } - g.drawString(lines.join('\n'), 5, y); + g.drawString(lines.join('\n'),10,y); y+=20 * lines.length; if(event.location) { - g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),5,y); - g.drawString(event.location, 20, y); + g.drawImage(atob("DBSBAA8D/H/nDuB+B+B+B3Dn/j/B+A8A8AYAYAYAAAAAAA=="),10,y); + var loclines = g.wrapString(event.location, g.getWidth()-30); + if(loclines.length>1) loclines[0] += "..."; + g.drawString(loclines[0],25,y); y+=20; } + if (event.color) { + var oldColor = g.getColor(); + g.setColor("#"+(0x1000000+Number(event.color)).toString(16).padStart(6,"0")); + g.fillRect(0,yStart,5,y-3); + g.setColor(oldColor); + } y+=5; return y; } @@ -68,19 +84,19 @@ function drawEvent(event, y) { var curEventHeight = 0; function drawCurrentEvents(y) { - g.setColor(g.theme.dark ? "#0ff" : "#0000ff"); - g.clearRect(5, y, g.getWidth() - 5, y + curEventHeight); + g.setColor(g.theme.dark ? "#0ff" : "#00f"); + g.clearRect(0,y,g.getWidth()-5,y+curEventHeight); curEventHeight = y; if(current.length === 0) { y = drawEvent({timestamp: getTime(), durationInSeconds: 100}, y); } else { - y = drawEventHeader(current[0], y); + y = drawEventHeader(current[0],y); for (var e of current) { - y = drawEventBody(e, y); + y = drawEventBody(e,y); } } - curEventHeight = y - curEventHeight; + curEventHeight = y-curEventHeight; return y; } @@ -94,7 +110,7 @@ function drawFutureEvents(y) { } function fullRedraw() { - g.clearRect(5,24,g.getWidth()-5,g.getHeight()); + g.clearRect(0,24,g.getWidth()-5,g.getHeight()); updateCalendar(); var y = 30; y = drawCurrentEvents(y); diff --git a/apps/calclock/metadata.json b/apps/calclock/metadata.json index 3aab55186..bfd847595 100644 --- a/apps/calclock/metadata.json +++ b/apps/calclock/metadata.json @@ -2,7 +2,7 @@ "id": "calclock", "name": "Calendar Clock", "shortName": "CalClock", - "version": "0.04", + "version": "0.06", "description": "Show the current and upcoming events synchronized from Gadgetbridge", "icon": "calclock.png", "type": "clock", diff --git a/apps/calclock/screenshot.patch b/apps/calclock/screenshot.patch new file mode 100644 index 000000000..3fdbf79d1 --- /dev/null +++ b/apps/calclock/screenshot.patch @@ -0,0 +1,32 @@ +diff --git a/apps/calclock/calclock.js b/apps/calclock/calclock.js +index cb8c6100e..2092c1a4e 100644 +--- a/apps/calclock/calclock.js ++++ b/apps/calclock/calclock.js +@@ -3,9 +3,24 @@ var current = []; + var next = []; + + function updateCalendar() { +- calendar = require("Storage").readJSON("android.calendar.json",true)||[]; +- calendar = calendar.filter(e => isActive(e) || getTime() <= e.timestamp); +- calendar.sort((a,b) => a.timestamp - b.timestamp); ++ calendar = [ ++ { ++ t: "calendar", ++ id: 2, type: 0, timestamp: getTime(), durationInSeconds: 200, ++ title: "Capture Screenshot", ++ description: "Capture Screenshot", ++ location: "", ++ calName: "", ++ color: -7151168, allDay: true }, ++ { ++ t: "calendar", ++ id: 7186, type: 0, timestamp: getTime() + 2000, durationInSeconds: 100, ++ title: "Upload to BangleApps", ++ description: "", ++ location: "", ++ calName: "", ++ color: -509406, allDay: false } ++ ]; + + current = calendar.filter(isActive); + next = calendar.filter(e=>!isActive(e)); diff --git a/apps/calclock/screenshot.png b/apps/calclock/screenshot.png index 4ab503f2b..8b2e39784 100644 Binary files a/apps/calclock/screenshot.png and b/apps/calclock/screenshot.png differ diff --git a/apps/calibration/metadata.json b/apps/calibration/metadata.json index b60650300..f428bd538 100644 --- a/apps/calibration/metadata.json +++ b/apps/calibration/metadata.json @@ -3,7 +3,7 @@ "shortName":"Calibration", "icon": "calibration.png", "version":"0.03", - "description": "A simple calibration app for the touchscreen", + "description": "(NOT RECOMMENDED) A simple calibration app for the touchscreen. Please use the Touchscreen Calibration in the Settings app instead.", "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "tags": "tool", diff --git a/apps/choozi/ChangeLog b/apps/choozi/ChangeLog index 03f7ef832..35adc7430 100644 --- a/apps/choozi/ChangeLog +++ b/apps/choozi/ChangeLog @@ -1,3 +1,9 @@ 0.01: New App! 0.02: Support Bangle.js 2 0.03: Fix bug for Bangle.js 2 where g.flip was not being called. +0.04: Combine code for both apps + Better colors for Bangle.js 2 + Fix selection animation for Bangle.js 2 + New icon + Slightly wider arc segments for better visibility + Extract arc drawing code in library diff --git a/apps/choozi/README.md b/apps/choozi/README.md index f1e4255bc..ccaa97a27 100644 --- a/apps/choozi/README.md +++ b/apps/choozi/README.md @@ -11,16 +11,21 @@ the players seated in a circle, set the number of segments equal to the number of players, ensure that each person knows which colour represents them, and then choose a segment. After a short animation, the chosen segment will fill the screen. -You can use Choozi to randomly select an element from any set with 2 to 13 members, +You can use Choozi to randomly select an element from any set with 2 to 15 members, as long as you can define a bijection between members of the set and coloured segments on the Bangle.js display. -## Controls +## Controls Bangle 1 BTN1: increase the number of segments BTN2: choose a segment at random BTN3: decrease the number of segments +## Controls Bangle 2 + +Swipe up/down: increase/decrease the number of segments +BTN1 or tap: choose a segment at random + ## Creator James Stanley diff --git a/apps/choozi/app-icon.js b/apps/choozi/app-icon.js index 51b3bead3..560286098 100644 --- a/apps/choozi/app-icon.js +++ b/apps/choozi/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwggLIrnM4uqAAIhPgvMAAPFzIABzWgCxkMCweqC4QABDBYtC5QVFDBoWCCo5KLOQIWKDARFICxhJIFwOpC5owFFyAwGUYIuOGAwuRC4guSJAgXBCyIwDIyQXF5IXSzJeVMAReUAAOQhheTMAVcC6yOUC4aOUC7GZUyoXXzWqhQXVxGqC9mYC7OqC9eoxEKC6uBC6uIwAXBPCSmBwEAC6Z2BiAXBJCR2BgEAjQXSlGBC4JgSLwYABJCJGBLwJIDGB+IIwRIDGByNBIwZIDGBhdBRoQwSLoIuFGAYYKCwIuGGAgYI1QWBRgYYJMYmaFoSMEAAyrBAAgVCCxgYGjAWQAAMBC4UILZQA==")) +require("heatshrink").decompress(atob("mEwwcH/4AW/u27dt2wQL/YOBCIXbv4QI+AODAQVsh4RHwEbCI0LCI9gCIOANAXbsFbG437tkDPg1btoRFFoILBgmSpMggECHQO/CAf2CIVJkgRBAQIjC24RFsECCItIgIRFMYMAiQRFpMAlqmDVwPYgAOEAQUggu274RD4BWCCIskCIPbCIPt20ABwwCCwARFgIRJyEWCIVt2EJCJi2BCJmSUgIRCwARNt/7CIIOICI1sWAwCFoFbCOtt8EACJsAgARR8hwBCJlJk4RlgARQAgIRKDwMn/gRBdJgRPyARBn4RBpARLiQRB/4RBgIRJwAREpIRLAYP///ypMgCJMACI0ECI4JCp4RB/wZECIsAAYN/CIP/5JPDCIhjDCIraHTIWTCAX//K7DCI+fCIf/EZA1CCAn//ipCLIsBk4RF/5ZHCIIQG//wPo8vCI//6QRFpYQIAAPpCIeXCBQAC/VfBI4=")) \ No newline at end of file diff --git a/apps/choozi/app.js b/apps/choozi/app.js index 1a5b2f17e..b9f53bc89 100644 --- a/apps/choozi/app.js +++ b/apps/choozi/app.js @@ -4,15 +4,16 @@ * * James Stanley 2021 */ - -var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff']; +const GU = require("graphics_utils"); +var colours = ['#ff0000', '#00ff00', '#0000ff', '#ffff00', '#00ffff', '#ff00ff', '#ffffff']; +var colours2 = ['#808080', '#404040', '#000040', '#004000', '#400000', '#ff8000', '#804000', '#4000c0']; var stepAngle = 0.18; // radians - resolution of polygon var gapAngle = 0.035; // radians - gap between segments -var perimMin = 110; // px - min. radius of perimeter -var perimMax = 120; // px - max. radius of perimeter +var perimMin = g.getWidth()*0.40; // px - min. radius of perimeter +var perimMax = g.getWidth()*0.49; // px - max. radius of perimeter -var segmentMax = 106; // px - max radius of filled-in segment +var segmentMax = g.getWidth()*0.38; // px - max radius of filled-in segment var segmentStep = 5; // px - step size of segment fill animation var circleStep = 4; // px - step size of circle fill animation @@ -22,10 +23,10 @@ var minSpeed = 0.001; // rad/sec var animStartSteps = 300; // how many steps before it can start slowing? var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate var ballSize = 3; // px - ball radius -var ballTrack = 100; // px - radius of ball path +var ballTrack = perimMin - ballSize*2; // px - radius of ball path -var centreX = 120; // px - centre of screen -var centreY = 120; // px - centre of screen +var centreX = g.getWidth()*0.5; // px - centre of screen +var centreY = g.getWidth()*0.5; // px - centre of screen var fontSize = 50; // px @@ -33,7 +34,6 @@ var radians = 2*Math.PI; // radians per circle var defaultN = 3; // default value for N var minN = 2; -var maxN = colours.length; var N; var arclen; @@ -51,42 +51,14 @@ function shuffle (array) { } } -// draw an arc between radii minR and maxR, and between -// angles minAngle and maxAngle -function arc(minR, maxR, minAngle, maxAngle) { - var step = stepAngle; - var angle = minAngle; - var inside = []; - var outside = []; - var c, s; - while (angle < maxAngle) { - c = Math.cos(angle); - s = Math.sin(angle); - inside.push(centreX+c*minR); // x - inside.push(centreY+s*minR); // y - // outside coordinates are built up in reverse order - outside.unshift(centreY+s*maxR); // y - outside.unshift(centreX+c*maxR); // x - angle += step; - } - c = Math.cos(maxAngle); - s = Math.sin(maxAngle); - inside.push(centreX+c*minR); - inside.push(centreY+s*minR); - outside.unshift(centreY+s*maxR); - outside.unshift(centreX+c*maxR); - - var vertices = inside.concat(outside); - g.fillPoly(vertices, true); -} - // draw the arc segments around the perimeter function drawPerimeter() { + g.setBgColor('#000000'); g.clear(); for (var i = 0; i < N; i++) { g.setColor(colours[i%colours.length]); var minAngle = (i/N)*radians; - arc(perimMin,perimMax,minAngle,minAngle+arclen); + GU.fillArc(g, centreX, centreY, perimMin,perimMax,minAngle,minAngle+arclen, stepAngle); } } @@ -131,6 +103,7 @@ function animateChoice(target) { g.fillCircle(x, y, ballSize); oldx=x; oldy=y; + if (process.env.HWVERSION == 2) g.flip(); } } @@ -141,11 +114,15 @@ function choose() { var maxAngle = minAngle + arclen; animateChoice((minAngle+maxAngle)/2); g.setColor(colours[chosen%colours.length]); - for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep) - arc(i, perimMax, minAngle, maxAngle); - arc(0, perimMax, minAngle, maxAngle); - for (var r = 1; r < segmentMax; r += circleStep) + for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep){ + GU.fillArc(g, centreX, centreY, i, perimMax, minAngle, maxAngle, stepAngle); + if (process.env.HWVERSION == 2) g.flip(); + } + GU.fillArc(g, centreX, centreY, 0, perimMax, minAngle, maxAngle, stepAngle); + for (var r = 1; r < segmentMax; r += circleStep){ g.fillCircle(centreX,centreY,r); + if (process.env.HWVERSION == 2) g.flip(); + } g.fillCircle(centreX,centreY,segmentMax); } @@ -171,38 +148,47 @@ function setN(n) { drawPerimeter(); } -// save N to choozi.txt +// save N to choozi.save function writeN() { - var file = require("Storage").open("choozi.txt","w"); - file.write(N); + var savedN = read(); + if (savedN != N) require("Storage").write("choozi.save","" + N); } -// load N from choozi.txt +function read(){ + var n = require("Storage").read("choozi.save"); + if (n !== undefined) return parseInt(n); + return defaultN; +} + +// load N from choozi.save function readN() { - var file = require("Storage").open("choozi.txt","r"); - var n = file.readLine(); - if (n !== undefined) setN(parseInt(n)); - else setN(defaultN); + setN(read()); } -shuffle(colours); // is this really best? -Bangle.setLCDMode("direct"); -Bangle.setLCDTimeout(0); // keep screen on +if (process.env.HWVERSION == 1){ + colours=colours.concat(colours2); + shuffle(colours); +} else { + shuffle(colours); + shuffle(colours2); + colours=colours.concat(colours2); +} + +var maxN = colours.length; +if (process.env.HWVERSION == 1){ + Bangle.setLCDMode("direct"); + Bangle.setLCDTimeout(0); // keep screen on +} readN(); drawN(); -setWatch(() => { - setN(N+1); - drawN(); -}, BTN1, {repeat:true}); - -setWatch(() => { - writeN(); - drawPerimeter(); - choose(); -}, BTN2, {repeat:true}); - -setWatch(() => { - setN(N-1); - drawN(); -}, BTN3, {repeat:true}); +Bangle.setUI("updown", (v)=>{ + if (!v){ + writeN(); + drawPerimeter(); + choose(); + } else { + setN(N-v); + drawN(); + } +}); diff --git a/apps/choozi/app.png b/apps/choozi/app.png index 99c9fa07a..50f09f164 100644 Binary files a/apps/choozi/app.png and b/apps/choozi/app.png differ diff --git a/apps/choozi/appb2.js b/apps/choozi/appb2.js deleted file mode 100644 index 5f217f638..000000000 --- a/apps/choozi/appb2.js +++ /dev/null @@ -1,207 +0,0 @@ -/* Choozi - Choose people or things at random using Bangle.js. - * Inspired by the "Chwazi" Android app - * - * James Stanley 2021 - */ - -var colours = ['#ff0000', '#ff8080', '#00ff00', '#80ff80', '#0000ff', '#8080ff', '#ffff00', '#00ffff', '#ff00ff', '#ff8000', '#ff0080', '#8000ff', '#0080ff']; - -var stepAngle = 0.18; // radians - resolution of polygon -var gapAngle = 0.035; // radians - gap between segments -var perimMin = 80; // px - min. radius of perimeter -var perimMax = 87; // px - max. radius of perimeter - -var segmentMax = 70; // px - max radius of filled-in segment -var segmentStep = 5; // px - step size of segment fill animation -var circleStep = 4; // px - step size of circle fill animation - -// rolling ball animation: -var maxSpeed = 0.08; // rad/sec -var minSpeed = 0.001; // rad/sec -var animStartSteps = 300; // how many steps before it can start slowing? -var accel = 0.0002; // rad/sec/sec - acc-/deceleration rate -var ballSize = 3; // px - ball radius -var ballTrack = 75; // px - radius of ball path - -var centreX = 88; // px - centre of screen -var centreY = 88; // px - centre of screen - -var fontSize = 50; // px - -var radians = 2*Math.PI; // radians per circle - -var defaultN = 3; // default value for N -var minN = 2; -var maxN = colours.length; -var N; -var arclen; - -// https://www.frankmitchell.org/2015/01/fisher-yates/ -function shuffle (array) { - var i = 0 - , j = 0 - , temp = null; - - for (i = array.length - 1; i > 0; i -= 1) { - j = Math.floor(Math.random() * (i + 1)); - temp = array[i]; - array[i] = array[j]; - array[j] = temp; - } -} - -// draw an arc between radii minR and maxR, and between -// angles minAngle and maxAngle -function arc(minR, maxR, minAngle, maxAngle) { - var step = stepAngle; - var angle = minAngle; - var inside = []; - var outside = []; - var c, s; - while (angle < maxAngle) { - c = Math.cos(angle); - s = Math.sin(angle); - inside.push(centreX+c*minR); // x - inside.push(centreY+s*minR); // y - // outside coordinates are built up in reverse order - outside.unshift(centreY+s*maxR); // y - outside.unshift(centreX+c*maxR); // x - angle += step; - } - c = Math.cos(maxAngle); - s = Math.sin(maxAngle); - inside.push(centreX+c*minR); - inside.push(centreY+s*minR); - outside.unshift(centreY+s*maxR); - outside.unshift(centreX+c*maxR); - - var vertices = inside.concat(outside); - g.fillPoly(vertices, true); -} - -// draw the arc segments around the perimeter -function drawPerimeter() { - g.clear(); - for (var i = 0; i < N; i++) { - g.setColor(colours[i%colours.length]); - var minAngle = (i/N)*radians; - arc(perimMin,perimMax,minAngle,minAngle+arclen); - } -} - -// animate a ball rolling around and settling at "target" radians -function animateChoice(target) { - var angle = 0; - var speed = 0; - var oldx = -10; - var oldy = -10; - var decelFromAngle = -1; - var allowDecel = false; - for (var i = 0; true; i++) { - angle = angle + speed; - if (angle > radians) angle -= radians; - if (i < animStartSteps || (speed < maxSpeed && !allowDecel)) { - speed = speed + accel; - if (speed > maxSpeed) { - speed = maxSpeed; - /* when we reach max speed, we know how long it takes - * to accelerate, and therefore how long to decelerate, so - * we can work out what angle to start decelerating from */ - if (decelFromAngle < 0) { - decelFromAngle = target-angle; - while (decelFromAngle < 0) decelFromAngle += radians; - while (decelFromAngle > radians) decelFromAngle -= radians; - } - } - } else { - if (!allowDecel && (angle < decelFromAngle) && (angle+speed >= decelFromAngle)) allowDecel = true; - if (allowDecel) speed = speed - accel; - if (speed < minSpeed) speed = minSpeed; - if (speed == minSpeed && angle < target && angle+speed >= target) return; - } - - var r = i/2; - if (r > ballTrack) r = ballTrack; - var x = centreX+Math.cos(angle)*r; - var y = centreY+Math.sin(angle)*r; - g.setColor('#000000'); - g.fillCircle(oldx,oldy,ballSize+1); - g.setColor('#ffffff'); - g.fillCircle(x, y, ballSize); - oldx=x; - oldy=y; - g.flip(); - } -} - -// choose a winning segment and animate its selection -function choose() { - var chosen = Math.floor(Math.random()*N); - var minAngle = (chosen/N)*radians; - var maxAngle = minAngle + arclen; - animateChoice((minAngle+maxAngle)/2); - g.setColor(colours[chosen%colours.length]); - for (var i = segmentMax-segmentStep; i >= 0; i -= segmentStep) - arc(i, perimMax, minAngle, maxAngle); - arc(0, perimMax, minAngle, maxAngle); - for (var r = 1; r < segmentMax; r += circleStep) - g.fillCircle(centreX,centreY,r); - g.fillCircle(centreX,centreY,segmentMax); -} - -// draw the current value of N in the middle of the screen, with -// up/down arrows -function drawN() { - g.setColor(g.theme.fg); - g.setFont("Vector",fontSize); - g.drawString(N,centreX-g.stringWidth(N)/2+4,centreY-fontSize/2); - if (N < maxN) - g.fillPoly([centreX-6,centreY-fontSize/2-7, centreX+6,centreY-fontSize/2-7, centreX, centreY-fontSize/2-14]); - if (N > minN) - g.fillPoly([centreX-6,centreY+fontSize/2+5, centreX+6,centreY+fontSize/2+5, centreX, centreY+fontSize/2+12]); -} - -// update number of segments, with min/max limit, "arclen" update, -// and screen reset -function setN(n) { - N = n; - if (N < minN) N = minN; - if (N > maxN) N = maxN; - arclen = radians/N - gapAngle; - drawPerimeter(); -} - -// save N to choozi.txt -function writeN() { - var file = require("Storage").open("choozi.txt","w"); - file.write(N); -} - -// load N from choozi.txt -function readN() { - var file = require("Storage").open("choozi.txt","r"); - var n = file.readLine(); - if (n !== undefined) setN(parseInt(n)); - else setN(defaultN); -} - -shuffle(colours); // is this really best? -Bangle.setLCDTimeout(0); // keep screen on -readN(); -drawN(); - -setWatch(() => { - writeN(); - drawPerimeter(); - choose(); -}, BTN1, {repeat:true}); - -Bangle.on('touch', function(zone,e) { - if(e.x>+88){ - setN(N-1); - drawN(); - }else{ - setN(N+1); - drawN(); - } -}); diff --git a/apps/choozi/bangle1-choozi-screenshot1.png b/apps/choozi/bangle1-choozi-screenshot1.png index 104024958..ee422ed10 100644 Binary files a/apps/choozi/bangle1-choozi-screenshot1.png and b/apps/choozi/bangle1-choozi-screenshot1.png differ diff --git a/apps/choozi/bangle1-choozi-screenshot2.png b/apps/choozi/bangle1-choozi-screenshot2.png index f3b6868bf..20edf4c78 100644 Binary files a/apps/choozi/bangle1-choozi-screenshot2.png and b/apps/choozi/bangle1-choozi-screenshot2.png differ diff --git a/apps/choozi/metadata.json b/apps/choozi/metadata.json index 79af76fa2..c42abe079 100644 --- a/apps/choozi/metadata.json +++ b/apps/choozi/metadata.json @@ -1,7 +1,7 @@ { "id": "choozi", "name": "Choozi", - "version": "0.03", + "version": "0.04", "description": "Choose people or things at random using Bangle.js.", "icon": "app.png", "tags": "tool", @@ -10,8 +10,10 @@ "allow_emulator": true, "screenshots": [{"url":"bangle1-choozi-screenshot1.png"},{"url":"bangle1-choozi-screenshot2.png"}], "storage": [ - {"name":"choozi.app.js","url":"app.js","supports": ["BANGLEJS"]}, - {"name":"choozi.app.js","url":"appb2.js","supports": ["BANGLEJS2"]}, + {"name":"choozi.app.js","url":"app.js"}, {"name":"choozi.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"choozi.save"} ] } diff --git a/apps/circlesclock/ChangeLog b/apps/circlesclock/ChangeLog index ea7266442..83abde6df 100644 --- a/apps/circlesclock/ChangeLog +++ b/apps/circlesclock/ChangeLog @@ -31,3 +31,11 @@ 0.16: Fix const error Use widget_utils if available 0.17: Load circles from clkinfo +0.18: Improved clkinfo handling and using it for the weather circle +0.19: Remove old code and fixing clkinfo handling (fix HRM and other items that change) + Remove settings for what is displayed and instead allow circles to be changed by swiping +0.20: Add much faster circle rendering (250ms -> 40ms) + Add fast load capability +0.21: Remade all icons without a palette for dark theme + Now re-adds widgets if they were hidden when fast-loading +0.22: Fixed crash if item has no image and cutting long overflowing text diff --git a/apps/circlesclock/README.md b/apps/circlesclock/README.md index 8c8fbe4ae..7f6a2585c 100644 --- a/apps/circlesclock/README.md +++ b/apps/circlesclock/README.md @@ -5,6 +5,7 @@ A clock with three or four circles for different data at the bottom in a probabl By default the time, date and day of week is shown. It can show the following information (this can be configured): + * Steps * Steps distance * Heart rate (automatically updates when screen is on and unlocked) @@ -14,15 +15,24 @@ It can show the following information (this can be configured): * Temperature inside circle * Condition as icon below circle * Big weather icon next to clock - * Time and progress until next sunrise or sunset (requires [my location app](https://banglejs.com/apps/#mylocation)) - * Temperature, air pressure or altitude from internal pressure sensor + * Altitude from internal pressure sensor + * Active alarms (if `Alarm` app installed) + * Sunrise or sunset (if `Sunrise Clockinfo` app installed) +To change what is shown: -The color of each circle can be configured. The following colors are available: +* Unlock the watch +* Tap on the circle to change (a border is drawn around it) +* Swipe up/down to change the guage within the given group +* Swipe left/right to change the group (eg. between standard Bangle.js and Alarms/etc) + +Data is provided by ['Clock Info'](http://www.espruino.com/Bangle.js+Clock+Info) +so any apps that implement this feature can add extra information to be displayed. + +The color of each circle can be configured from `Settings -> Apps -> Circles Clock`. The following colors are available: * Basic colors (red, green, blue, yellow, magenta, cyan, black, white) * Color depending on value (green -> red, red -> green) - ## Screenshots ![Screenshot dark theme](screenshot-dark.png) ![Screenshot light theme](screenshot-light.png) @@ -38,5 +48,5 @@ The color of each circle can be configured. The following colors are available: Marco ([myxor](https://github.com/myxor)) ## Icons -Most of the icons are taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 except the big weather icons which are from +Most of the icons are taken from [materialdesignicons](https://materialdesignicons.com) under Apache License 2.0 except the big weather icons which are from [icons8](https://icons8.com/icon/set/weather/small--static--black) diff --git a/apps/circlesclock/app.js b/apps/circlesclock/app.js index d4a170ce8..30d6a48f4 100644 --- a/apps/circlesclock/app.js +++ b/apps/circlesclock/app.js @@ -1,6 +1,3 @@ -let clock_info = require("clock_info"); -let locale = require("locale"); -let storage = require("Storage"); Graphics.prototype.setFontRobotoRegular50NumericOnly = function(scale) { // Actual height 39 (40 - 2) this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAAAB8AAAAAAAfAAAAAAAPwAAAAAAB8AAAAAAAeAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAA4AAAAAAB+AAAAAAD/gAAAAAD/4AAAAAH/4AAAAAP/wAAAAAP/gAAAAAf/gAAAAAf/AAAAAA/+AAAAAB/+AAAAAB/8AAAAAD/4AAAAAH/4AAAAAD/wAAAAAA/wAAAAAAPgAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA///wAAAB////gAAA////8AAA/////gAAP////8AAH8AAA/gAB8AAAD4AA+AAAAfAAPAAAADwADwAAAA8AA8AAAAPAAPAAAADwADwAAAA8AA8AAAAPAAPgAAAHwAB8AAAD4AAfwAAD+AAD/////AAA/////wAAH////4AAAf///4AAAB///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAAAAAAPgAAAAAADwAAAAAAB8AAAAAAAfAAAAAAAHgAAAAAAD4AAAAAAA+AAAAAAAPAAAAAAAH/////wAB/////8AA//////AAP/////wAD/////8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOAAAAAAAfgAADwAAP4AAB8AAH+AAA/AAD/gAAfwAB/AAAf8AAfAAAP/AAPgAAH7wAD4AAD88AA8AAB+PAAPAAA/DwADwAAfg8AA8AAPwPAAPAAH4DwADwAH8A8AA+AD+APAAPwB/ADwAB/D/gA8AAf//gAPAAD//wADwAAf/wAA8AAD/4AAPAAAHwAADwAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAADgAAAHwAA+AAAD8AAP4AAB/AAD/AAA/wAA/wAAf4AAD+AAHwAAAPgAD4APAB8AA+ADwAPAAPAA8ADwADwAPAA8AA8ADwAPAAPAA8ADwADwAfAA8AA8AH4APAAPgD+AHwAB8B/wD4AAf7/+B+AAD//v//AAA//x//wAAD/4P/4AAAf8B/4AAAAYAH4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAHwAAAAAAH8AAAAAAD/AAAAAAD/wAAAAAD/8AAAAAB/vAAAAAB/jwAAAAA/g8AAAAA/wPAAAAAfwDwAAAAf4A8AAAAf4APAAAAP8ADwAAAP8AA8AAAH8AAPAAAD/////8AA//////AAP/////wAD/////8AA//////AAAAAAPAAAAAAADwAAAAAAA8AAAAAAAPAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAB/APwAAH//wD+AAD//8A/wAA///AH+AAP//wAPgAD/B4AB8AA8A+AAfAAPAPAADwADwDwAA8AA8A8AAPAAPAPAADwADwD4AA8AA8A+AAPAAPAPwAHwADwD8AD4AA8AfwD+AAPAH///AADwA///wAA8AH//4AAPAAf/4AAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAD//+AAAAD///4AAAD////AAAB////4AAA/78D/AAAfw8AH4AAPweAA+AAD4PgAHwAB8DwAA8AAfA8AAPAAHgPAADwAD4DwAA8AA+A8AAPAAPAPgAHwADwD4AB8AA8AfgA+AAPAH+B/gAAAA///wAAAAH//4AAAAA//8AAAAAH/8AAAAAAP4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAAAAAAA8AAAAAAAPAAAAAAADwAAAAAAA8AAAABAAPAAAABwADwAAAB8AA8AAAB/AAPAAAB/wADwAAD/8AA8AAD/8AAPAAD/4AADwAD/4AAA8AD/4AAAPAH/wAAADwH/wAAAA8H/wAAAAPH/wAAAAD3/gAAAAA//gAAAAAP/gAAAAAD/gAAAAAA/AAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwA/4AAAH/Af/AAAH/8P/4AAD//n//AAA//7//4AAfx/+A+AAHwD+AHwAD4AfgB8AA8AHwAPAAPAA8ADwADwAPAA8AA8ADwAPAAPAA8ADwADwAfAA8AA+AH4AfAAHwD+AHwAB/D/4D4AAP/+/n+AAD//n//AAAf/w//gAAB/wH/wAAAHwA/4AAAAAABgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB+AAAAAAD/8AAAAAD//wAAAAB//+AAAAA///wAAAAf4H+APAAH4AfgDwAD8AB8A8AA+AAfAPAAPAADwDwADwAA8B8AA8AAPAfAAPAADwHgADwAA8D4AA+AAeB+AAHwAHg/AAB+ADwfgAAP8D4/4AAD////8AAAf///8AAAB///+AAAAP//+AAAAAP/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAOAAAB8AAHwAAAfgAD8AAAH4AA/AAAB8AAHwAAAOAAA4AAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("DRUcHBwcHBwcHBwcDA=="), 50+(scale<<8)+(1<<16)); @@ -13,12 +10,16 @@ Graphics.prototype.setFontRobotoRegular21 = function(scale) { return this; }; +{ +let clock_info = require("clock_info"); +let locale = require("locale"); +let storage = require("Storage"); + let SETTINGS_FILE = "circlesclock.json"; let settings = Object.assign( storage.readJSON("circlesclock.default.json", true) || {}, storage.readJSON(SETTINGS_FILE, true) || {} ); - //TODO deprecate this (and perhaps use in the clkinfo module) // Load step goal from health app and pedometer widget as fallback if (settings.stepGoal == undefined) { @@ -31,20 +32,10 @@ if (settings.stepGoal == undefined) { } } -let timerHrm; //TODO deprecate this let drawTimeout; - -/* - * Read location from myLocation app - */ -function getLocation() { - return storage.readJSON("mylocation.json", 1) || undefined; -} -let location = getLocation(); - -let showWidgets = settings.showWidgets || false; -let circleCount = settings.circleCount || 3; -let showBigWeather = settings.showBigWeather || false; +const showWidgets = settings.showWidgets || false; +const circleCount = settings.circleCount || 3; +const showBigWeather = settings.showBigWeather || false; let hrtValue; //TODO deprecate this let now = Math.round(new Date().getTime() / 1000); @@ -52,11 +43,6 @@ let now = Math.round(new Date().getTime() / 1000); // layout values: let colorFg = g.theme.dark ? '#fff' : '#000'; let colorBg = g.theme.dark ? '#000' : '#fff'; -let colorGrey = '#808080'; -let colorRed = '#ff0000'; -let colorGreen = '#008000'; -let colorBlue = '#0000ff'; -let colorYellow = '#ffff00'; let widgetOffset = showWidgets ? 24 : 0; let dowOffset = circleCount == 3 ? 20 : 22; // dow offset relative to date let h = g.getHeight() - widgetOffset; @@ -64,7 +50,7 @@ let w = g.getWidth(); let hOffset = (circleCount == 3 ? 34 : 30) - widgetOffset; let h1 = Math.round(1 * h / 5 - hOffset); let h2 = Math.round(3 * h / 5 - hOffset); -let h3 = Math.round(8 * h / 8 - hOffset - 3); // circle y position +let h3 = Math.round(8 * h / 8 - hOffset - 3); // circle middle y position /* * circle x positions @@ -87,54 +73,17 @@ let circlePosX = [ ]; let radiusOuter = circleCount == 3 ? 25 : 20; +let radiusBorder = radiusOuter+3; // absolute border of circles let radiusInner = circleCount == 3 ? 20 : 15; let circleFontSmall = circleCount == 3 ? "Vector:14" : "Vector:10"; let circleFont = circleCount == 3 ? "Vector:15" : "Vector:11"; let circleFontBig = circleCount == 3 ? "Vector:16" : "Vector:12"; let iconOffset = circleCount == 3 ? 6 : 8; -let defaultCircleTypes = ["Bangle/Steps", "Bangle/HRM", "Bangle/Battery", "weather"]; -let circleInfoNum = [ - 0, // circle1 - 0, // circle2 - 0, // circle3 - 0, // circle4 -]; -let circleItemNum = [ - 0, // circle1 - 1, // circle2 - 2, // circle3 - 3, // circle4 -]; -function hideWidgets() { - /* - * we are not drawing the widgets as we are taking over the whole screen - * so we will blank out the draw() functions of each widget and change the - * area to the top bar doesn't get cleared. - */ - if (WIDGETS && typeof WIDGETS === "object") { - for (let wd of WIDGETS) { - wd.draw = () => {}; - wd.area = ""; - } - } -} - -function draw() { - g.clear(true); - let widgetUtils; - - try { - widgetUtils = require("widget_utils"); - } catch (e) { - } - if (!showWidgets) { - if (widgetUtils) widgetUtils.hide(); else hideWidgets(); - } else { - if (widgetUtils) widgetUtils.show(); - Bangle.drawWidgets(); - } +let draw = function() { + let R = Bangle.appRect; + g.reset().clearRect(R.x,R.y, R.x2, h3-(radiusBorder+1)); g.setColor(colorBg); g.fillRect(0, widgetOffset, w, h2 + 22); @@ -176,118 +125,16 @@ function draw() { if (icon) g.drawImage(icon, w - 48, h1, {scale:0.75}); } - drawCircle(1); - drawCircle(2); - drawCircle(3); - if (circleCount >= 4) drawCircle(4); - queueDraw(); } -function drawCircle(index) { - let type = settings['circle' + index]; - if (!type) type = defaultCircleTypes[index - 1]; - let w = getCircleXPosition(type); - - switch (type) { - case "weather": - drawWeather(w); - break; - case "sunprogress": - case "sunProgress": - drawSunProgress(w); - break; - //TODO those are going to be deprecated, keep for backwards compatibility for now - //ideally all data should come from some clkinfo - case "steps": - drawSteps(w); - break; - case "stepsDist": - drawStepsDistance(w); - break; - case "hr": - drawHeartRate(w); - break; - case "battery": - drawBattery(w); - break; - case "temperature": - drawTemperature(w); - break; - case "pressure": - drawPressure(w); - break; - case "altitude": - drawAltitude(w); - break; - //end deprecated - case "empty": - // we draw nothing here - return; - default: - drawClkInfo(index, w); - } -} - -// serves as cache for quicker lookup of circle positions -let circlePositionsCache = []; -/* - * Looks in the following order if a circle with the given type is somewhere visible/configured - * 1. circlePositionsCache - * 2. settings - * 3. defaultCircleTypes - * - * In case 2 and 3 the circlePositionsCache will be updated - */ -function getCirclePosition(type) { - if (circlePositionsCache[type] >= 0) { - return circlePositionsCache[type]; - } - for (let i = 1; i <= circleCount; i++) { - let setting = settings['circle' + i]; - if (setting == type) { - circlePositionsCache[type] = i - 1; - return i - 1; - } - } - for (let i = 0; i < defaultCircleTypes.length; i++) { - if (type == defaultCircleTypes[i] && (!settings || settings['circle' + (i + 1)] == undefined)) { - circlePositionsCache[type] = i; - return i; - } - } - return undefined; -} - -function getCircleXPosition(type) { - let circlePos = getCirclePosition(type); - if (circlePos != undefined) { - return circlePosX[circlePos]; - } - return undefined; -} - -function isCircleEnabled(type) { - return getCirclePosition(type) != undefined; -} - -function getCircleColor(type) { - let pos = getCirclePosition(type); - let color = settings["circle" + (pos + 1) + "color"]; +let getCircleColor = function(index) { + let color = settings["circle" + index + "color"]; if (color && color != "") return color; + return g.theme.fg; } -function getCircleIconColor(type, color, percent) { - let pos = getCirclePosition(type); - let colorizeIcon = settings["circle" + (pos + 1) + "colorizeIcon"] == true; - if (colorizeIcon) { - return getGradientColor(color, percent); - } else { - return ""; - } -} - -function getGradientColor(color, percent) { +let getGradientColor = function(color, percent) { if (isNaN(percent)) percent = 0; if (percent > 1) percent = 1; let colorList = [ @@ -307,370 +154,16 @@ function getGradientColor(color, percent) { return color; } -function getImage(graphic, color) { - if (!color || color == "") { - return graphic; +let getCircleIconColor = function(index, color, percent) { + let colorizeIcon = settings["circle" + index + "colorizeIcon"] == true; + if (colorizeIcon) { + return getGradientColor(color, percent); } else { - return { - width: 16, - height: 16, - bpp: 1, - transparent: 0, - buffer: E.toArrayBuffer(graphic), - palette: new Uint16Array([colorBg, g.toColor(color)]) - }; + return g.theme.fg; } } -function drawWeather(w) { - if (!w) w = getCircleXPosition("weather"); - let weather = getWeather(); - let tempString = weather ? locale.temp(weather.temp - 273.15) : undefined; - let code = weather ? weather.code : -1; - - drawCircleBackground(w); - - let color = getCircleColor("weather"); - let percent; - let data = settings.weatherCircleData; - switch (data) { - case "humidity": - let humidity = weather ? weather.hum : undefined; - if (humidity >= 0) { - percent = humidity / 100; - drawGauge(w, h3, percent, color); - } - break; - case "wind": - if (weather) { - let wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); - if (wind[1] >= 0) { - if (wind[2] == "kmh") { - wind[1] = windAsBeaufort(wind[1]); - } - // wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale) - percent = wind[1] / 12; - drawGauge(w, h3, percent, color); - } - } - break; - case "empty": - break; - } - - drawInnerCircleAndTriangle(w); - - writeCircleText(w, tempString ? tempString : "?"); - - if (code > 0) { - let icon = getWeatherIconByCode(code); - if (icon) g.drawImage(getImage(icon, getCircleIconColor("weather", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); - } else { - g.drawString("?", w, h3 + radiusOuter); - } -} - -function drawSunProgress(w) { - if (!w) w = getCircleXPosition("sunprogress"); - let percent = getSunProgress(); - - // sunset icons: - let sunSetDown = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAAGYAPAAYAAAAAA"); - let sunSetUp = atob("EBCBAAAAAAABgAAAAAATyAZoBCB//gAAAAABgAPABmAAAAAA"); - - drawCircleBackground(w); - - let color = getCircleColor("sunprogress"); - - drawGauge(w, h3, percent, color); - - drawInnerCircleAndTriangle(w); - - let icon = sunSetDown; - let text = "?"; - let times = getSunData(); - if (times != undefined) { - let sunRise = Math.round(times.sunrise.getTime() / 1000); - let sunSet = Math.round(times.sunset.getTime() / 1000); - if (!isDay()) { - // night - if (now > sunRise) { - // after sunRise - let upcomingSunRise = sunRise + 60 * 60 * 24; - text = formatSeconds(upcomingSunRise - now); - } else { - text = formatSeconds(sunRise - now); - } - icon = sunSetUp; - } else { - // day, approx sunrise tomorrow: - text = formatSeconds(sunSet - now); - icon = sunSetDown; - } - } - - writeCircleText(w, text); - - g.drawImage(getImage(icon, getCircleIconColor("sunprogress", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); -} - -/* - * Deprecated but nice as references for clkinfo - */ - -function drawSteps(w) { - if (!w) w = getCircleXPosition("steps"); - let steps = getSteps(); - - drawCircleBackground(w); - - let color = getCircleColor("steps"); - - let percent; - let stepGoal = settings.stepGoal; - if (stepGoal > 0) { - percent = steps / stepGoal; - if (stepGoal < steps) percent = 1; - drawGauge(w, h3, percent, color); - } - - drawInnerCircleAndTriangle(w); - - writeCircleText(w, shortValue(steps)); - - g.drawImage(getImage(atob("EBCBAAAACAAcAB4AHgAeABwwADgGeAZ4AHgAMAAAAHAAIAAA"), getCircleIconColor("steps", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); -} - -function drawStepsDistance(w) { - if (!w) w = getCircleXPosition("stepsDistance"); - let steps = getSteps(); - let stepDistance = settings.stepLength; - let stepsDistance = Math.round(steps * stepDistance); - - drawCircleBackground(w); - - let color = getCircleColor("stepsDistance"); - - let percent; - let stepDistanceGoal = settings.stepDistanceGoal; - if (stepDistanceGoal > 0) { - percent = stepsDistance / stepDistanceGoal; - if (stepDistanceGoal < stepsDistance) percent = 1; - drawGauge(w, h3, percent, color); - } - - drawInnerCircleAndTriangle(w); - - writeCircleText(w, shortValue(stepsDistance)); - - g.drawImage(getImage(atob("EBCBAAAACAAcAB4AHgAeABwwADgGeAZ4AHgAMAAAAHAAIAAA"), getCircleIconColor("stepsDistance", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); -} - -function drawHeartRate(w) { - if (!w) w = getCircleXPosition("hr"); - - let heartIcon = atob("EBCBAAAAAAAeeD/8P/x//n/+P/w//B/4D/AH4APAAYAAAAAA"); - - drawCircleBackground(w); - - let color = getCircleColor("hr"); - - let percent; - if (hrtValue != undefined) { - let minHR = settings.minHR; - let maxHR = settings.maxHR; - percent = (hrtValue - minHR) / (maxHR - minHR); - if (isNaN(percent)) percent = 0; - drawGauge(w, h3, percent, color); - } - - drawInnerCircleAndTriangle(w); - - writeCircleText(w, hrtValue != undefined ? hrtValue : "-"); - - g.drawImage(getImage(heartIcon, getCircleIconColor("hr", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); -} - -function drawBattery(w) { - if (!w) w = getCircleXPosition("battery"); - let battery = E.getBattery(); - - let powerIcon = atob("EBCBAAAAA8ADwA/wD/AP8A/wD/AP8A/wD/AP8A/wD/AH4AAA"); - - drawCircleBackground(w); - - let color = getCircleColor("battery"); - - let percent; - if (battery > 0) { - percent = battery / 100; - drawGauge(w, h3, percent, color); - } - - drawInnerCircleAndTriangle(w); - - if (Bangle.isCharging()) { - color = colorGreen; - } else { - if (settings.batteryWarn != undefined && battery <= settings.batteryWarn) { - color = colorRed; - } - } - writeCircleText(w, battery + '%'); - - g.drawImage(getImage(powerIcon, getCircleIconColor("battery", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); -} -function drawTemperature(w) { - if (!w) w = getCircleXPosition("temperature"); - - getPressureValue("temperature").then((temperature) => { - drawCircleBackground(w); - - let color = getCircleColor("temperature"); - - let percent; - if (temperature) { - let min = -40; - let max = 85; - percent = (temperature - min) / (max - min); - drawGauge(w, h3, percent, color); - } - - drawInnerCircleAndTriangle(w); - - if (temperature) - writeCircleText(w, locale.temp(temperature)); - - g.drawImage(getImage(atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"), getCircleIconColor("temperature", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); - - }); -} - -function drawPressure(w) { - if (!w) w = getCircleXPosition("pressure"); - - getPressureValue("pressure").then((pressure) => { - drawCircleBackground(w); - - let color = getCircleColor("pressure"); - - let percent; - if (pressure && pressure > 0) { - let minPressure = 950; - let maxPressure = 1050; - percent = (pressure - minPressure) / (maxPressure - minPressure); - drawGauge(w, h3, percent, color); - } - - drawInnerCircleAndTriangle(w); - - if (pressure) - writeCircleText(w, Math.round(pressure)); - - g.drawImage(getImage(atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"), getCircleIconColor("pressure", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); - - }); -} - -function drawAltitude(w) { - if (!w) w = getCircleXPosition("altitude"); - - getPressureValue("altitude").then((altitude) => { - drawCircleBackground(w); - - let color = getCircleColor("altitude"); - - let percent; - if (altitude) { - let min = 0; - let max = 10000; - percent = (altitude - min) / (max - min); - drawGauge(w, h3, percent, color); - } - - drawInnerCircleAndTriangle(w); - - if (altitude) - writeCircleText(w, locale.distance(Math.round(altitude))); - - g.drawImage(getImage(atob("EBCBAAAAAYADwAJAAkADwAPAA8ADwAfgB+AH4AfgA8ABgAAA"), getCircleIconColor("altitude", color, percent)), w - iconOffset, h3 + radiusOuter - iconOffset); - - }); -} - -function shortValue(v) { - if (isNaN(v)) return '-'; - if (v <= 999) return v; - if (v >= 1000 && v < 10000) { - v = Math.floor(v / 100) * 100; - return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; - } - if (v >= 10000) { - v = Math.floor(v / 1000) * 1000; - return (v / 1000).toFixed(1).replace(/\.0$/, '') + 'k'; - } -} - -function getSteps() { - if (Bangle.getHealthStatus) { - return Bangle.getHealthStatus("day").steps; - } - if (WIDGETS && WIDGETS.wpedom !== undefined) { - return WIDGETS.wpedom.getSteps(); - } - return 0; -} - -function getPressureValue(type) { - return new Promise((resolve) => { - if (Bangle.getPressure) { - if (!pressureLocked) { - pressureLocked = true; - if (pressureCache && pressureCache[type]) { - resolve(pressureCache[type]); - } - Bangle.getPressure().then(function(d) { - pressureLocked = false; - if (d) { - pressureCache = d; - if (d[type]) { - resolve(d[type]); - } - } - }).catch(() => {}); - } else { - if (pressureCache && pressureCache[type]) { - resolve(pressureCache[type]); - } - } - } - }); -} - -/* - * end deprecated - */ - -var menu = null; -function reloadMenu() { - menu = clock_info.load(); - for(var i=1; i<5; i++) - if(settings['circle'+i].includes("/")) { - let parts = settings['circle'+i].split("/"); - let infoName = parts[0], itemName = parts[1]; - let infoNum = menu.findIndex(e=>e.name==infoName); - let itemNum = 0; - //suppose unnamed are varying (like timers or events), pick the first - if(itemName) - itemNum = menu[infoNum].items.findIndex(it=>it.name==itemName); - circleInfoNum[i-1] = infoNum; - circleItemNum[i-1] = itemNum; - } -} -//reload periodically for changes? -reloadMenu(); - -function drawEmpty(img, w, color) { +let drawEmpty = function(img, w, color) { drawGauge(w, h3, 0, color); drawInnerCircleAndTriangle(w); writeCircleText(w, "?"); @@ -679,65 +172,48 @@ function drawEmpty(img, w, color) { .drawImage(img, w - iconOffset, h3 + radiusOuter - iconOffset, {scale: 16/24}); } -function drawClkInfo(index, w) { - var info = menu[circleInfoNum[index-1]]; - var type = settings['circle'+index]; - if (!w) w = getCircleXPosition(type); +let drawCircle = function(index, item, data) { + var w = circlePosX[index-1]; drawCircleBackground(w); - const color = getCircleColor(type); - if(!info || !info.items.length) { - drawEmpty(info? info.img : null, w, color); - return; - } - var item = info.items[circleItemNum[index-1]]; - //TODO do hide()+get() here - item.show(); - item.hide(); - item=item.get(); - var img = item.img; - if(!img) img = info.img; - let percent = (item.v-item.min) / item.max; - if(isNaN(percent)) percent = 1; //fill it up + const color = getCircleColor(index); + //drawEmpty(info? info.img : null, w, color); + var img = data.img; + var percent = 1; //fill up if no range + var txt = ""+data.text; + if (txt.endsWith(" bpm")) txt=txt.slice(0,-4); // hack for heart rate - remove the 'bpm' text + if(item.hasRange) percent = (data.v-data.min) / (data.max-data.min); + if(data.short) txt = data.short; + //long text can overflow and we do not draw there anymore.. + if(txt.length>6) txt = txt.slice(0,5)+"\n"+txt.slice(5,10) drawGauge(w, h3, percent, color); drawInnerCircleAndTriangle(w); - writeCircleText(w, item.text); - g.setColor(getCircleIconColor(type, color, percent)) + writeCircleText(w, txt); + if(!img) return; //or get it from the clkinfo? + g.setColor(getCircleIconColor(index, color, percent)) .drawImage(img, w - iconOffset, h3 + radiusOuter - iconOffset, {scale: 16/24}); } -/* - * wind goes from 0 to 12 (see https://en.wikipedia.org/wiki/Beaufort_scale) - */ -function windAsBeaufort(windInKmh) { - let beaufort = [2, 6, 12, 20, 29, 39, 50, 62, 75, 89, 103, 118]; - let l = 0; - while (l < beaufort.length && beaufort[l] < windInKmh) { - l++; - } - return l; -} - /* * Choose weather icon to display based on weather conditition code * https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 */ -function getWeatherIconByCode(code, big) { +let getWeatherIconByCode = function(code, big) { let codeGroup = Math.round(code / 100); if (big == undefined) big = false; // weather icons: - let weatherCloudy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAAAAAfg+AAAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD3gAAAAAAAAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP////////8Af////////gA////////8AAf//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA"); - let weatherSunny = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAwADwADAAAAHgAPAAeAAAAfAA8AD4AAAA+ADwAfAAAAB8APAD4AAAAD4B+AfAAAAAHw//D4AAAAAPv//fAAAAAAf///4AAAAAA/4H/AAAAAAB+AH4AAAAAAPgAHwAAAAAA8AAPAAAAAAHwAA+AAAAAAeAAB4AAAAAB4AAHgAAAAAPAAAPAAAA//8AAA//8AD//wAAD//wAP//AAAP//AA//8AAA//8AAADwAADwAAAAAHgAAeAAAAAAeAAB4AAAAAB8AAPgAAAAADwAA8AAAAAAPgAHwAAAAAAfgB+AAAAAAD/gf8AAAAAAf///4AAAAAD7//3wAAAAAfD/8PgAAAAD4B+AfAAAAAfADwA+AAAAD4APAB8AAAAfAA8AD4AAAB4ADwAHgAAADAAPAAMAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA"); - let weatherMoon = big ? atob("QEDBAP//wxgAAAYAAAAPAAAAD4AAAA8AAAAPwAAADwAAAA/gAAAPAAAAB/APAP/wAAAH+A8A//AAAAf4DwD/8AAAB/wPAP/wAAAH/gAADwAAAAe+AAAPAAAAB54AAA8AAAAHngAADwAAAAePAAAAAAAAD48OAAAAAAAPDw+AAAAAAB8PD8AAAAAAHg8P4AAAAAA+DwPwAAAAAHwfAfgAAAAB+D4A/AAA8AfwfgB/8AD//+D+AD/8AP//wfgAH/4Af/8B8AAf/wB//APgAAgfgD+AA8AAAAfAH8AHwAAAA+AP8B+AAAAB4Af//4AAAAHgA///gAAAAPAA//8AAAAA8AAf/wAAAADwAAAAAAAAAPAAAAAAAAAA8AcAAAAAAADwD+AAAAAAAfAfgAAAAAAB+D4AAAAAAAB8fAAAAAAAAD54AAAAAAAAHngAAAAAAAAe8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAPeAAAAAAAAB54AAAAAAAAHnwAAAAAAAA+PgAAAAAAAHwfgAAAAAAB+A/////////wB////////+AD////////wAB///////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA"); - let weatherPartlyCloudy = big ? atob("QEDBAP//wxgAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAABwAPAA4AAAAHgA8AHgAAAAfADwA+AAAAA+AfgHwAAAAB8P/w+AAAAAD7//3wAAAAAH///+BAAAAAP+B/wOAAAAAfgB+B8AAAAD4AD8H4AAAAPAA/wPwAAAB8AH+Af/AAAHgA/AA//AAAeAH4AB/+AADwAfAAH/8A//AD4AAIH4D/8AfAAAAHwP/wB4AAAAPg//AHgAAAAeAA8B+AAAAB4AB4fwAAAADwAHn/AAAAAPAAff8AAAAA8AA/8AAAAADwAD/AAAAAAPAEH4AAAAAA8A4PgAAAAAHwHgcAAAAAAfg+AwAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD3gAAAAAAAAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP////////8Af////////gA////////8AAf//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA"); - let weatherRainy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4APAA8AAfg+AA8ADwAAfHwADwAPAAA+eAAPAA8AAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AADw8PDwAP8AAPDw8PAA/wAA8PDw8AD3gADw8PDwAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP/w8PDw8P8Af/Dw8PDw/gA/8PDw8PD8AAfw8PDw8OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADwAPAAAAAAAPAA8AAAAAAA8ADwAAAAAADwAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA"); - let weatherPartlyRainy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAA8AAfg+AAAADwAAfHwAAAAPAAA+eAAAAA8AAB54AAAADwAAHvAAAAAPAAAP8AAAAA8AAA/wAAAADwAAD/AAAA8PAAAP8AAADw8AAA/wAAAPDwAAD3gAAA8PAAAeeAAADw8AAB58AAAPDwAAPj4AAA8PAAB8H4AADw8AAfgP//8PDw//8Af//w8PD//gA///Dw8P/8AAf/8PDw/+AAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA"); - let weatherSnowy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAADwAfg+AAAAAPAAfHwAAAAA8AA+eAAAAADwAB54AA8AD/8AHvAADwAP/wAP8AAPAA//AA/wAA8AD/8AD/AA//AA8AAP8AD/8ADwAA/wAP/wAPAAD3gA//AA8AAeeAAPAAAAAB58AA8AAAAAPj4ADwAAAAB8H4APAAAAAfgP/wAA8A//8Af/AADwD//gA/8AAPAP/8AAfwAA8A/+AAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA"); - let weatherFoggy = big ? atob("QEDBAP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAwADwADAAAAHgAPAAeAAAAfAA8AD4AAAA+ADwAfAAAAB8APAD4AAAAD4B+AfAAAAAHw//D4AAAAAPv//fAAAAAAf///4AAAAAA/4H/AAAAAAB+AH4AAAAAAPgAHwAAAAAA8AAPAAAAAAHwAA+AAAAAAeAAB4AAAAAB4AAHgAAAAAPAAAPAAAAAAAAAA//8AAAAAAAD//wAAAAAAAP//AAAAAAAA//8AD///AADwAAAP//8AAeAAAA///wAB4AAAD///AAPgAAAAAAAAA8AAAAAAAAAHwAAAAAAAAB+AAAAAAAAAf8AAAAD///D/4AAAAP//8P3wAAAA///w8PgAAAD///CAfAAAAAAAAAA+AAAAAAAAAB8AAAAAAAAAD4AAAAAAAAAHgAAP//8PAAMAAA///w8AAAAAD///DwAAAAAP//8PAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA"); - let weatherStormy = big ? atob("QEDBAP//wxgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAB//gAAAAAAAP//gAAAAAAD///AAAAAAAf4H+AAAAAAD8AD8AAAAAAfgAH4AAAAAB8AAPwAAAAAPgAAf/AAAAB8AAA//AAAAHgAAB/+AAAAeAAAH/8AAAH4AAAIH4AAB/AAAAAHwAAf8AAAAAPgAD/wAAAAAeAAPwAAAAAB4AB8AAAAAADwAHgAAAAAAPAA+AAAAAAA8ADwAAAAAADwA/AAAAAAAPAH8AAAAAAA8A/wAAAAAAHwH4AAAAAAAfg+AAAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAD/AAHvAAAAAf4AAP8AAAAB/gAA/wAAAAP8AAD/AAAAA/gAAP8AAAAH+AAA/wAAAAfwAAD3gAAAD/AAAeeAAAAP4AAB58AAAB/AAAPj4AAAH8AAB8H4AAA/gAAfgP//+D//D/8Af//4f/4P/gA///B//B/8AAf/8P/8P+AAAAAAAPgAAAAAAAAB8AAAAAAAAAHwAAAAAAAAA+AAAAAAAAADwAAAAAAAAAfAAAAAAAAAB4AAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA"); - let unknown = big ? atob("QEDBAP//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAH//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAA/AAD4AAAAAD4H4HwAAAAAfB/4PgAAAAB8P/weAAAAAHg//h4AAAAA+Hw+HwAAAAD4eB8PAAAAAP/wDw8AAAAA//APDwAAAAD/8A8PAAAAAH/gDw8AAAAAAAAfDwAAAAAAAH4fAAAAAAAB/B4AAAAAAAf4HgAAAAAAD/A+AAAAAAAfwHwAAAAAAD8A+AAAAAAAPgH4AAAAAAB8B/AAAAAAAHgf4AAAAAAA+H+AAAAAAADwfwAAAAAAAPD8AAAAAAAA8PAAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf+AAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAADw8AAAAAAAAPDwAAAAAAAA8PAAAAAAAADw8AAAAAAAAP/wAAAAAAAA//AAAAAAAAD/8AAAAAAAAH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==") : undefined; - + let weatherCloudy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+AAAAAAAH4PgAAAAAAAHx8AAAAAAAAPngAAAAAAAAeeAAAAAAAAB7wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA94AAAAAAAAHngAAAAAAAAefAAAAAAAAD4+AAAAAAAAfB+AAAAAAAH4D/////////AH////////4AP////////AAH///////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAAAAAAfgD/Af8H/4//7///////9//z/+AAAAAAAA"); + let weatherSunny = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAMAA8AAwAAAB4ADwAHgAAAHwAPAA+AAAAPgA8AHwAAAAfADwA+AAAAA+AfgHwAAAAB8P/w+AAAAAD7//3wAAAAAH///+AAAAAAP+B/wAAAAAAfgB+AAAAAAD4AB8AAAAAAPAADwAAAAAB8AAPgAAAAAHgAAeAAAAAAeAAB4AAAAADwAADwAAAP//AAAP//AA//8AAA//8AD//wAAD//wAP//AAAP//AAAA8AAA8AAAAAB4AAHgAAAAAHgAAeAAAAAAfAAD4AAAAAA8AAPAAAAAAD4AB8AAAAAAH4AfgAAAAAA/4H/AAAAAAH///+AAAAAA+//98AAAAAHw//D4AAAAA+AfgHwAAAAHwA8APgAAAA+ADwAfAAAAHwAPAA+AAAAeAA8AB4AAAAwADwADAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAYAQCBAIA8AH4A/wb/YP8A/gB+ARiBAIAYABgAAA"); + let weatherMoon = big ? atob("QECBAAAGAAAADwAAAA+AAAAPAAAAD8AAAA8AAAAP4AAADwAAAAfwDwD/8AAAB/gPAP/wAAAH+A8A//AAAAf8DwD/8AAAB/4AAA8AAAAHvgAADwAAAAeeAAAPAAAAB54AAA8AAAAHjwAAAAAAAA+PDgAAAAAADw8PgAAAAAAfDw/AAAAAAB4PD+AAAAAAPg8D8AAAAAB8HwH4AAAAAfg+APwAAPAH8H4Af/AA///g/gA//AD//8H4AB/+AH//AfAAH/8Af/wD4AAIH4A/gAPAAAAHwB/AB8AAAAPgD/AfgAAAAeAH//+AAAAB4AP//4AAAADwAP//AAAAAPAAH/8AAAAA8AAAAAAAAADwAAAAAAAAAPAHAAAAAAAA8A/gAAAAAAHwH4AAAAAAAfg+AAAAAAAAfHwAAAAAAAA+eAAAAAAAAB54AAAAAAAAHvAAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD3gAAAAAAAAeeAAAAAAAAB58AAAAAAAAPj4AAAAAAAB8H4AAAAAAAfgP////////8Af////////gA////////8AAf//////+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAYAP8B/4P/w//D/8f/5//j/8P/w//B/4D/ABgAAA"); + let weatherPartlyCloudy = big ? atob("QECBAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAcADwAOAAAAB4APAB4AAAAHwA8APgAAAAPgH4B8AAAAAfD/8PgAAAAA+//98AAAAAB////gQAAAAD/gf8DgAAAAH4AfgfAAAAA+AA/B+AAAADwAP8D8AAAAfAB/gH/wAAB4APwAP/wAAHgB+AAf/gAA8AHwAB//AP/wA+AACB+A//AHwAAAB8D/8AeAAAAD4P/wB4AAAAHgAPAfgAAAAeAAeH8AAAAA8AB5/wAAAADwAH3/AAAAAPAAP/AAAAAA8AA/wAAAAADwBB+AAAAAAPAOD4AAAAAB8B4HAAAAAAH4PgMAAAAAAHx8AAAAAAAAPngAAAAAAAAeeAAAAAAAAB7wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA94AAAAAAAAHngAAAAAAAAefAAAAAAAAD4+AAAAAAAAfB+AAAAAAAH4D/////////AH////////4AP////////AAH///////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAAAYQAMAD8AIQBhoW+AOYBwwOBBgHGAGP/wf+AAA"); + let weatherRainy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+ADwAPAAH4PgAPAA8AAHx8AA8ADwAAPngADwAPAAAeeAAAAAAAAB7wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAA8PDw8AD/AADw8PDwAP8AAPDw8PAA94AA8PDw8AHngAAAAAAAAefAAAAAAAAD4+AAAAAAAAfB+AAAAAAAH4D/8PDw8PD/AH/w8PDw8P4AP/Dw8PDw/AAH8PDw8PDgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA8ADwAAAAAADwAPAAAAAAAPAA8AAAAAAA8ADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAYAH4AwwOBBgGEAOQAJBgjPOEkgGYAZgA8ABgAAA"); + let weatherPartlyRainy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+AAAAPAAH4PgAAAA8AAHx8AAAADwAAPngAAAAPAAAeeAAAAA8AAB7wAAAADwAAD/AAAAAPAAAP8AAAAA8AAA/wAAAPDwAAD/AAAA8PAAAP8AAADw8AAA94AAAPDwAAHngAAA8PAAAefAAADw8AAD4+AAAPDwAAfB+AAA8PAAH4D///Dw8P//AH//8PDw//4AP//w8PD//AAH//Dw8P/gAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAEEAQAAeADMAYaFvoTmAMMDgQIBxhhiGGG9wDwAGA"); + let weatherSnowy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+AAAAA8AH4PgAAAADwAHx8AAAAAPAAPngAAAAA8AAeeAAPAA//AB7wAA8AD/8AD/AADwAP/wAP8AAPAA//AA/wAP/wAPAAD/AA//AA8AAP8AD/8ADwAA94AP/wAPAAHngADwAAAAAefAAPAAAAAD4+AA8AAAAAfB+ADwAAAAH4D/8AAPAP//AH/wAA8A//4AP/AADwD//AAH8AAPAP/gAAAAAP/wAAAAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAAADwAGAEYg73C50BCAEIC50O9wRiAGAA8AAAAAA"); + let weatherFoggy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAMAA8AAwAAAB4ADwAHgAAAHwAPAA+AAAAPgA8AHwAAAAfADwA+AAAAA+AfgHwAAAAB8P/w+AAAAAD7//3wAAAAAH///+AAAAAAP+B/wAAAAAAfgB+AAAAAAD4AB8AAAAAAPAADwAAAAAB8AAPgAAAAAHgAAeAAAAAAeAAB4AAAAADwAADwAAAAAAAAAP//AAAAAAAA//8AAAAAAAD//wAAAAAAAP//AA///wAA8AAAD///AAHgAAAP//8AAeAAAA///wAD4AAAAAAAAAPAAAAAAAAAB8AAAAAAAAAfgAAAAAAAAH/AAAAA///w/+AAAAD///D98AAAAP//8PD4AAAA///wgHwAAAAAAAAAPgAAAAAAAAAfAAAAAAAAAA+AAAAAAAAAB4AAD///DwADAAAP//8PAAAAAA///w8AAAAAD///DwAAAAAAAAAPAAAAAAAAAA8AAAAAAAAADwAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAAADwAZgDDA4EGAcQAZAAgAAf74AAAAAd/4AAAAA"); + let weatherStormy = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAf/4AAAAAAAD//4AAAAAAA///wAAAAAAH+B/gAAAAAA/AA/AAAAAAH4AB+AAAAAAfAAD8AAAAAD4AAH/wAAAAfAAAP/wAAAB4AAAf/gAAAHgAAB//AAAB+AAACB+AAAfwAAAAB8AAH/AAAAAD4AA/8AAAAAHgAD8AAAAAAeAAfAAAAAAA8AB4AAAAAADwAPgAAAAAAPAA8AAAAAAA8APwAAAAAADwB/AAAAAAAPAP8AAAAAAB8B+AAAAAAAH4PgAAAAAAAHx8AAAAAAAAPngAAAAAAAAeeAAAAA/wAB7wAAAAH+AAD/AAAAAf4AAP8AAAAD/AAA/wAAAAP4AAD/AAAAB/gAAP8AAAAH8AAA94AAAA/wAAHngAAAD+AAAefAAAAfwAAD4+AAAB/AAAfB+AAAP4AAH4D///g//w//AH//+H/+D/4AP//wf/wf/AAH//D//D/gAAAAAAD4AAAAAAAAAfAAAAAAAAAB8AAAAAAAAAPgAAAAAAAAA8AAAAAAAAAHwAAAAAAAAAeAAAAAAAAADwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : atob("EBCBAAAAAYAH4AwwOBBgGEAOQMJAgjmOGcgAgACAAAAAAAAA"); + let unknown = big ? atob("QECBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AAAAAAAAH/+AAAAAAAB//+AAAAAAAP//8AAAAAAB/gf4AAAAAAPwAPwAAAAAB+AAfgAAAAAPwAA+AAAAAA+B+B8AAAAAHwf+D4AAAAAfD/8HgAAAAB4P/4eAAAAAPh8Ph8AAAAA+HgfDwAAAAD/8A8PAAAAAP/wDw8AAAAA//APDwAAAAB/4A8PAAAAAAAAHw8AAAAAAAB+HwAAAAAAAfweAAAAAAAH+B4AAAAAAA/wPgAAAAAAH8B8AAAAAAA/APgAAAAAAD4B+AAAAAAAfAfwAAAAAAB4H+AAAAAAAPh/gAAAAAAA8H8AAAAAAADw/AAAAAAAAPDwAAAAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAAf+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/gAAAAAAAA//AAAAAAAAD/8AAAAAAAAP/wAAAAAAAA8PAAAAAAAADw8AAAAAAAAPDwAAAAAAAA8PAAAAAAAAD/8AAAAAAAAP/wAAAAAAAA//AAAAAAAAB/4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") : undefined; + switch (codeGroup) { case 2: return weatherStormy; @@ -765,7 +241,9 @@ function getWeatherIconByCode(code, big) { case 8: switch (code) { case 800: - return isDay() ? weatherSunny : weatherMoon; + var hr = (new Date()).getHours(); + var isDay = (hr>6) && (hr<=18); // fixme we don't want to include ALL of suncalc just to choose one icon + return isDay ? weatherSunny : weatherMoon; case 801: return weatherPartlyCloudy; case 802: @@ -778,80 +256,19 @@ function getWeatherIconByCode(code, big) { } } - -function isDay() { - let times = getSunData(); - if (times == undefined) return true; - let sunRise = Math.round(times.sunrise.getTime() / 1000); - let sunSet = Math.round(times.sunset.getTime() / 1000); - - return (now > sunRise && now < sunSet); -} - -function formatSeconds(s) { - if (s > 60 * 60) { // hours - return Math.round(s / (60 * 60)) + "h"; - } - if (s > 60) { // minutes - return Math.round(s / 60) + "m"; - } - return "<1m"; -} - -function getSunData() { - if (location != undefined && location.lat != undefined) { - let SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); - // get today's sunlight times for lat/lon - return SunCalc ? SunCalc.getTimes(new Date(), location.lat, location.lon) : undefined; - } - return undefined; -} - -/* - * Calculated progress of the sun between sunrise and sunset in percent - * - * Taken from rebble app and modified - */ -function getSunProgress() { - let times = getSunData(); - if (times == undefined) return 0; - let sunRise = Math.round(times.sunrise.getTime() / 1000); - let sunSet = Math.round(times.sunset.getTime() / 1000); - - if (isDay()) { - // during day - let dayLength = sunSet - sunRise; - if (now > sunRise) { - return (now - sunRise) / dayLength; - } else { - return (sunRise - now) / dayLength; - } - } else { - // during night - if (now < sunRise) { - let prevSunSet = sunSet - 60 * 60 * 24; - return 1 - (sunRise - now) / (sunRise - prevSunSet); - } else { - let upcomingSunRise = sunRise + 60 * 60 * 24; - return (upcomingSunRise - now) / (upcomingSunRise - sunSet); - } - } -} - /* * Draws the background and the grey circle */ -function drawCircleBackground(w) { - g.clearRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); +let drawCircleBackground = function(w) { // Draw rectangle background: g.setColor(colorBg); - g.fillRect(w - radiusOuter - 3, h3 - radiusOuter - 3, w + radiusOuter + 3, h3 + radiusOuter + 3); + g.fillRect(w - radiusBorder, h3 - radiusBorder, w + radiusBorder, g.getHeight()-1); // Draw grey background circle: - g.setColor(colorGrey); + g.setColor('#808080'); // grey g.fillCircle(w, h3, radiusOuter); } -function drawInnerCircleAndTriangle(w) { +let drawInnerCircleAndTriangle = function(w) { // Draw inner circle g.setColor(colorBg); g.fillCircle(w, h3, radiusInner); @@ -859,18 +276,13 @@ function drawInnerCircleAndTriangle(w) { g.fillPoly([w, h3, w - 15, h3 + radiusOuter + 5, w + 15, h3 + radiusOuter + 5]); } -function radians(a) { - return a * Math.PI / 180; -} - /* * This draws the actual gauge consisting out of lots of little filled circles */ -function drawGauge(cx, cy, percent, color) { +let drawGauge = function(cx, cy, percent, color) { let offset = 15; let end = 360 - offset; - let radius = radiusInner + (circleCount == 3 ? 3 : 2); - let size = radiusOuter - radiusInner - 2; + let radius = radiusOuter+1; if (percent <= 0) return; // no gauge needed if (percent > 1) percent = 1; @@ -880,15 +292,21 @@ function drawGauge(cx, cy, percent, color) { color = getGradientColor(color, percent); g.setColor(color); - - for (let i = startRotation; i > endRotation - size; i -= size) { - x = cx + radius * Math.sin(radians(i)); - y = cy + radius * Math.cos(radians(i)); - g.fillCircle(x, y, size); - } + // convert to radians + startRotation *= Math.PI / 180; + let amt = Math.PI / 10; + endRotation = (endRotation * Math.PI / 180) - amt; + // all we need to draw is an arc, because we'll fill the center + let poly = [cx,cy]; + for (let r = startRotation; r > endRotation; r -= amt) + poly.push( + cx + radius * Math.sin(r), + cy + radius * Math.cos(r) + ); + g.fillPoly(poly); } -function writeCircleText(w, content) { +let writeCircleText = function(w, content) { if (content == undefined) return; let font = String(content).length > 4 ? circleFontSmall : String(content).length > 3 ? circleFont : circleFontBig; g.setFont(font); @@ -898,26 +316,48 @@ function writeCircleText(w, content) { g.drawString(content, w, h3); } -function getWeather() { +let getWeather=function() { let jsonWeather = storage.readJSON('weather.json'); return jsonWeather && jsonWeather.weather ? jsonWeather.weather : undefined; } +g.clear(1); // clear the whole screen + Bangle.setUI({ mode : "clock", remove : function() { - // Called to unload all of the clock app + // Called to unload all of the clock app (allowing for 'fast load') if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; - + clockInfoMenu.forEach(c => c.remove()); delete Graphics.prototype.setFontRobotoRegular50NumericOnly; delete Graphics.prototype.setFontRobotoRegular21; - }}); + if (!showWidgets) require("widget_utils").show(); + } +}); + +let clockInfoDraw = (itm, info, options) => { + //print("Draw",itm.name,options); + drawCircle(options.circlePosition, itm, info); + if (options.focus) g.reset().drawRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1) +}; +let clockInfoItems = require("clock_info").load(); +let clockInfoMenu = []; +for(var i=0;i{ - //TODO filter for hasRange and other - if(!e.items.length || !e.items[0].name) { - //suppose unnamed are varying (like timers or events), pick the first - item = e.items[0]; - valuesCircleTypes = valuesCircleTypes.concat([e.name+"/"]); - namesCircleTypes = namesCircleTypes.concat([e.name]); - } else { - let values = e.items.map(i=>e.name+"/"+i.name); - let names =e.name=="Bangle" ? e.items.map(i=>i.name) : values; - valuesCircleTypes = valuesCircleTypes.concat(values); - namesCircleTypes = namesCircleTypes.concat(names); - } - }) - - const valuesColors = ["", "#ff0000", "#00ff00", "#0000ff", "#ffff00", "#ff00ff", "#00ffff", "#fff", "#000", "green-red", "red-green", "fg"]; const namesColors = ["default", "red", "green", "blue", "yellow", "magenta", @@ -85,6 +65,12 @@ }, onchange: x => save('updateInterval', x), }, + //TODO deprecated local icons, may disappear in future + /*LANG*/'legacy weather icons': { + value: !!settings.legacyWeatherIcons, + format: () => (settings.legacyWeatherIcons ? 'Yes' : 'No'), + onchange: x => save('legacyWeatherIcons', x), + }, /*LANG*/'show big weather': { value: !!settings.showBigWeather, format: () => (settings.showBigWeather ? 'Yes' : 'No'), @@ -102,12 +88,6 @@ const menu = { '': { 'title': /*LANG*/'Circle ' + circleId }, /*LANG*/'< Back': ()=>showMainMenu(), - /*LANG*/'data': { - value: valuesCircleTypes.indexOf(settings[circleName]), - min: 0, max: valuesCircleTypes.length - 1, - format: v => namesCircleTypes[v], - onchange: x => save(circleName, valuesCircleTypes[x]), - }, /*LANG*/'color': { value: valuesColors.indexOf(settings[colorKey]) || 0, min: 0, max: valuesColors.length - 1, diff --git a/apps/clkinfocal/ChangeLog b/apps/clkinfocal/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/clkinfocal/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/clkinfocal/app.png b/apps/clkinfocal/app.png new file mode 100644 index 000000000..ed79cd884 Binary files /dev/null and b/apps/clkinfocal/app.png differ diff --git a/apps/clkinfocal/clkinfo.js b/apps/clkinfocal/clkinfo.js new file mode 100644 index 000000000..a7949cda4 --- /dev/null +++ b/apps/clkinfocal/clkinfo.js @@ -0,0 +1,32 @@ +(function() { + require("Font4x8Numeric").add(Graphics); + return { + name: "Bangle", + items: [ + { name : "Date", + get : () => { + let d = new Date(); + let g = Graphics.createArrayBuffer(24,24,1,{msb:true}); + g.drawImage(atob("FhgBDADAMAMP/////////////////////8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAPAAA8AADwAAP///////"),1,0); + g.setFont("6x15").setFontAlign(0,0).drawString(d.getDate(),11,17); + return { + text : require("locale").dow(d,1).toUpperCase(), + img : g.asImage("string") + }; + }, + show : function() { + this.interval = setTimeout(()=>{ + this.emit("redraw"); + this.interval = setInterval(()=>{ + this.emit("redraw"); + }, 86400000); + }, 86400000 - (Date.now() % 86400000)); + }, + hide : function() { + clearInterval(this.interval); + this.interval = undefined; + } + } + ] + }; +}) diff --git a/apps/clkinfocal/metadata.json b/apps/clkinfocal/metadata.json new file mode 100644 index 000000000..6d6dd63fc --- /dev/null +++ b/apps/clkinfocal/metadata.json @@ -0,0 +1,12 @@ +{ "id": "clkinfocal", + "name": "Calendar Clockinfo", + "version":"0.01", + "description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays the day of the month in the icon, and the weekday", + "icon": "app.png", + "type": "clkinfo", + "tags": "clkinfo,calendar", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"clkinfocal.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkinfofw/ChangeLog b/apps/clkinfofw/ChangeLog new file mode 100644 index 000000000..10810802b --- /dev/null +++ b/apps/clkinfofw/ChangeLog @@ -0,0 +1,2 @@ +0.01: First release +0.02: Update clock_info to avoid a redraw and image allocation diff --git a/apps/clkinfofw/app.png b/apps/clkinfofw/app.png new file mode 100644 index 000000000..c6575b73b Binary files /dev/null and b/apps/clkinfofw/app.png differ diff --git a/apps/clkinfofw/clkinfo.js b/apps/clkinfofw/clkinfo.js new file mode 100644 index 000000000..2b3cb32ba --- /dev/null +++ b/apps/clkinfofw/clkinfo.js @@ -0,0 +1,17 @@ +(function() { + return { + name: "Bangle", + items: [ + { name : "FW", + get : () => { + return { + text : process.env.VERSION, + img : atob("GBjC////AADve773VWmmmmlVVW22nnlVVbLL445VVwAAAADVWAAAAAAlrAAAAAA6sAAAAAAOWAAAAAAlrAD//wA6sANVVcAOWANVVcAlrANVVcA6rANVVcA6WANVVcAlsANVVcAOrAD//wA6WAAAAAAlsAAAAAAOrAAAAAA6WAAAAAAlVwAAAADVVbLL445VVW22nnlVVWmmmmlV") + }; + }, + show : function() {}, + hide : function() {} + } + ] + }; +}) diff --git a/apps/clkinfofw/metadata.json b/apps/clkinfofw/metadata.json new file mode 100644 index 000000000..720a5baa5 --- /dev/null +++ b/apps/clkinfofw/metadata.json @@ -0,0 +1,13 @@ +{ "id": "clkinfofw", + "name": "Firmware Clockinfo", + "version":"0.02", + "description": "For clocks that display 'clockinfo', this displays the firmware version string", + "icon": "app.png", + "type": "clkinfo", + "screenshots": [{"url":"screenshot.png"}], + "tags": "clkinfo,firmware", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"clkinfofw.clkinfo.js","url":"clkinfo.js"} + ] +} diff --git a/apps/clkinfofw/screenshot.png b/apps/clkinfofw/screenshot.png new file mode 100644 index 000000000..da185bd2e Binary files /dev/null and b/apps/clkinfofw/screenshot.png differ diff --git a/apps/clkinfosunrise/ChangeLog b/apps/clkinfosunrise/ChangeLog index 5560f00bc..86e7a7fa8 100644 --- a/apps/clkinfosunrise/ChangeLog +++ b/apps/clkinfosunrise/ChangeLog @@ -1 +1,4 @@ 0.01: New App! +0.02: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps + Add a 'time' clockinfo that also displays a percentage of day left +0.03: Change 3rd mode to show the time to next sunrise/sunset time (not actual time) diff --git a/apps/clkinfosunrise/clkinfo.js b/apps/clkinfosunrise/clkinfo.js index 1454a83f3..22c507f34 100644 --- a/apps/clkinfosunrise/clkinfo.js +++ b/apps/clkinfosunrise/clkinfo.js @@ -1,32 +1,75 @@ (function() { // get today's sunlight times for lat/lon - var sunrise, sunset; + var sunrise, sunset, date; + var SunCalc = require("suncalc"); // from modules folder + const locale = require("locale"); function calculate() { - var SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); - const locale = require("locale"); var location = require("Storage").readJSON("mylocation.json",1)||{}; location.lat = location.lat||51.5072; - location.lon = location.lon||0.1276; - location.location = location.location||"London"; - var times = SunCalc.getTimes(new Date(), location.lat, location.lon); - sunrise = locale.time(times.sunrise,1); - sunset = locale.time(times.sunset,1); + location.lon = location.lon||0.1276; // London + date = new Date(Date.now()); + var times = SunCalc.getTimes(date, location.lat, location.lon); + sunrise = times.sunrise; + sunset = times.sunset; /* do we want to re-calculate this every day? Or we just assume that 'show' will get called once a day? */ } + function show() { + this.interval = setTimeout(()=>{ + this.emit("redraw"); + this.interval = setInterval(()=>{ + this.emit("redraw"); + }, 60000); + }, 60000 - (Date.now() % 60000)); + } + function hide() { + clearInterval(this.interval); + this.interval = undefined; + } + return { name: "Bangle", items: [ { name : "Sunrise", - get : () => ({ text : sunrise, - img : atob("GBiBAAAAAAAAAAAAAAAYAAA8AAB+AAD/AAAAAAAAAAAAAAAYAAAYAAQYIA4AcAYAYAA8AAB+AAD/AAH/gD///D///AAAAAAAAAAAAA==") }), - show : calculate, hide : () => {} + get : () => { calculate(); + return { text : locale.time(sunrise,1), + img : atob("GBiBAAAAAAAAAAAAAAAYAAA8AAB+AAD/AAAAAAAAAAAAAAAYAAAYAAQYIA4AcAYAYAA8AAB+AAD/AAH/gD///D///AAAAAAAAAAAAA==") }}, + show : show, hide : hide }, { name : "Sunset", - get : () => ({ text : sunset, - img : atob("GBiBAAAAAAAAAAAAAAB+AAA8AAAYAAAYAAAAAAAAAAAAAAAYAAAYAAQYIA4AcAYAYAA8AAB+AAD/AAH/gD///D///AAAAAAAAAAAAA==") }), - show : calculate, hide : () => {} + get : () => { calculate(); + return { text : locale.time(sunset,1), + img : atob("GBiBAAAAAAAAAAAAAAB+AAA8AAAYAAAYAAAAAAAAAAAAAAAYAAAYAAQYIA4AcAYAYAA8AAB+AAD/AAH/gD///D///AAAAAAAAAAAAA==") }}, + show : show, hide : hide + }, { name : "Sunrise/set", // Time in day (uses v/min/max to show percentage through day) + hasRange : true, + get : () => { + calculate(); + let day = true; + let d = date.getTime(); + let dayLength = sunset.getTime()-sunrise.getTime(); + let timeUntil, timeTotal; + if (d < sunrise.getTime()) { + day = false; // early morning + timePast = sunrise.getTime()-d; + timeTotal = 86400000-dayLength; + } else if (d > sunset.getTime()) { + day = false; // evening + timePast = d-sunset.getTime(); + timeTotal = 86400000-dayLength; + } else { // day! + timePast = d-sunrise.getTime(); + timeTotal = dayLength; + } + let v = Math.round(100 * timePast / timeTotal); + let minutesTo = (timeTotal-timePast)/60000; + return { text : (minutesTo>90) ? (Math.round(minutesTo/60)+"h") : (Math.round(minutesTo)+"m"), + v : v, min : 0, max : 100, + img : day ? atob("GBiBAAAYAAAYAAAYAAgAEBwAOAx+MAD/AAH/gAP/wAf/4Af/4Of/5+f/5wf/4Af/4AP/wAH/gAD/AAx+MBwAOAgAEAAYAAAYAAAYAA==") : atob("GBiBAAfwAA/8AAP/AAH/gAD/wAB/wAB/4AA/8AA/8AA/8AAf8AAf8AAf8AAf8AA/8AA/8AA/4AB/4AB/wAD/wAH/gAf/AA/8AAfwAA==") + } + }, + show : show, hide : hide } ] }; diff --git a/apps/clkinfosunrise/metadata.json b/apps/clkinfosunrise/metadata.json index f8b68e11f..d130c6453 100644 --- a/apps/clkinfosunrise/metadata.json +++ b/apps/clkinfosunrise/metadata.json @@ -1,6 +1,6 @@ { "id": "clkinfosunrise", "name": "Sunrise Clockinfo", - "version":"0.01", + "version":"0.03", "description": "For clocks that display 'clockinfo' (messages that can be cycled through using the clock_info module) this displays sunrise and sunset based on the location from the 'My Location' app", "icon": "app.png", "type": "clkinfo", diff --git a/apps/contourclock/ChangeLog b/apps/contourclock/ChangeLog index d415a604d..387340d5b 100644 --- a/apps/contourclock/ChangeLog +++ b/apps/contourclock/ChangeLog @@ -7,3 +7,4 @@ 0.25: Fixed a bug that would let widgets change the color of the clock. 0.26: Time formatted to locale 0.27: Fixed the timing code, which sometimes did not update for one minute +0.28: More config options for cleaner look, enabled fast loading diff --git a/apps/contourclock/README.md b/apps/contourclock/README.md new file mode 100644 index 000000000..3341439da --- /dev/null +++ b/apps/contourclock/README.md @@ -0,0 +1,6 @@ +# New Features: +- Fast load! (only works if your launcher uses widgets) +- widgets, date and weekday are individually configurable +- you can hide widgets, date and weekday for a cleaner look when the watch is locked + +Contact me for bug reports or feature requests: ContourClock@gmx.de diff --git a/apps/contourclock/app.js b/apps/contourclock/app.js index d5c97edfa..8efa406c6 100644 --- a/apps/contourclock/app.js +++ b/apps/contourclock/app.js @@ -1,35 +1,64 @@ -var digits = []; -var drawTimeout; -var fontName=""; -var settings = require('Storage').readJSON("contourclock.json", true) || {}; -if (settings.fontIndex==undefined) { - settings.fontIndex=0; - require('Storage').writeJSON("myapp.json", settings); -} +{ + let digits = []; + let drawTimeout; + let fontName=""; + let settings = require('Storage').readJSON("contourclock.json", true) || {}; + if (settings.fontIndex==undefined) { + settings.fontIndex=0; + settings.widgets=true; + settings.hide=false; + settings.weekday=true; + settings.hideWhenLocked=false; + settings.date=true; require('Storage').writeJSON("myapp.json", settings); + } -function queueDraw() { - setTimeout(function() { + let queueDraw = function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + queueDraw(); + }, 60000 - (Date.now() % 60000)); + }; + + let draw = function() { + var date = new Date(); + // Draw day of the week + g.reset(); + if ((!settings.hideWhenLocked) || (!Bangle.isLocked())) { + // Draw day of the week + g.setFont("Teletext10x18Ascii"); + g.clearRect(0,138,g.getWidth()-1,176); + if (settings.weekday) g.setFontAlign(0,1).drawString(require("locale").dow(date).toUpperCase(),g.getWidth()/2,g.getHeight()-18); + // Draw Date + if (settings.date) g.setFontAlign(0,1).drawString(require('locale').date(new Date(),1),g.getWidth()/2,g.getHeight()); + } + require('contourclock').drawClock(settings.fontIndex); + }; + + require("FontTeletext10x18Ascii").add(Graphics); + g.clear(); + + draw(); + if (settings.hideWhenLocked) Bangle.on('lock', function (locked) { + if (!locked) require("widget_utils").show(); + else { + g.clear(); + if (settings.hide) require("widget_utils").swipeOn(); + else require("widget_utils").hide(); + } draw(); - queueDraw(); - }, 60000 - (Date.now() % 60000)); + }); + Bangle.setUI({mode:"clock", remove:function() { + if (drawTimeout) clearTimeout(drawTimeout); + if (settings.widgets && settings.hide) require("widget_utils").show(); + g.reset(); + g.clear(); + }}); + if (settings.widgets) { + Bangle.loadWidgets(); + if (settings.hide) require("widget_utils").swipeOn(); + else Bangle.drawWidgets(); + } + queueDraw(); } - -function draw() { - var date = new Date(); - // Draw day of the week - g.reset(); - g.setFont("Teletext10x18Ascii"); - g.clearRect(0,138,g.getWidth()-1,176); - g.setFontAlign(0,1).drawString(require("locale").dow(date).toUpperCase(),g.getWidth()/2,g.getHeight()-18); - // Draw Date - g.setFontAlign(0,1).drawString(require('locale').date(new Date(),1),g.getWidth()/2,g.getHeight()); - require('contourclock').drawClock(settings.fontIndex); -} - -require("FontTeletext10x18Ascii").add(Graphics); -Bangle.setUI("clock"); -g.clear(); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -queueDraw(); -draw(); diff --git a/apps/contourclock/contourclock.settings.js b/apps/contourclock/contourclock.settings.js index a12538fc5..f2a75d9b5 100644 --- a/apps/contourclock/contourclock.settings.js +++ b/apps/contourclock/contourclock.settings.js @@ -1,43 +1,73 @@ (function(back) { - Bangle.removeAllListeners('drag'); Bangle.setUI(""); var settings = require('Storage').readJSON('contourclock.json', true) || {}; if (settings.fontIndex==undefined) { - settings.fontIndex=0; + settings.fontIndex=0; + settings.widgets=true; + settings.hide=false; + settings.weekday=true; + settings.date=true; + settings.hideWhenLocked=false; require('Storage').writeJSON("myapp.json", settings); } - savedIndex=settings.fontIndex; - saveListener = setWatch(function() { //save changes and return to settings menu - require('Storage').writeJSON('contourclock.json', settings); - Bangle.removeAllListeners('swipe'); - Bangle.removeAllListeners('lock'); - clearWatch(saveListener); - g.clear(); - back(); - }, BTN, { repeat:false, edge:'falling' }); - lockListener = Bangle.on('lock', function () { //discard changes and return to clock - settings.fontIndex=savedIndex; - require('Storage').writeJSON('contourclock.json', settings); - Bangle.removeAllListeners('swipe'); - Bangle.removeAllListeners('lock'); - clearWatch(saveListener); - g.clear(); - load(); - }); - swipeListener = Bangle.on('swipe', function (direction) { - var fontName = require('contourclock').drawClock(settings.fontIndex+direction); - if (fontName) { - settings.fontIndex+=direction; - g.clearRect(0,0,g.getWidth()-1,16); - g.setFont('6x8:2x2').setFontAlign(0,-1).drawString(fontName,g.getWidth()/2,0); - } else { - require('contourclock').drawClock(settings.fontIndex); - } - }); - g.reset(); - g.clear(); - g.setFont('6x8:2x2').setFontAlign(0,-1); - g.drawString(require('contourclock').drawClock(settings.fontIndex),g.getWidth()/2,0); - g.drawString('Swipe - change',g.getWidth()/2,g.getHeight()-36); - g.drawString('BTN - save',g.getWidth()/2,g.getHeight()-18); + function mainMenu() { + E.showMenu({ + "" : { "title" : "ContourClock" }, + "< Back" : () => back(), + 'Widgets': { + value: (settings.widgets !== undefined ? settings.widgets : true), + onchange : v => {settings.widgets=v; require('Storage').writeJSON('contourclock.json', settings);} + }, + 'hide Widgets': { + value: (settings.hide !== undefined ? settings.hide : false), + onchange : v => {settings.hide=v; require('Storage').writeJSON('contourclock.json', settings);} + }, + 'Weekday': { + value: (settings.weekday !== undefined ? settings.weekday : true), + onchange : v => {settings.weekday=v; require('Storage').writeJSON('contourclock.json', settings);} + }, + 'Date': { + value: (settings.date !== undefined ? settings.date : true), + onchange : v => {settings.date=v; require('Storage').writeJSON('contourclock.json', settings);} + }, + 'Hide when locked': { + value: (settings.hideWhenLocked !== undefined ? settings.hideWhenLocked : false), + onchange : v => {settings.hideWhenLocked=v; require('Storage').writeJSON('contourclock.json', settings);} + }, + 'set Font': () => fontMenu() + }); + } + function fontMenu() { + Bangle.setUI(""); + savedIndex=settings.fontIndex; + saveListener = setWatch(function() { //save changes and return to settings menu + require('Storage').writeJSON('contourclock.json', settings); + Bangle.removeAllListeners('swipe'); + Bangle.removeAllListeners('lock'); + mainMenu(); + }, BTN, { repeat:false, edge:'falling' }); + lockListener = Bangle.on('lock', function () { //discard changes and return to clock + settings.fontIndex=savedIndex; + require('Storage').writeJSON('contourclock.json', settings); + Bangle.removeAllListeners('swipe'); + Bangle.removeAllListeners('lock'); + mainMenu(); + }); + swipeListener = Bangle.on('swipe', function (direction) { + var fontName = require('contourclock').drawClock(settings.fontIndex+direction); + if (fontName) { + settings.fontIndex+=direction; + g.clearRect(0,g.getHeight()-36,g.getWidth()-1,g.getHeight()-36+16); + g.setFont('6x8:2x2').setFontAlign(0,-1).drawString(fontName,g.getWidth()/2,g.getHeight()-36); + } else { + require('contourclock').drawClock(settings.fontIndex); + } + }); + g.reset(); + g.clearRect(0,24,g.getWidth()-1,g.getHeight()-1); + g.setFont('6x8:2x2').setFontAlign(0,-1); + g.drawString(require('contourclock').drawClock(settings.fontIndex),g.getWidth()/2,g.getHeight()-36); + g.drawString('Button to save',g.getWidth()/2,g.getHeight()-18); + } + mainMenu(); }) diff --git a/apps/contourclock/metadata.json b/apps/contourclock/metadata.json index eb0dd39fb..6b2b51991 100644 --- a/apps/contourclock/metadata.json +++ b/apps/contourclock/metadata.json @@ -1,9 +1,10 @@ { "id": "contourclock", "name": "Contour Clock", "shortName" : "Contour Clock", - "version":"0.27", + "version":"0.28", "icon": "app.png", - "description": "A Minimalist clockface with large Digits. Now with more fonts!", + "readme": "README.md", + "description": "A Minimalist clockface with large Digits.", "screenshots" : [{"url":"cc-screenshot-1.png"},{"url":"cc-screenshot-2.png"}], "tags": "clock", "custom": "custom.html", diff --git a/apps/daisy/ChangeLog b/apps/daisy/ChangeLog index b13ce261b..61a09a18d 100644 --- a/apps/daisy/ChangeLog +++ b/apps/daisy/ChangeLog @@ -6,3 +6,4 @@ 0.06: better contrast for light theme, use fg color instead of dithered for ring 0.07: Use default Bangle formatter for booleans 0.08: fix idle timer always getting set to true +0.09: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps diff --git a/apps/daisy/app.js b/apps/daisy/app.js index 848cd1801..c99b19228 100644 --- a/apps/daisy/app.js +++ b/apps/daisy/app.js @@ -1,4 +1,4 @@ -var SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); +var SunCalc = require("suncalc"); // from modules folder const storage = require('Storage'); const locale = require("locale"); const SETTINGS_FILE = "daisy.json"; @@ -70,7 +70,7 @@ function getSteps() { try { return Bangle.getHealthStatus("day").steps; } catch (e) { - if (WIDGETS.wpedom !== undefined) + if (WIDGETS.wpedom !== undefined) return WIDGETS.wpedom.getSteps(); else return 0; @@ -151,7 +151,7 @@ function prevInfo() { function clearInfo() { g.setColor(g.theme.bg); //g.setColor(g.theme.fg); - g.fillRect((w/2) - infoWidth, infoLine - infoHeight, (w/2) + infoWidth, infoLine + infoHeight); + g.fillRect((w/2) - infoWidth, infoLine - infoHeight, (w/2) + infoWidth, infoLine + infoHeight); } function drawInfo() { @@ -202,7 +202,7 @@ function drawClock() { var mm = da[4].substr(3,2); var steps = getSteps(); var p_steps = Math.round(100*(steps/10000)); - + g.reset(); g.setColor(g.theme.bg); g.fillRect(0, 0, w, h); @@ -218,7 +218,7 @@ function drawClock() { g.drawString(mm, (w/2) + 1, h/2); drawInfo(); - + // recalc sunrise / sunset every hour if (drawCount % 60 == 0) updateSunRiseSunSet(new Date(), location.lat, location.lon); @@ -254,7 +254,7 @@ function resetHrm() { Bangle.on('HRM', function(hrm) { hrmCurrent = hrm.bpm; hrmConfidence = hrm.confidence; - log_debug("HRM=" + hrm.bpm + " (" + hrm.confidence + ")"); + log_debug("HRM=" + hrm.bpm + " (" + hrm.confidence + ")"); if (infoMode == "ID_HRM" ) drawHrm(); }); @@ -360,7 +360,7 @@ function getGaugeImage(p) { palette : pal2, buffer : require("heatshrink").decompress(atob("AH4A/AH4AChWq1WpqtUFUgpBFYYABoApggQqDFYlVqBVjFYxZfFQorGLLrWCFZbgbVguoBQcFLD8qFQYMHiosDKzoOJFgZYYKwYPLFgZWawARMLDJWCawgAJcAZWYCZ6FCLCkKFQOgCZ8BFYNUFaZWSLAlAQShWQLAiESQQRtTLAKESFQOoFacFQiSCCwArTgCESQSyEUlTZTboyCnQiSCYQiSCYQiSCZQgdAVxwqYQgSwMVwOoFbMFWBquaWCArBVzKwDbRoqaWATcKbQKuaWAbcKbQKuaWAbcKVzqwNFYIqcWATaKVziwDbhDaebhjaebhgrBbTrcCFZDafbheqFcTcHbT7cDFY0CbT7cDqArxhWqwArfgFVqgrHFUDcBFY0qFcdVFY2oFcMFFY2qFclAFYugFcMBFYsCFctQFYuAFcMAFYsKFctUFYoqigEVFeEqFctVFYmoFccFFYmqFc1AcIdQFccBFf4rbGAoAhKQYr/Fa8FFc9UFYYqkgEVFf4r/FYwDDAEZTDFf4r/Ff4rbqorooArBqArlgIr/Ff4r/Ff4r/Ff4r/Ff4r/Ff4r/Ff4rbqgrlgorCioroAYIr/Ff4r/FbYDDAEZTDFf4r/FYtAFclVFYUBFc9QFf4rZAgoAgKQor/FbFUFccFFYkVFcwFDioFEAD4lFGIorgPogrtWoYAfqorEgIrlqArFAwgAdEg4rlPgqKFADrUHcQorfA4sVA4wAbEY4zHFbh7GRY4AbaY7jBqAqfERArrMBAAZUxNVbkEVFZAJBFcJhRAC6lJFYLcebQIrIBRTaXJhIrhUhLcfD5YLBbjtVFZTceZ5jceJRpkLVyaiLWDpJNFYKwaUIIrMSIKwaDhw6OVx50NFYKwZDZ6waOaCTBQjBGBZZw8CQi4ZBOR6EYeySEYQSCEaQSITDH6BvGIaKEWQSSEEbqQVVQgRYSKwLGUQgRCQKwTFUC4RYQKwSCTDAhEONQTwULAqcNCARWVLAhGMB55YPDhQqDKy4dFFhAMMLCzgFawZWbEI4AIGogAYFZtAFbgsMFTyyGVkBZOKr7gJazoA/AHIA=")) }; - + // p90 if (p >= 90 && p < 100) return { width : 176, height : 176, bpp : 2, @@ -410,7 +410,7 @@ function BUTTON(name,x,y,w,h,c,f,tx) { // if pressed the callback BUTTON.prototype.check = function(x,y) { //console.log(this.name + ":check() x=" + x + " y=" + y +"\n"); - + if (x>= this.x && x<= (this.x + this.w) && y>= this.y && y<= (this.y + this.h)) { log_debug(this.name + ":callback\n"); this.callback(); @@ -472,7 +472,7 @@ function checkIdle() { warned = false; return; } - + let hour = (new Date()).getHours(); let active = (hour >= 9 && hour < 21); //let active = true; @@ -501,7 +501,7 @@ function buzzer(n) { if (n-- < 1) return; Bangle.buzz(250); - + if (buzzTimeout) clearTimeout(buzzTimeout); buzzTimeout = setTimeout(function() { buzzTimeout = undefined; diff --git a/apps/daisy/metadata.json b/apps/daisy/metadata.json index c6cc93620..0bad50151 100644 --- a/apps/daisy/metadata.json +++ b/apps/daisy/metadata.json @@ -1,6 +1,6 @@ { "id": "daisy", "name": "Daisy", - "version":"0.08", + "version":"0.09", "dependencies": {"mylocation":"app"}, "description": "A beautiful digital clock with large ring guage, idle timer and a cyclic information line that includes, day, date, steps, battery, sunrise and sunset times", "icon": "app.png", diff --git a/apps/espruinoprog/metadata.json b/apps/espruinoprog/metadata.json index ebb55b23d..7371e005d 100644 --- a/apps/espruinoprog/metadata.json +++ b/apps/espruinoprog/metadata.json @@ -11,7 +11,8 @@ "custom": "custom.html", "storage": [ {"name":"espruinoprog.app.js","url":"app.js"}, - {"name":"espruinoprog.img","url":"app-icon.js","evaluate":true}, + {"name":"espruinoprog.img","url":"app-icon.js","evaluate":true} + ], "data": [ {"name":"espruinoprog.json"} ] } diff --git a/apps/f9lander/ChangeLog b/apps/f9lander/ChangeLog index 5560f00bc..a13f2a313 100644 --- a/apps/f9lander/ChangeLog +++ b/apps/f9lander/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Add lightning diff --git a/apps/f9lander/app.js b/apps/f9lander/app.js index 7e52104c0..2f17a5bd5 100644 --- a/apps/f9lander/app.js +++ b/apps/f9lander/app.js @@ -46,6 +46,9 @@ var booster = { x : g.getWidth()/4 + Math.random()*g.getWidth()/2, var exploded = false; var nExplosions = 0; var landed = false; +var lightning = 0; + +var settings = require("Storage").readJSON('f9settings.json', 1) || {}; const gravity = 4; const dt = 0.1; @@ -61,18 +64,40 @@ function flameImageGen (throttle) { function drawFalcon(x, y, throttle, angle) { g.setColor(1, 1, 1).drawImage(falcon9, x, y, {rotate:angle}); - if (throttle>0) { + if (throttle>0 || lightning>0) { var flameImg = flameImageGen(throttle); var r = falcon9.height/2 + flameImg.height/2-1; var xoffs = -Math.sin(angle)*r; var yoffs = Math.cos(angle)*r; if (Math.random()>0.7) g.setColor(1, 0.5, 0); else g.setColor(1, 1, 0); - g.drawImage(flameImg, x+xoffs, y+yoffs, {rotate:angle}); + if (throttle>0) g.drawImage(flameImg, x+xoffs, y+yoffs, {rotate:angle}); + if (lightning>1 && lightning<30) { + for (var i=0; i<6; ++i) { + var r = Math.random()*6; + var x = Math.random()*5 - xoffs; + var y = Math.random()*5 - yoffs; + g.setColor(1, Math.random()*0.5+0.5, 0).fillCircle(booster.x+x, booster.y+y, r); + } + } } } +function drawLightning() { + var c = {x:cloudOffs+50, y:30}; + var dx = c.x-booster.x; + var dy = c.y-booster.y; + var m1 = {x:booster.x+0.6*dx+Math.random()*20, y:booster.y+0.6*dy+Math.random()*10}; + var m2 = {x:booster.x+0.4*dx+Math.random()*20, y:booster.y+0.4*dy+Math.random()*10}; + g.setColor(1, 1, 1).drawLine(c.x, c.y, m1.x, m1.y).drawLine(m1.x, m1.y, m2.x, m2.y).drawLine(m2.x, m2.y, booster.x, booster.y); +} + function drawBG() { + if (lightning==1) { + g.setBgColor(1, 1, 1).clear(); + Bangle.buzz(200); + return; + } g.setBgColor(0.2, 0.2, 1).clear(); g.setColor(0, 0, 1).fillRect(0, g.getHeight()-oceanHeight, g.getWidth()-1, g.getHeight()-1); g.setColor(0.5, 0.5, 1).fillCircle(cloudOffs+34, 30, 15).fillCircle(cloudOffs+60, 35, 20).fillCircle(cloudOffs+75, 20, 10); @@ -88,6 +113,7 @@ function renderScreen(input) { drawBG(); showFuel(); drawFalcon(booster.x, booster.y, Math.floor(input.throttle*12), input.angle); + if (lightning>1 && lightning<6) drawLightning(); } function getInputs() { @@ -97,6 +123,7 @@ function getInputs() { if (t > 1) t = 1; if (t < 0) t = 0; if (booster.fuel<=0) t = 0; + if (lightning>0 && lightning<20) t = 0; return {throttle: t, angle: a}; } @@ -121,7 +148,6 @@ function gameStep() { else { var input = getInputs(); if (booster.y >= targetY) { -// console.log(booster.x + " " + booster.y + " " + booster.vy + " " + droneX + " " + input.angle); if (Math.abs(booster.x-droneX-droneShip.width/2)40) && Math.random()>0.98) lightning = 1; booster.x += booster.vx*dt; booster.y += booster.vy*dt; booster.vy += gravity*dt; diff --git a/apps/f9lander/metadata.json b/apps/f9lander/metadata.json index 75c6a0164..1db777099 100644 --- a/apps/f9lander/metadata.json +++ b/apps/f9lander/metadata.json @@ -1,7 +1,7 @@ { "id": "f9lander", "name": "Falcon9 Lander", "shortName":"F9lander", - "version":"0.01", + "version":"0.02", "description": "Land a rocket booster", "icon": "f9lander.png", "screenshots" : [ { "url":"f9lander_screenshot1.png" }, { "url":"f9lander_screenshot2.png" }, { "url":"f9lander_screenshot3.png" }], @@ -10,6 +10,7 @@ "supports" : ["BANGLEJS", "BANGLEJS2"], "storage": [ {"name":"f9lander.app.js","url":"app.js"}, - {"name":"f9lander.img","url":"app-icon.js","evaluate":true} + {"name":"f9lander.img","url":"app-icon.js","evaluate":true}, + {"name":"f9lander.settings.js", "url":"settings.js"} ] } diff --git a/apps/f9lander/settings.js b/apps/f9lander/settings.js new file mode 100644 index 000000000..0f9fba302 --- /dev/null +++ b/apps/f9lander/settings.js @@ -0,0 +1,36 @@ +// This file should contain exactly one function, which shows the app's settings +/** + * @param {function} back Use back() to return to settings menu + */ +const boolFormat = v => v ? /*LANG*/"On" : /*LANG*/"Off"; +(function(back) { + const SETTINGS_FILE = 'f9settings.json' + // initialize with default settings... + let settings = { + 'lightning': false, + } + // ...and overwrite them with any saved values + // This way saved values are preserved if a new version adds more settings + const storage = require('Storage') + const saved = storage.readJSON(SETTINGS_FILE, 1) || {} + for (const key in saved) { + settings[key] = saved[key]; + } + // creates a function to safe a specific setting, e.g. save('color')(1) + function save(key) { + return function (value) { + settings[key] = value; + storage.write(SETTINGS_FILE, settings); + } + } + const menu = { + '': { 'title': 'OpenWind' }, + '< Back': back, + 'Lightning': { + value: settings.lightning, + format: boolFormat, + onchange: save('lightning'), + } + } + E.showMenu(menu); +}) diff --git a/apps/fastload/ChangeLog b/apps/fastload/ChangeLog new file mode 100644 index 000000000..53e3c2591 --- /dev/null +++ b/apps/fastload/ChangeLog @@ -0,0 +1,3 @@ +0.01: New App! +0.02: Allow redirection of loads to the launcher +0.03: Allow hiding the fastloading info screen diff --git a/apps/fastload/README.md b/apps/fastload/README.md new file mode 100644 index 000000000..a1feedcf8 --- /dev/null +++ b/apps/fastload/README.md @@ -0,0 +1,21 @@ +# Fastload Utils + +*EXPERIMENTAL* Use this with caution. When you find something misbehaving please check if the problem actually persists when removing this app. + +This allows fast loading of all apps with two conditions: +* Loaded app contains `Bangle.loadWidgets`. This is needed to prevent problems with apps not expecting widgets to be already loaded. +* Current app can be removed completely from RAM. + +## Settings + +* Allows to redirect all loads usually loading the clock to the launcher instead +* The "Fastloading..." screen can be switched off + +## Technical infos + +This is still experimental but it uses the same mechanism as `.bootcde` does. +It checks the app to be loaded for widget use and stores the result of that and a hash of the js in a cache. + +# Creator + +[halemmerich](https://github.com/halemmerich) diff --git a/apps/fastload/boot.js b/apps/fastload/boot.js new file mode 100644 index 000000000..c9271abbf --- /dev/null +++ b/apps/fastload/boot.js @@ -0,0 +1,66 @@ +{ +const SETTINGS = require("Storage").readJSON("fastload.json") || {}; + +let loadingScreen = function(){ + g.reset(); + + let x = g.getWidth()/2; + let y = g.getHeight()/2; + g.setColor(g.theme.bg); + g.fillRect(x-49, y-19, x+49, y+19); + g.setColor(g.theme.fg); + g.drawRect(x-50, y-20, x+50, y+20); + g.setFont("6x8"); + g.setFontAlign(0,0); + g.drawString("Fastloading...", x, y); + g.flip(true); +}; + +let cache = require("Storage").readJSON("fastload.cache") || {}; + +let checkApp = function(n){ + // no widgets, no problem + if (!global.WIDGETS) return true; + let app = require("Storage").read(n); + if (cache[n] && E.CRC32(app) == cache[n].crc) + return cache[n].fast + cache[n] = {}; + cache[n].fast = app.includes("Bangle.loadWidgets"); + cache[n].crc = E.CRC32(app); + require("Storage").writeJSON("fastload.cache", cache); + return cache[n].fast; +} + +global._load = load; + +let slowload = function(n){ + global._load(n); +} + +let fastload = function(n){ + if (!n || checkApp(n)){ + // Bangle.load can call load, to prevent recursion this must be the system load + global.load = slowload; + Bangle.load(n); + // if fastloading worked, we need to set load back to this method + global.load = fastload; + } + else + slowload(n); +}; +global.load = fastload; + +Bangle.load = (o => (name) => { + if (Bangle.uiRemove && !SETTINGS.hideLoading) loadingScreen(); + if (SETTINGS.autoloadLauncher && !name){ + let orig = Bangle.load; + Bangle.load = (n)=>{ + Bangle.load = orig; + fastload(n); + } + Bangle.showLauncher(); + Bangle.load = orig; + } else + o(name); +})(Bangle.load); +} diff --git a/apps/fastload/icon.png b/apps/fastload/icon.png new file mode 100644 index 000000000..7fe9afe6e Binary files /dev/null and b/apps/fastload/icon.png differ diff --git a/apps/fastload/metadata.json b/apps/fastload/metadata.json new file mode 100644 index 000000000..15adcb7e3 --- /dev/null +++ b/apps/fastload/metadata.json @@ -0,0 +1,16 @@ +{ "id": "fastload", + "name": "Fastload Utils", + "shortName" : "Fastload Utils", + "version": "0.03", + "icon": "icon.png", + "description": "Enable experimental fastloading for more apps", + "type":"bootloader", + "tags": "system", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"fastload.5.boot.js","url":"boot.js"}, + {"name":"fastload.settings.js","url":"settings.js"} + ], + "data": [{"name":"fastload.json"}] +} diff --git a/apps/fastload/settings.js b/apps/fastload/settings.js new file mode 100644 index 000000000..4904e057e --- /dev/null +++ b/apps/fastload/settings.js @@ -0,0 +1,38 @@ +(function(back) { + var FILE="fastload.json"; + var settings; + + function writeSettings(key, value) { + var s = require('Storage').readJSON(FILE, true) || {}; + s[key] = value; + require('Storage').writeJSON(FILE, s); + readSettings(); + } + + function readSettings(){ + settings = require('Storage').readJSON(FILE, true) || {}; + } + + readSettings(); + + function buildMainMenu(){ + var mainmenu = { + '': { 'title': 'Fastload', back: back }, + 'Force load to launcher': { + value: !!settings.autoloadLauncher, + onchange: v => { + writeSettings("autoloadLauncher",v); + } + }, + 'Hide "Fastloading..."': { + value: !!settings.hideLoading, + onchange: v => { + writeSettings("hideLoading",v); + } + } + }; + return mainmenu; + } + + E.showMenu(buildMainMenu()); +}) diff --git a/apps/files/ChangeLog b/apps/files/ChangeLog index 1908f7e5c..4622e6f0f 100644 --- a/apps/files/ChangeLog +++ b/apps/files/ChangeLog @@ -3,4 +3,5 @@ 0.04: Add functionality to sort apps manually or alphabetically ascending/descending. 0.05: Tweaks to help with memory usage 0.06: Reduce memory usage -0.07: Allow negative numbers when manual-sorting \ No newline at end of file +0.07: Allow negative numbers when manual-sorting +0.08: Automatic translation of strings. diff --git a/apps/files/files.js b/apps/files/files.js index e81e9589f..2f7b5c9a1 100644 --- a/apps/files/files.js +++ b/apps/files/files.js @@ -3,20 +3,20 @@ const store = require('Storage'); function showMainMenu() { const mainmenu = { '': { - 'title': 'App Manager', + 'title': /*LANG*/'App Manager', }, '< Back': ()=> {load();}, - 'Sort Apps': () => showSortAppsMenu(), - 'Manage Apps': ()=> showApps(), - 'Compact': () => { - E.showMessage('Compacting...'); + /*LANG*/'Sort Apps': () => showSortAppsMenu(), + /*LANG*/'Manage Apps': ()=> showApps(), + /*LANG*/'Compact': () => { + E.showMessage(/*LANG*/'Compacting...'); try { store.compact(); } catch (e) { } showMainMenu(); }, - 'Free': { + /*LANG*/'Free': { value: undefined, format: (v) => { return store.getFree(); @@ -65,13 +65,13 @@ function eraseData(info) { }); } function eraseApp(app, files,data) { - E.showMessage('Erasing\n' + app.name + '...'); + E.showMessage(/*LANG*/'Erasing\n' + app.name + '...'); var info = store.readJSON(app.id + ".info", 1)||{}; if (files) eraseFiles(info); if (data) eraseData(info); } function eraseOne(app, files,data){ - E.showPrompt('Erase\n'+app.name+'?').then((v) => { + E.showPrompt(/*LANG*/'Erase\n'+app.name+'?').then((v) => { if (v) { Bangle.buzz(100, 1); eraseApp(app, files, data); @@ -82,7 +82,7 @@ function eraseOne(app, files,data){ }); } function eraseAll(apps, files,data) { - E.showPrompt('Erase all?').then((v) => { + E.showPrompt(/*LANG*/'Erase all?').then((v) => { if (v) { Bangle.buzz(100, 1); apps.forEach(app => eraseApp(app, files, data)); @@ -99,11 +99,11 @@ function showAppMenu(app) { '< Back': () => showApps(), }; if (app.hasData) { - appmenu['Erase Completely'] = () => eraseOne(app, true, true); - appmenu['Erase App,Keep Data'] = () => eraseOne(app, true, false); - appmenu['Only Erase Data'] = () => eraseOne(app, false, true); + appmenu[/*LANG*/'Erase Completely'] = () => eraseOne(app, true, true); + appmenu[/*LANG*/'Erase App,Keep Data'] = () => eraseOne(app, true, false); + appmenu[/*LANG*/'Only Erase Data'] = () => eraseOne(app, false, true); } else { - appmenu['Erase'] = () => eraseOne(app, true, false); + appmenu[/*LANG*/'Erase'] = () => eraseOne(app, true, false); } E.showMenu(appmenu); } @@ -111,7 +111,7 @@ function showAppMenu(app) { function showApps() { const appsmenu = { '': { - 'title': 'Apps', + 'title': /*LANG*/'Apps', }, '< Back': () => showMainMenu(), }; @@ -128,17 +128,17 @@ function showApps() { menu[app.name] = () => showAppMenu(app); return menu; }, appsmenu); - appsmenu['Erase All'] = () => { + appsmenu[/*LANG*/'Erase All'] = () => { E.showMenu({ - '': {'title': 'Erase All'}, - 'Erase Everything': () => eraseAll(list, true, true), - 'Erase Apps,Keep Data': () => eraseAll(list, true, false), - 'Only Erase Data': () => eraseAll(list, false, true), + '': {'title': /*LANG*/'Erase All'}, + /*LANG*/'Erase Everything': () => eraseAll(list, true, true), + /*LANG*/'Erase Apps,Keep Data': () => eraseAll(list, true, false), + /*LANG*/'Only Erase Data': () => eraseAll(list, false, true), '< Back': () => showApps(), }); }; } else { - appsmenu['...No Apps...'] = { + appsmenu[/*LANG*/'...No Apps...'] = { value: undefined, format: ()=> '', onchange: ()=> {} @@ -150,16 +150,16 @@ function showApps() { function showSortAppsMenu() { const sorterMenu = { '': { - 'title': 'App Sorter', + 'title': /*LANG*/'App Sorter', }, '< Back': () => showMainMenu(), - 'Sort: manually': ()=> showSortAppsManually(), - 'Sort: alph. ASC': () => { - E.showMessage('Sorting:\nAlphabetically\nascending ...'); + /*LANG*/'Sort: manually': ()=> showSortAppsManually(), + /*LANG*/'Sort: alph. ASC': () => { + E.showMessage(/*LANG*/'Sorting:\nAlphabetically\nascending ...'); sortAlphabet(false); }, 'Sort: alph. DESC': () => { - E.showMessage('Sorting:\nAlphabetically\ndescending ...'); + E.showMessage(/*LANG*/'Sorting:\nAlphabetically\ndescending ...'); sortAlphabet(true); } }; @@ -169,7 +169,7 @@ function showSortAppsMenu() { function showSortAppsManually() { const appsSorterMenu = { '': { - 'title': 'Sort: manually', + 'title': /*LANG*/'Sort: manually', }, '< Back': () => showSortAppsMenu(), }; @@ -186,7 +186,7 @@ function showSortAppsManually() { return menu; }, appsSorterMenu); } else { - appsSorterMenu['...No Apps...'] = { + appsSorterMenu[/*LANG*/'...No Apps...'] = { value: undefined, format: ()=> '', onchange: ()=> {} diff --git a/apps/files/metadata.json b/apps/files/metadata.json index ac73a7717..a53f914e6 100644 --- a/apps/files/metadata.json +++ b/apps/files/metadata.json @@ -1,7 +1,7 @@ { "id": "files", "name": "App Manager", - "version": "0.07", + "version": "0.08", "description": "Show currently installed apps, free space, and allow their deletion from the watch", "icon": "files.png", "tags": "tool,system,files", diff --git a/apps/gallery/ChangeLog b/apps/gallery/ChangeLog index 76db22053..0167bc1f9 100644 --- a/apps/gallery/ChangeLog +++ b/apps/gallery/ChangeLog @@ -1,2 +1,3 @@ 0.01: New app! -0.02: Submitted to app loader \ No newline at end of file +0.02: Submitted to app loader +0.03: Do not invert colors diff --git a/apps/gallery/README.md b/apps/gallery/README.md index b70fa07c2..65a0c36d3 100644 --- a/apps/gallery/README.md +++ b/apps/gallery/README.md @@ -8,6 +8,8 @@ Upon opening the gallery app, you will be presented with a list of images that y ## Adding images +Once this app is installed you can manage images by pressing the Disk icon next to it or by following the manual steps below: + 1. The gallery app does not perform any scaling, and does not support panning. Therefore, you should use your favorite image editor to produce an image of the appropriate size for your watch. (240x240 for Bangle 1 or 176x176 for Bangle 2.) How you achieve this is up to you. If on a Bangle 2, I recommend adjusting the colors here to comply with the color restrictions. 2. Upload your image to the [Espruino image converter](https://www.espruino.com/Image+Converter). I recommend enabling compression and choosing one of the following color settings: @@ -15,4 +17,4 @@ Upon opening the gallery app, you will be presented with a list of images that y * 3 bit RGB for Bangle 2 * 1 bit black/white for monochrome images that you want to respond to your system theme. (White will be rendered as your foreground color and black will be rendered as your background color.) -3. Set the output format to an image string, copy it into the [IDE](https://www.espruino.com/ide/), and set the destination to a file in storage. The file name should begin with "gal-" (without the quotes) and end with ".img" (without the quotes) to appear in the gallery. Note that the gal- prefix and .img extension will be removed in the UI. Upload the file. \ No newline at end of file +3. Set the output format to an image string, copy it into the [IDE](https://www.espruino.com/ide/), and set the destination to a file in storage. The file name should begin with "gal-" (without the quotes) and end with ".img" (without the quotes) to appear in the gallery. Note that the gal- prefix and .img extension will be removed in the UI. Upload the file. diff --git a/apps/gallery/app.js b/apps/gallery/app.js index ca9392f13..6fb2bdf49 100644 --- a/apps/gallery/app.js +++ b/apps/gallery/app.js @@ -35,7 +35,7 @@ function drawImage(fileName) { Bangle.setLCDBrightness(1); // Full brightness image = eval(storage.read(fileName)); // Sadly, the only reasonable way to do this - g.clear().reset().drawImage(image, 88, 88, { rotate: angle }); + g.clear().reset().setBgColor(0).setColor("#fff").drawImage(image, 88, 88, { rotate: angle }); } setWatch(info => { @@ -44,9 +44,9 @@ setWatch(info => { else angle = 0; Bangle.buzz(); - g.clear().reset().drawImage(image, 88, 88, { rotate: angle }) + g.clear().reset().setBgColor(0).setColor("#fff").drawImage(image, 88, 88, { rotate: angle }) } }, BTN1, { repeat: true }); // We don't load the widgets because there is no reasonable way to unload them -drawMenu(); \ No newline at end of file +drawMenu(); diff --git a/apps/gallery/interface.html b/apps/gallery/interface.html new file mode 100644 index 000000000..f309270ca --- /dev/null +++ b/apps/gallery/interface.html @@ -0,0 +1,165 @@ + + + + + + + + + +

Existing Images

+
    +
+ +

Convert & Upload Images

+ +
+ Use Compression?
+ Transparency to Color
+ Transparency?
+ Inverted?
+ Crop?
+ Diffusion:
+ + Brightness: +
+ Contrast: +
+ Colours:
+ + + + + + + + diff --git a/apps/gallery/metadata.json b/apps/gallery/metadata.json index 89f7606aa..00ac42075 100644 --- a/apps/gallery/metadata.json +++ b/apps/gallery/metadata.json @@ -1,7 +1,7 @@ { "id": "gallery", "name": "Gallery", - "version": "0.02", + "version": "0.03", "description": "A gallery that lets you view images uploaded with the IDE (see README)", "readme": "README.md", "icon": "icon.png", @@ -12,6 +12,7 @@ "BANGLEJS" ], "allow_emulator": true, + "interface": "interface.html", "storage": [ { "name": "gallery.app.js", @@ -23,4 +24,4 @@ "evaluate": true } ] -} \ No newline at end of file +} diff --git a/apps/gpstrek/ChangeLog b/apps/gpstrek/ChangeLog index c36c23c72..849088c64 100644 --- a/apps/gpstrek/ChangeLog +++ b/apps/gpstrek/ChangeLog @@ -13,3 +13,4 @@ Reconstruct battery voltage by using calibrated batFullVoltage Averaging for smoothing compass headings Save state if route or waypoint has been chosen +0.09: Workaround a minifier issue allowing to install gpstrek with minification enabled diff --git a/apps/gpstrek/app.js b/apps/gpstrek/app.js index b3ec79fd2..1f46ae2b4 100644 --- a/apps/gpstrek/app.js +++ b/apps/gpstrek/app.js @@ -259,7 +259,8 @@ let getCompassSlice = function(compassDataSource){ if (compassDataSource.getPoints){ - for (let p of compassDataSource.getPoints()){ + let points = compassDataSource.getPoints(); //storing this in a variable works around a minifier bug causing a problem in the next line: for(let a of a.getPoints()) + for (let p of points){ g.reset(); var bpos = p.bearing - lastDrawnValue; if (bpos>180) bpos -=360; @@ -285,7 +286,8 @@ let getCompassSlice = function(compassDataSource){ } } if (compassDataSource.getMarkers){ - for (let m of compassDataSource.getMarkers()){ + let markers = compassDataSource.getMarkers(); //storing this in a variable works around a minifier bug causing a problem in the next line: for(let a of a.getMarkers()) + for (let m of markers){ g.reset(); g.setColor(m.fillcolor); let mpos = m.xpos * width; diff --git a/apps/gpstrek/metadata.json b/apps/gpstrek/metadata.json index 3e27a3247..2e2e481af 100644 --- a/apps/gpstrek/metadata.json +++ b/apps/gpstrek/metadata.json @@ -1,7 +1,7 @@ { "id": "gpstrek", "name": "GPS Trekking", - "version": "0.08", + "version": "0.09", "description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!", "icon": "icon.png", "screenshots": [{"url":"screen1.png"},{"url":"screen2.png"},{"url":"screen3.png"},{"url":"screen4.png"}], diff --git a/apps/ha/ChangeLog b/apps/ha/ChangeLog index a4865be3f..c0d58e5bc 100644 --- a/apps/ha/ChangeLog +++ b/apps/ha/ChangeLog @@ -2,4 +2,6 @@ 0.02: Includeas the ha.lib.js library that can be used by other apps or clocks. 0.03: Added clkinfo for clocks. 0.04: Feedback if clkinfo run is called. -0.05: Clkinfo improvements. \ No newline at end of file +0.05: Clkinfo improvements. +0.06: Updated clkinfo icon. +0.07: Update clock_info to avoid a redraw diff --git a/apps/ha/ha.clkinfo.js b/apps/ha/ha.clkinfo.js index 1b1e468d7..09724ba45 100644 --- a/apps/ha/ha.clkinfo.js +++ b/apps/ha/ha.clkinfo.js @@ -4,7 +4,7 @@ var haItems = { name: "Home", - img: atob("GBiBAf/////////n///D//+B//8A//48T/wkD/gkD/A8D+AYB8AYA4eZ4QyZMOyZN+fb5+D/B+B+B+A8B+AYB+AYB+AYB+AYB+A8Bw=="), + img: atob("GBiBAAAAAAAAAAAAAAAYAAA+AAB+AADD4AHb4APD4Afn8A/n+BxmOD0mnA0ksAwAMA+B8A/D8A/n8A/n8A/n8A/n8AAAAAAAAAAAAA=="), items: [] }; @@ -12,7 +12,7 @@ haItems.items.push({ name: null, get: () => ({ text: trigger.display, img: trigger.getIcon()}), - show: function() { haItems.items[i].emit("redraw"); }, + show: function() {}, hide: function () {}, run: function() { ha.sendTrigger("TRIGGER_BW"); @@ -23,4 +23,4 @@ }); return haItems; -}) \ No newline at end of file +}) diff --git a/apps/ha/metadata.json b/apps/ha/metadata.json index fad052544..1432e010e 100644 --- a/apps/ha/metadata.json +++ b/apps/ha/metadata.json @@ -1,7 +1,7 @@ { "id": "ha", "name": "HomeAssistant", - "version": "0.05", + "version": "0.07", "description": "Integrates your BangleJS into HomeAssistant.", "icon": "ha.png", "type": "app", diff --git a/apps/hcclock/ChangeLog b/apps/hcclock/ChangeLog index 289c7ac2d..e2eb18be3 100644 --- a/apps/hcclock/ChangeLog +++ b/apps/hcclock/ChangeLog @@ -1,4 +1,5 @@ 0.01: Base code 0.02: Saved settings when switching color scheme 0.03: Added Button 3 opening messages (if app is installed) -0.04: Use `messages` library to check for new messages \ No newline at end of file +0.04: Use `messages` library to check for new messages +0.05: Use `messages` library to open message GUI \ No newline at end of file diff --git a/apps/hcclock/hcclock.app.js b/apps/hcclock/hcclock.app.js index 9558c052b..f12a4733e 100644 --- a/apps/hcclock/hcclock.app.js +++ b/apps/hcclock/hcclock.app.js @@ -234,7 +234,7 @@ function handleMessages() { if(!hasMessages()) return; E.showMessage("Loading Messages..."); - load("messages.app.js"); + require("messages").openGUI(); } function hasMessages() diff --git a/apps/hcclock/metadata.json b/apps/hcclock/metadata.json index b8f8c14b9..407114e25 100644 --- a/apps/hcclock/metadata.json +++ b/apps/hcclock/metadata.json @@ -1,7 +1,7 @@ { "id": "hcclock", "name": "Hi-Contrast Clock", - "version": "0.04", + "version": "0.05", "description": "Hi-Contrast Clock : A simple yet very bold clock that aims to be readable in high luninosity environments. Uses big 10x5 pixel digits. Use BTN 1 to switch background and foreground colors.", "icon": "hcclock-icon.png", "type": "clock", diff --git a/apps/health/ChangeLog b/apps/health/ChangeLog index 62d93e606..fc8f2c950 100644 --- a/apps/health/ChangeLog +++ b/apps/health/ChangeLog @@ -14,3 +14,5 @@ 0.13: Add support for internationalization 0.14: Move settings 0.15: Fix charts (fix #1366) +0.16: Code tidyup, add back button in top left of health app graphs +0.17: Add automatic translation of bar chart labels diff --git a/apps/health/app.js b/apps/health/app.js index c0a40bd93..844dd7241 100644 --- a/apps/health/app.js +++ b/apps/health/app.js @@ -1,6 +1,4 @@ function menuMain() { - swipe_enabled = false; - clearButton(); E.showMenu({ "": { title: /*LANG*/"Health Tracking" }, /*LANG*/"< Back": () => load(), @@ -12,8 +10,6 @@ function menuMain() { } function menuStepCount() { - swipe_enabled = false; - clearButton(); E.showMenu({ "": { title:/*LANG*/"Steps" }, /*LANG*/"< Back": () => menuMain(), @@ -23,8 +19,6 @@ function menuStepCount() { } function menuMovement() { - swipe_enabled = false; - clearButton(); E.showMenu({ "": { title:/*LANG*/"Movement" }, /*LANG*/"< Back": () => menuMain(), @@ -34,8 +28,6 @@ function menuMovement() { } function menuHRM() { - swipe_enabled = false; - clearButton(); E.showMenu({ "": { title:/*LANG*/"Heart Rate" }, /*LANG*/"< Back": () => menuMain(), @@ -48,22 +40,16 @@ function stepsPerHour() { E.showMessage(/*LANG*/"Loading..."); var data = new Uint16Array(24); require("health").readDay(new Date(), h=>data[h.hr]+=h.steps); - g.clear(1); - Bangle.drawWidgets(); - g.reset(); setButton(menuStepCount); - barChart("HOUR", data); + barChart(/*LANG*/"HOUR", data); } function stepsPerDay() { E.showMessage(/*LANG*/"Loading..."); var data = new Uint16Array(31); require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps); - g.clear(1); - Bangle.drawWidgets(); - g.reset(); setButton(menuStepCount); - barChart("DAY", data); + barChart(/*LANG*/"DAY", data); } function hrmPerHour() { @@ -75,11 +61,8 @@ function hrmPerHour() { if (h.bpm) cnt[h.hr]++; }); data.forEach((d,i)=>data[i] = d/cnt[i]); - g.clear(1); - Bangle.drawWidgets(); - g.reset(); setButton(menuHRM); - barChart("HOUR", data); + barChart(/*LANG*/"HOUR", data); } function hrmPerDay() { @@ -91,37 +74,27 @@ function hrmPerDay() { if (h.bpm) cnt[h.day]++; }); data.forEach((d,i)=>data[i] = d/cnt[i]); - g.clear(1); - Bangle.drawWidgets(); - g.reset(); setButton(menuHRM); - barChart("DAY", data); + barChart(/*LANG*/"DAY", data); } function movementPerHour() { E.showMessage(/*LANG*/"Loading..."); var data = new Uint16Array(24); require("health").readDay(new Date(), h=>data[h.hr]+=h.movement); - g.clear(1); - Bangle.drawWidgets(); - g.reset(); setButton(menuMovement); - barChart("HOUR", data); + barChart(/*LANG*/"HOUR", data); } function movementPerDay() { E.showMessage(/*LANG*/"Loading..."); var data = new Uint16Array(31); require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement); - g.clear(1); - Bangle.drawWidgets(); - g.reset(); setButton(menuMovement); - barChart("DAY", data); + barChart(/*LANG*/"DAY", data); } // Bar Chart Code - const w = g.getWidth(); const h = g.getHeight(); @@ -130,13 +103,10 @@ var chart_index; var chart_max_datum; var chart_label; var chart_data; -var swipe_enabled = false; -var btn; // find the max value in the array, using a loop due to array size function max(arr) { var m = -Infinity; - for(var i=0; i< arr.length; i++) if(arr[i] > m) m = arr[i]; return m; @@ -145,10 +115,8 @@ function max(arr) { // find the end of the data, the array might be for 31 days but only have 2 days of data in it function get_data_length(arr) { var nlen = arr.length; - for(var i = arr.length - 1; i > 0 && arr[i] == 0; i--) nlen--; - return nlen; } @@ -167,15 +135,11 @@ function drawBarChart() { const bar_width = (w - 2) / 9; // we want 9 bars, bar 5 in the centre var bar_top; var bar; - - g.setColor(g.theme.bg); - g.fillRect(0,24,w,h); + g.reset().clearRect(0,24,w,h); for (bar = 1; bar < 10; bar++) { if (bar == 5) { - g.setFont('6x8', 2); - g.setFontAlign(0,-1); - g.setColor(g.theme.fg); + g.setFont('6x8', 2).setFontAlign(0,-1).setColor(g.theme.fg); g.drawString(chart_label + " " + (chart_index + bar -1) + " " + chart_data[chart_index + bar - 1], g.getWidth()/2, 150); g.setColor("#00f"); } else { @@ -189,45 +153,26 @@ function drawBarChart() { bar_top = bar_bot; g.fillRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top); - g.setColor(g.theme.fg); - g.drawRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top); + g.setColor(g.theme.fg).drawRect( 1 + (bar - 1)* bar_width, bar_bot, 1 + bar*bar_width, bar_top); } } -function next_bar() { - chart_index = Math.min(data_len - 5, chart_index + 1); -} - -function prev_bar() { - // HOUR data starts at index 0, DAY data starts at index 1 - chart_index = Math.max((chart_label == "DAY") ? -3 : -4, chart_index - 1); -} - -Bangle.on('swipe', dir => { - if (!swipe_enabled) return; - if (dir == 1) prev_bar(); else next_bar(); - drawBarChart(); -}); - -// use setWatch() as Bangle.setUI("updown",..) interacts with swipes function setButton(fn) { - // cancel callback, otherwise a slight up down movement will show the E.showMenu() - Bangle.setUI("updown", undefined); - - if (process.env.HWVERSION == 1) - btn = setWatch(fn, BTN2); - else - btn = setWatch(fn, BTN1); -} - -function clearButton() { - if (btn !== undefined) { - clearWatch(btn); - btn = undefined; - } + Bangle.setUI({mode:"custom", + back:fn, + swipe:(lr,ud) => { + if (lr == 1) { + // HOUR data starts at index 0, DAY data starts at index 1 + chart_index = Math.max((chart_label == /*LANG*/"DAY") ? -3 : -4, chart_index - 1); + } else if (lr<0) { + chart_index = Math.min(data_len - 5, chart_index + 1); + } else { + return fn(); + } + drawBarChart(); + }}); } Bangle.loadWidgets(); Bangle.drawWidgets(); - menuMain(); diff --git a/apps/health/metadata.json b/apps/health/metadata.json index a038f67b5..12f6b617f 100644 --- a/apps/health/metadata.json +++ b/apps/health/metadata.json @@ -1,7 +1,8 @@ { "id": "health", "name": "Health Tracking", - "version": "0.15", + "shortName": "Health", + "version": "0.17", "description": "Logs health data and provides an app to view it", "icon": "app.png", "tags": "tool,system,health", diff --git a/apps/hourstrike/metadata.json b/apps/hourstrike/metadata.json index 614db54e4..d0ddb511a 100644 --- a/apps/hourstrike/metadata.json +++ b/apps/hourstrike/metadata.json @@ -11,7 +11,8 @@ "storage": [ {"name":"hourstrike.app.js","url":"app.js"}, {"name":"hourstrike.boot.js","url":"boot.js"}, - {"name":"hourstrike.img","url":"app-icon.js","evaluate":true}, + {"name":"hourstrike.img","url":"app-icon.js","evaluate":true} + ], "data" : [ {"name":"hourstrike.json","url":"hourstrike.json"} ] } diff --git a/apps/hrm/ChangeLog b/apps/hrm/ChangeLog index 62956e8cd..b55ba8930 100644 --- a/apps/hrm/ChangeLog +++ b/apps/hrm/ChangeLog @@ -8,3 +8,4 @@ 0.08: Don't force backlight on/watch unlocked on Bangle 2 0.09: Grey out BPM until confidence is over 50% 0.10: Autoscale raw graph to maximum value seen +0.11: Automatic translation of strings. diff --git a/apps/hrm/heartrate.js b/apps/hrm/heartrate.js index 386341e6d..2e5a720e5 100644 --- a/apps/hrm/heartrate.js +++ b/apps/hrm/heartrate.js @@ -37,7 +37,7 @@ function updateHrm(){ var px = g.getWidth()/2; g.setFontAlign(0,-1); g.clearRect(0,24,g.getWidth(),80); - g.setFont("6x8").drawString("Confidence "+(hrmInfo.confidence || "--")+"%", px, 70); + g.setFont("6x8").drawString(/*LANG*/"Confidence "+(hrmInfo.confidence || "--")+"%", px, 70); updateScale(); @@ -46,7 +46,7 @@ function updateHrm(){ g.setFontVector(40).setColor(hrmInfo.confidence > 50 ? g.theme.fg : "#888").drawString(str,px,45); px += g.stringWidth(str)/2; g.setFont("6x8").setColor(g.theme.fg); - g.drawString("BPM",px+15,45); + g.drawString(/*LANG*/"BPM",px+15,45); } function updateScale(){ @@ -101,7 +101,7 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); g.setColor(g.theme.fg); g.reset().setFont("6x8",2).setFontAlign(0,-1); -g.drawString("Please wait...",g.getWidth()/2,g.getHeight()/2 - 16); +g.drawString(/*LANG*/"Please wait...",g.getWidth()/2,g.getHeight()/2 - 16); countDown(); diff --git a/apps/hrm/metadata.json b/apps/hrm/metadata.json index c5a5f4f4d..f254b5d23 100644 --- a/apps/hrm/metadata.json +++ b/apps/hrm/metadata.json @@ -1,7 +1,7 @@ { "id": "hrm", "name": "Heart Rate Monitor", - "version": "0.10", + "version": "0.11", "description": "Measure your heart rate and see live sensor data", "icon": "heartrate.png", "tags": "health", diff --git a/apps/hrmmar/README.md b/apps/hrmmar/README.md new file mode 100644 index 000000000..ff90d9156 --- /dev/null +++ b/apps/hrmmar/README.md @@ -0,0 +1,11 @@ +# HRM Motion Artifacts removal + +Measurements from the build in PPG-Sensor (Photoplethysmograph) is sensitive to motion and can be corrupted with Motion Artifacts (MA). This module allows to remove these. + +## Settings + +* **MA removal** + +Select the algorithm to Remove Motion artifacts: + - None: (default) No Motion Artifact removal. + - fft elim: (*experimental*) Remove Motion Artifacts by cutting out the frequencies from the HRM frequency spectrum that are noisy in acceleration spectrum. Under motion this can report a heart rate that is closer to the real one but will fail if motion frequency and heart rate overlap. diff --git a/apps/hrmmar/app.png b/apps/hrmmar/app.png new file mode 100644 index 000000000..9db19be37 Binary files /dev/null and b/apps/hrmmar/app.png differ diff --git a/apps/hrmmar/boot.js b/apps/hrmmar/boot.js new file mode 100644 index 000000000..52d88c313 --- /dev/null +++ b/apps/hrmmar/boot.js @@ -0,0 +1,40 @@ +{ + let bpm_corrected; // result of algorithm + + const updateHrm = (bpm) => { + bpm_corrected = bpm; + }; + + Bangle.on('HRM', (hrm) => { + if (bpm_corrected > 0) { + // replace bpm data in event + hrm.bpm_orig = hrm.bpm; + hrm.confidence_orig = hrm.confidence; + hrm.bpm = bpm_corrected; + hrm.confidence = 0; + } + }); + + let run = () => { + const settings = Object.assign({ + mAremoval: 0 + }, require("Storage").readJSON("hrmmar.json", true) || {}); + + // select motion artifact removal algorithm + switch(settings.mAremoval) { + case 1: + require("hrmfftelim").run(settings, updateHrm); + break; + } + } + + // override setHRMPower so we can run our code on HRM enable + const oldSetHRMPower = Bangle.setHRMPower; + Bangle.setHRMPower = function(on, id) { + if (on && run !== undefined) { + run(); + run = undefined; // Make sure we run only once + } + return oldSetHRMPower(on, id); + }; +} diff --git a/apps/hrmmar/fftelim.js b/apps/hrmmar/fftelim.js new file mode 100644 index 000000000..98b7f33ad --- /dev/null +++ b/apps/hrmmar/fftelim.js @@ -0,0 +1,190 @@ +exports.run = (settings, updateHrm) => { + const SAMPLE_RATE = 12.5; + const NUM_POINTS = 256; // fft size + const ACC_PEAKS = 2; // remove this number of ACC peaks + + // ringbuffers + const hrmvalues = new Int16Array(8*SAMPLE_RATE); + const accvalues = new Int16Array(8*SAMPLE_RATE); + // fft buffers + const hrmfftbuf = new Int16Array(NUM_POINTS); + const accfftbuf = new Int16Array(NUM_POINTS); + let BPM_est_1 = 0; + let BPM_est_2 = 0; + + let hrmdata; + let idx=0, wraps=0; + + // init settings + Bangle.setOptions({hrmPollInterval: 40, powerSave: false}); // hrm=25Hz + Bangle.setPollInterval(80); // 12.5Hz + + calcfft = (values, idx, normalize, fftbuf) => { + fftbuf.fill(0); + let i_out=0; + let avg = 0; + if (normalize) { + const sum = values.reduce((a, b) => a + b, 0); + avg = sum/values.length; + } + // sort ringbuffer to fft buffer + for(let i_in=idx; i_in { + let maxVal = -Number.MAX_VALUE; + let maxIdx = 0; + + values.forEach((value,i) => { + if (value > maxVal) { + maxVal = value; + maxIdx = i; + } + }); + return {idx: maxIdx, val: maxVal}; + }; + + getSign = (value) => { + return value < 0 ? -1 : 1; + }; + + // idx in fft buffer to frequency + getFftFreq = (idx, rate, size) => { + return idx*rate/(size-1); + }; + + // frequency to idx in fft buffer + getFftIdx = (freq, rate, size) => { + return Math.round(freq*(size-1)/rate); + }; + + calc2ndDeriative = (values) => { + const result = new Int16Array(values.length-2); + for(let i=1; i { + // fft + const ppg_fft = calcfft(hrmvalues, idx, true, hrmfftbuf).subarray(minFreqIdx, maxFreqIdx+1); + const acc_fft = calcfft(accvalues, idx, false, accfftbuf).subarray(minFreqIdx, maxFreqIdx+1); + + // remove spectrum that have peaks in acc fft from ppg fft + const accGlobalMax = getMax(acc_fft); + const acc2nddiff = calc2ndDeriative(acc_fft); // calculate second derivative + for(let iClean=0; iClean < ACC_PEAKS; iClean++) { + // get max peak in ACC + const accMax = getMax(acc_fft); + + if (accMax.val >= 10 && accMax.val/accGlobalMax.val > 0.75) { + // set all values in PPG FFT to zero until second derivative of ACC has zero crossing + for (let k = accMax.idx-1; k>=0; k--) { + ppg_fft[k] = 0; + acc_fft[k] = -Math.abs(acc_fft[k]); // max(acc_fft) should no longer find this + if (k-2 > 0 && getSign(acc2nddiff[k-1-2]) != getSign(acc2nddiff[k-2]) && Math.abs(acc_fft[k]) < accMax.val*0.75) { + break; + } + } + // set all values in PPG FFT to zero until second derivative of ACC has zero crossing + for (let k = accMax.idx; k < acc_fft.length-1; k++) { + ppg_fft[k] = 0; + acc_fft[k] = -Math.abs(acc_fft[k]); // max(acc_fft) should no longer find this + if (k-2 >= 0 && getSign(acc2nddiff[k+1-2]) != getSign(acc2nddiff[k-2]) && Math.abs(acc_fft[k]) < accMax.val*0.75) { + break; + } + } + } + } + + // bpm result is maximum peak in PPG fft + const hrRangeMax = getMax(ppg_fft.subarray(rangeIdx[0], rangeIdx[1])); + const hrTotalMax = getMax(ppg_fft); + const maxDiff = hrTotalMax.val/hrRangeMax.val; + let idxMaxPPG = hrRangeMax.idx+rangeIdx[0]; // offset range limit + + if ((maxDiff > 3 && idxMaxPPG != hrTotalMax.idx) || hrRangeMax.val === 0) { // prevent tracking from loosing the real heart rate by checking the full spectrum + if (hrTotalMax.idx > idxMaxPPG) { + idxMaxPPG = idxMaxPPG+Math.ceil(6/freqStep); // step 6 BPM up into the direction of max peak + } else { + idxMaxPPG = idxMaxPPG-Math.ceil(2/freqStep); // step 2 BPM down into the direction of max peak + } + } + + idxMaxPPG = idxMaxPPG + minFreqIdx; + const BPM_est_0 = getFftFreq(idxMaxPPG, SAMPLE_RATE, NUM_POINTS)*60; + + // smooth with moving average + let BPM_est_res; + if (BPM_est_2 > 0) { + BPM_est_res = 0.9*BPM_est_0 + 0.05*BPM_est_1 + 0.05*BPM_est_2; + } else { + BPM_est_res = BPM_est_0; + } + + return BPM_est_res.toFixed(1); + }; + + Bangle.on('HRM-raw', (hrm) => { + hrmdata = hrm; + }); + + Bangle.on('accel', (acc) => { + if (hrmdata !== undefined) { + hrmvalues[idx] = hrmdata.filt; + accvalues[idx] = acc.x*1000 + acc.y*1000 + acc.z*1000; + idx++; + if (idx >= 8*SAMPLE_RATE) { + idx = 0; + wraps++; + } + + if (idx % (SAMPLE_RATE*2) == 0) { // every two seconds + if (wraps === 0) { // use rate of firmware until hrmvalues buffer is filled + updateHrm(undefined); + BPM_est_2 = BPM_est_1; + BPM_est_1 = hrmdata.bpm; + } else { + let bpm_result; + if (hrmdata.confidence >= 90) { // display firmware value if good + bpm_result = hrmdata.bpm; + updateHrm(undefined); + } else { + bpm_result = calculate(idx); + bpm_corrected = bpm_result; + updateHrm(bpm_result); + } + BPM_est_2 = BPM_est_1; + BPM_est_1 = bpm_result; + + // set search range of next BPM + const est_res_idx = getFftIdx(bpm_result/60, SAMPLE_RATE, NUM_POINTS)-minFreqIdx; + rangeIdx = [est_res_idx-maxBpmDiffIdxDown, est_res_idx+maxBpmDiffIdxUp]; + if (rangeIdx[0] < 0) { + rangeIdx[0] = 0; + } + if (rangeIdx[1] > maxFreqIdx-minFreqIdx) { + rangeIdx[1] = maxFreqIdx-minFreqIdx; + } + } + } + } + }); +}; diff --git a/apps/hrmmar/metadata.json b/apps/hrmmar/metadata.json new file mode 100644 index 000000000..232ff64a7 --- /dev/null +++ b/apps/hrmmar/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "hrmmar", + "name": "HRM Motion Artifacts removal", + "shortName":"HRM MA removal", + "icon": "app.png", + "version":"0.01", + "description": "Removes Motion Artifacts in Bangle.js's heart rate sensor data.", + "type": "bootloader", + "tags": "health", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"hrmmar.boot.js","url":"boot.js"}, + {"name":"hrmfftelim","url":"fftelim.js"}, + {"name":"hrmmar.settings.js","url":"settings.js"} + ], + "data": [{"name":"hrmmar.json"}] +} diff --git a/apps/hrmmar/settings.js b/apps/hrmmar/settings.js new file mode 100644 index 000000000..3c6e62c91 --- /dev/null +++ b/apps/hrmmar/settings.js @@ -0,0 +1,26 @@ +(function(back) { + var FILE = "hrmmar.json"; + // Load settings + var settings = Object.assign({ + mAremoval: 0, + }, require('Storage').readJSON(FILE, true) || {}); + + function writeSettings() { + require('Storage').writeJSON(FILE, settings); + } + + // Show the menu + E.showMenu({ + "" : { "title" : "HRM MA removal" }, + "< Back" : () => back(), + 'MA removal': { + value: settings.mAremoval, + min: 0, max: 1, + format: v => ["None", "fft elim."][v], + onchange: v => { + settings.mAremoval = v; + writeSettings(); + } + }, + }); +}) diff --git a/apps/hworldclock/ChangeLog b/apps/hworldclock/ChangeLog index 8c1517842..99727c50b 100644 --- a/apps/hworldclock/ChangeLog +++ b/apps/hworldclock/ChangeLog @@ -9,3 +9,11 @@ 0.23: Added note to configure position in "my location" if not done yet. Small fixes. 0.24: Added fast load 0.25: Minor code optimization +0.26: BJS2: Swipe down to rotate 180 degree +0.27: BJS2: Changed swipe down to swipe up +0.28: Reverted changes to implementation of 0.25 +0.29: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps +0.30: BJS2: swipe seems to be working now +0.31: Tweaking the swipe option; Added mylocation as a dependency. + Remove calls to Bangle.loadWidgets as they are not needed and create warnings +0.32: Added setting to show single timezone small, like where multiple ones are configured \ No newline at end of file diff --git a/apps/hworldclock/README.md b/apps/hworldclock/README.md index 25cc368ca..d8343e5b9 100644 --- a/apps/hworldclock/README.md +++ b/apps/hworldclock/README.md @@ -1,8 +1,17 @@ + # Hanks World Clock - See the time in four locations In addition to the main clock and date in your current location, you can add up to three other locations. Great for travel or remote working. -Additionally we show the sunset/sunrise and seconds for the current location and the day name is shown in your locale. -If watch is locked, seconds get refreshed every 10 seconds. +Additionally we show the sunset/sunrise and seconds for the current location. The day name is shown in your locale. +Also, you can swipe up to show the current time to a friend. + +All this is configurable: + + - Show seconds only when unlocked (saves battery) / always / do not show seconds + - Green color on dark mode (on/off) + - 1 Offset Small: single location shows as small (like more than 1) + - Show sun info (on/off) (set your location in the mylocation app) + - Rotation degree on swipe (off / 90 / 180 / 270) ![](hworldclock.png) @@ -10,10 +19,11 @@ If watch is locked, seconds get refreshed every 10 seconds. Location for sun set / rise set with mylocation app. -Provide names and the UTC offsets for up to three other timezones in the app store. These are stored in a json file on your watch. UTC offsets can be decimal (e.g., 5.5 for India). +Provide names and the UTC offsets for up to three other timezones in the App Loader before uploading. These are stored in a json file on your watch. UTC offsets can be decimal (e.g., 5.5 for India). -The clock does not handle summer time / daylight saving time changes automatically. If one of your three locations changes its UTC offset, you can simply change the setting in the app store and update. Currently the clock only supports 24 hour time format for the additional time zones. +The clock does not handle summer time / daylight saving time changes automatically. If one of your three locations changes its UTC offset, you can simply change the setting in the App Launcher and update. Currently the clock only supports 24 hour time format for the additional time zones. +BangleJS2: Swipe up to rotate screen (Target rotation can be set in the settings). Swipe up again to go back to the default rotation. So you can show the time to a friend real quick or temporarily change orientation for sports etc. ## Requests @@ -23,5 +33,4 @@ Please use [the Espruino Forum](http://forum.espruino.com/microcosms/1424/) if y Created by Hank. -Based on the great work of "World Clock - 4 time zones". Made by [Scott Hale](https://www.github.com/computermacgyver), based upon the [Simple Clock](https://github.com/espruino/BangleApps/tree/master/apps/sclock). -And Sun Clock [Sun Clock](https://github.com/espruino/BangleApps/tree/master/apps/sunclock) \ No newline at end of file +Based on the great work of "World Clock - 4 time zones". Made by [Scott Hale](https://www.github.com/computermacgyver) diff --git a/apps/hworldclock/app.js b/apps/hworldclock/app.js index c80b712da..79e916ea9 100644 --- a/apps/hworldclock/app.js +++ b/apps/hworldclock/app.js @@ -2,17 +2,19 @@ // ------- Settings file const SETTINGSFILE = "hworldclock.json"; -var secondsMode; -var showSunInfo; -var colorWhenDark; +let secondsMode; +let showSunInfo; +let colorWhenDark; +let rotationTarget; // ------- Settings file +//const BANGLEJS2 = process.env.HWVERSION == 2; const big = g.getWidth()>200; // Font for primary time and date const primaryTimeFontSize = big?6:5; const primaryDateFontSize = big?3:2; -require("Font5x9Numeric7Seg").add(Graphics); -require("FontTeletext10x18Ascii").add(Graphics); +let font5x9 = require("Font5x9Numeric7Seg").add(Graphics); +let font10x18 = require("FontTeletext10x18Ascii").add(Graphics); // Font for single secondary time const secondaryTimeFontSize = 4; @@ -24,9 +26,8 @@ const xcol1 = 10; const xcol2 = g.getWidth() - xcol1; const font = "6x8"; +let drag; -/* TODO: we could totally use 'Layout' here and -avoid a whole bunch of hard-coded offsets */ const xyCenter = g.getWidth() / 2; const xyCenterSeconds = xyCenter + (big ? 85 : 68); @@ -39,22 +40,27 @@ const yposWorld = big ? 170 : 120; const OFFSET_TIME_ZONE = 0; const OFFSET_HOURS = 1; -var PosInterval = 0; +let PosInterval = 0; -var offsets = require("Storage").readJSON("hworldclock.settings.json") || []; +let offsets = require("Storage").readJSON("hworldclock.settings.json") || []; //=======Sun -setting = require("Storage").readJSON("setting.json",1); +let setting = require("Storage").readJSON("setting.json",1); E.setTimeZone(setting.timezone); // timezone = 1 for MEZ, = 2 for MESZ -SunCalc = require("hsuncalc.js"); +//https://raw.githubusercontent.com/pebl-hank/BangleApps/master/modules/suncalc.js +let SunCalc = require("suncalc"); // from modules folder const LOCATION_FILE = "mylocation.json"; -var rise = "read"; -var set = "..."; +let rise = "read"; +let set = "..."; //var pos = {altitude: 20, azimuth: 135}; //var noonpos = {altitude: 37, azimuth: 180}; //=======Sun -var ampm = "AM"; +let ampm = "AM"; + +let defaultRotation = setting.rotate || 0; +let currentRotation = defaultRotation; + // TESTING CODE // Used to test offset array values during development. @@ -89,30 +95,37 @@ const mockOffsets = { // END TESTING CODE -// Load settings -function loadMySettings() { - // Helper function default setting - function def (value, def) {return value !== undefined ? value : def;} - - var settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; - secondsMode = def(settings.secondsMode, "when unlocked"); - showSunInfo = def(settings.showSunInfo, true); - colorWhenDark = def(settings.colorWhenDark, "green"); +// ================ Load settings +// Helper function default setting +let def = function(value, def) { + return value !== undefined ? value : def; } +let settings = require('Storage').readJSON(SETTINGSFILE, true) || {}; +secondsMode = def(settings.secondsMode, "when unlocked"); +showSunInfo = def(settings.showSunInfo, true); +singleOffsetSmall = def(settings.singleOffsetSmall, false); +colorWhenDark = def(settings.colorWhenDark, "green"); +rotationTarget = def(settings.rotationTarget, "90"); +rotationTarget = parseInt(rotationTarget) || 0; +if (rotationTarget == 90) rotationTarget = 1; // very lame, but works for now. +if (rotationTarget == 180) rotationTarget = 2; +if (rotationTarget == 270) rotationTarget = 3; +// ================ Load settings + // Check settings for what type our clock should be -var _12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false; +let _12hour = (require("Storage").readJSON("setting.json",1)||{})["12hour"]||false; // timeout used to update every minute -var drawTimeout; -var drawTimeoutSeconds; -var secondsTimeout; +let drawTimeout; +let drawTimeoutSeconds; +let secondsTimeout; g.setBgColor(g.theme.bg); // schedule a draw for the next minute -function queueDraw() { +let queueDraw = function() { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; @@ -121,7 +134,7 @@ function queueDraw() { } // schedule a draw for the next second -function queueDrawSeconds() { +let queueDrawSeconds = function() { if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds); drawTimeoutSeconds = setTimeout(function() { drawTimeoutSeconds = undefined; @@ -130,16 +143,16 @@ function queueDrawSeconds() { }, secondsTimeout - (Date.now() % secondsTimeout)); } -function doublenum(x) { +let doublenum = function(x) { return x < 10 ? "0" + x : "" + x; } -function getCurrentTimeFromOffset(dt, offset) { +let getCurrentTimeFromOffset = function(dt, offset) { return new Date(dt.getTime() + offset * 60 * 60 * 1000); } -function updatePos() { - coord = require("Storage").readJSON(LOCATION_FILE,1)|| {"lat":0,"lon":0,"location":"-"}; //{"lat":53.3,"lon":10.1,"location":"Pattensen"}; +let updatePos = function() { + let coord = require("Storage").readJSON(LOCATION_FILE,1)|| {"lat":0,"lon":0,"location":"-"}; //{"lat":53.3,"lon":10.1,"location":"Pattensen"}; if (coord.lat != 0 && coord.lon != 0) { //pos = SunCalc.getPosition(Date.now(), coord.lat, coord.lon); times = SunCalc.getTimes(Date.now(), coord.lat, coord.lon); @@ -152,8 +165,7 @@ function updatePos() { } } - -function drawSeconds() { +let drawSeconds = function() { // get date let d = new Date(); let da = d.toString().split(" "); @@ -175,16 +187,13 @@ function drawSeconds() { } else { g.setColor(g.theme.fg); } - //console.log("---"); - //console.log(seconds); if (Bangle.isLocked() && secondsMode != "always") seconds = seconds.slice(0, -1) + ':::'; // we use :: as the font does not have an x - //console.log(seconds); g.drawString(`${seconds}`, xyCenterSeconds, yposTime+14, true); queueDrawSeconds(); } -function draw() { +let draw = function() { // get date let d = new Date(); let da = d.toString().split(" "); @@ -209,7 +218,6 @@ function draw() { } } - //g.setFont(font, primaryTimeFontSize); g.setFont("5x9Numeric7Seg",primaryTimeFontSize); if (g.theme.dark) { if (colorWhenDark == "green") { @@ -250,7 +258,7 @@ function draw() { minutes = doublenum(dx.getMinutes()); - if (offsets.length === 1) { + if (offsets.length === 1 && !singleOffsetSmall) { let date = [require("locale").dow(new Date(), 1), require("locale").date(new Date(), 1)]; // For a single secondary timezone, draw it bigger and drop time zone to second line const xOffset = 30; @@ -291,30 +299,58 @@ function draw() { if (secondsMode != "none") queueDrawSeconds(); } -// clean app screen -g.clear(); - -// Init the settings of the app -loadMySettings(); - -// Show launcher when middle button pressed -Bangle.setUI({ - mode : "clock", - remove : function() { - // Called to unload all of the clock app - if (PosInterval) clearInterval(PosInterval); - PosInterval = undefined; - if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds); - drawTimeoutSeconds = undefined; - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = undefined; - }}); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -// draw immediately at first, queue update -draw(); + +//if (BANGLEJS2) { + let onDrag = e => { + if (!drag) { // start dragging + drag = {x: e.x, y: e.y}; + } else if (!e.b) { // released + const dx = e.x-drag.x, dy = e.y-drag.y; + drag = null; + if (Math.abs(dx)>Math.abs(dy)+10) { + // horizontal + if (dx < dy) { + // for later purpose + } else { + // for later purpose + } + } else if (Math.abs(dy)>Math.abs(dx)+10) { + // vertical + if (dx < dy) { //down + //g.clear().setRotation(defaultRotation); + //currentRotation = defaultRotation; + //draw(); + //Bangle.drawWidgets(); + } else { + if (currentRotation == rotationTarget) { + g.clear().setRotation(defaultRotation); + currentRotation = defaultRotation; + } else { + g.clear().setRotation(rotationTarget); + currentRotation = rotationTarget; + } + + draw(); + Bangle.drawWidgets(); + } + } else { + //console.log("tap " + e.x + " " + e.y); + if (e.x > 145 && e.y > 145) { + // for later purpose + } + } + } + }; //); + Bangle.on("drag", onDrag); + //} <-- BJS2 only } else { + //setWatch(xxx, BTN1, { repeat: true, debounce:50 }); // maybe adding this later + //setWatch(xxx, BTN3, { repeat: true, debounce:50 }); + //setWatch(xxx, BTN4, { repeat: true, debounce:50 }); + //setWatch(xxx, BTN5, { repeat: true, debounce:50 }); + // } + if (!Bangle.isLocked()) { // Initial state @@ -350,11 +386,10 @@ if (!Bangle.isLocked()) { // Initial state updatePos(); } draw(); // draw immediately, queue redraw - } -Bangle.on('lock',on=>{ +let onLock = on => { if (!on) { // UNlocked if (showSunInfo) { if (PosInterval != 0) clearInterval(PosInterval); @@ -390,5 +425,37 @@ Bangle.on('lock',on=>{ } draw(); // draw immediately, queue redraw } - }); -} \ No newline at end of file + }; +Bangle.on('lock', onLock); + + +// Show launcher when middle button pressed +Bangle.setUI({ + mode : "custom",clock:true, + remove : function() { + // Called to unload all of the clock app + g.setRotation(defaultRotation); // bring back default rotation + if (typeof PosInterval === "undefined") { + console.log("PosInterval is undefined"); + } else { + if (PosInterval) clearInterval(PosInterval); + } + PosInterval = undefined; + if (drawTimeoutSeconds) clearTimeout(drawTimeoutSeconds); + drawTimeoutSeconds = undefined; + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + //if (BANGLEJS2) + Bangle.removeListener("drag",onDrag); + Bangle.removeListener("onLock",onLock); + }}); + + +g.clear().setRotation(defaultRotation); // clean app screen and make sure the default rotation is set +draw(); // draw immediately at first, queue update + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +// ); +} diff --git a/apps/hworldclock/hsuncalc.js b/apps/hworldclock/hsuncalc.js deleted file mode 100644 index b1af0a0d9..000000000 --- a/apps/hworldclock/hsuncalc.js +++ /dev/null @@ -1,298 +0,0 @@ -/* Module suncalc.js - (c) 2011-2015, Vladimir Agafonkin - SunCalc is a JavaScript library for calculating sun/moon position and light phases. - https://github.com/mourner/suncalc - -PB: Usage: -E.setTimeZone(2); // 1 = MEZ, 2 = MESZ -SunCalc = require("suncalc.js"); -pos = SunCalc.getPosition(Date.now(), 53.3, 10.1); -times = SunCalc.getTimes(Date.now(), 53.3, 10.1); -rise = times.sunrise; // Date object -rise_str = rise.getHours() + ':' + rise.getMinutes(); //hh:mm -*/ -var exports={}; - -// shortcuts for easier to read formulas - -var PI = Math.PI, - sin = Math.sin, - cos = Math.cos, - tan = Math.tan, - asin = Math.asin, - atan = Math.atan2, - acos = Math.acos, - rad = PI / 180; - -// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas - -// date/time constants and conversions - -var dayMs = 1000 * 60 * 60 * 24, - J1970 = 2440588, - J2000 = 2451545; - -function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; } -function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); } // PB: onece removed + 0.5; included it again 4 Jan 2021 -function toDays(date) { return toJulian(date) - J2000; } - - -// general calculations for position - -var e = rad * 23.4397; // obliquity of the Earth - -function rightAscension(l, b) { return atan(sin(l) * cos(e) - tan(b) * sin(e), cos(l)); } -function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); } - -function azimuth(H, phi, dec) { return atan(sin(H), cos(H) * sin(phi) - tan(dec) * cos(phi)); } -function altitude(H, phi, dec) { return asin(sin(phi) * sin(dec) + cos(phi) * cos(dec) * cos(H)); } - -function siderealTime(d, lw) { return rad * (280.16 + 360.9856235 * d) - lw; } - -function astroRefraction(h) { - if (h < 0) // the following formula works for positive altitudes only. - h = 0; // if h = -0.08901179 a div/0 would occur. - - // formula 16.4 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. - // 1.02 / tan(h + 10.26 / (h + 5.10)) h in degrees, result in arc minutes -> converted to rad: - return 0.0002967 / Math.tan(h + 0.00312536 / (h + 0.08901179)); -} - -// general sun calculations - -function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); } - -function eclipticLongitude(M) { - - var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center - P = rad * 102.9372; // perihelion of the Earth - - return M + C + P + PI; -} - -function sunCoords(d) { - - var M = solarMeanAnomaly(d), - L = eclipticLongitude(M); - - return { - dec: declination(L, 0), - ra: rightAscension(L, 0) - }; -} - -// calculates sun position for a given date and latitude/longitude - -exports.getPosition = function (date, lat, lng) { - - var lw = rad * -lng, - phi = rad * lat, - d = toDays(date), - - c = sunCoords(d), - H = siderealTime(d, lw) - c.ra; - - return { - azimuth: Math.round((azimuth(H, phi, c.dec) / rad + 180) % 360), // PB: converted to deg - altitude: Math.round( altitude(H, phi, c.dec) / rad) // PB: converted to deg - }; -}; - - -// sun times configuration (angle, morning name, evening name) - -var times = [ - [-0.833, 'sunrise', 'sunset' ] -]; - -// calculations for sun times -var J0 = 0.0009; - -function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); } - -function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; } -function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); } - -function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); } -function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; } - -// returns set time for the given sun altitude -function getSetJ(h, lw, phi, dec, n, M, L) { - - var w = hourAngle(h, phi, dec), - a = approxTransit(w, lw, n); - return solarTransitJ(a, M, L); -} - - -// calculates sun times for a given date, latitude/longitude, and, optionally, -// the observer height (in meters) relative to the horizon - -exports.getTimes = function (date, lat, lng, height) { - - height = height || 0; - - var lw = rad * -lng, - phi = rad * lat, - - dh = observerAngle(height), - - d = toDays(date), - n = julianCycle(d, lw), - ds = approxTransit(0, lw, n), - - M = solarMeanAnomaly(ds), - L = eclipticLongitude(M), - dec = declination(L, 0), - - Jnoon = solarTransitJ(ds, M, L), - - i, len, time, h0, Jset, Jrise; - - - var result = { - solarNoon: fromJulian(Jnoon), - nadir: fromJulian(Jnoon - 0.5) - }; - - for (i = 0, len = times.length; i < len; i += 1) { - time = times[i]; - h0 = (time[0] + dh) * rad; - - Jset = getSetJ(h0, lw, phi, dec, n, M, L); - Jrise = Jnoon - (Jset - Jnoon); - - result[time[1]] = fromJulian(Jrise); - result[time[2]] = fromJulian(Jset); - } - - return result; -}; - - -// moon calculations, based on http://aa.quae.nl/en/reken/hemelpositie.html formulas - -function moonCoords(d) { // geocentric ecliptic coordinates of the moon - - var L = rad * (218.316 + 13.176396 * d), // ecliptic longitude - M = rad * (134.963 + 13.064993 * d), // mean anomaly - F = rad * (93.272 + 13.229350 * d), // mean distance - - l = L + rad * 6.289 * sin(M), // longitude - b = rad * 5.128 * sin(F), // latitude - dt = 385001 - 20905 * cos(M); // distance to the moon in km - - return { - ra: rightAscension(l, b), - dec: declination(l, b), - dist: dt - }; -} - -getMoonPosition = function (date, lat, lng) { - - var lw = rad * -lng, - phi = rad * lat, - d = toDays(date), - - c = moonCoords(d), - H = siderealTime(d, lw) - c.ra, - h = altitude(H, phi, c.dec), - // formula 14.1 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. - pa = atan(sin(H), tan(phi) * cos(c.dec) - sin(c.dec) * cos(H)); - - h = h + astroRefraction(h); // altitude correction for refraction - - return { - azimuth: azimuth(H, phi, c.dec), - altitude: h, - distance: c.dist, - parallacticAngle: pa - }; -}; - - -// calculations for illumination parameters of the moon, -// based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and -// Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. - -getMoonIllumination = function (date) { - - var d = toDays(date || new Date()), - s = sunCoords(d), - m = moonCoords(d), - - sdist = 149598000, // distance from Earth to Sun in km - - phi = acos(sin(s.dec) * sin(m.dec) + cos(s.dec) * cos(m.dec) * cos(s.ra - m.ra)), - inc = atan(sdist * sin(phi), m.dist - sdist * cos(phi)), - angle = atan(cos(s.dec) * sin(s.ra - m.ra), sin(s.dec) * cos(m.dec) - - cos(s.dec) * sin(m.dec) * cos(s.ra - m.ra)); - - return { - fraction: (1 + cos(inc)) / 2, - phase: 0.5 + 0.5 * inc * (angle < 0 ? -1 : 1) / Math.PI, - angle: angle - }; -}; - - -function hoursLater(date, h) { - return new Date(date.valueOf() + h * dayMs / 24); -} - -// calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article - -getMoonTimes = function (date, lat, lng, inUTC) { - var t = new Date(date); - if (inUTC) t.setUTCHours(0, 0, 0, 0); - else t.setHours(0, 0, 0, 0); - - var hc = 0.133 * rad, - h0 = SunCalc.getMoonPosition(t, lat, lng).altitude - hc, - h1, h2, rise, set, a, b, xe, ye, d, roots, x1, x2, dx; - - // go in 2-hour chunks, each time seeing if a 3-point quadratic curve crosses zero (which means rise or set) - for (var i = 1; i <= 24; i += 2) { - h1 = SunCalc.getMoonPosition(hoursLater(t, i), lat, lng).altitude - hc; - h2 = SunCalc.getMoonPosition(hoursLater(t, i + 1), lat, lng).altitude - hc; - - a = (h0 + h2) / 2 - h1; - b = (h2 - h0) / 2; - xe = -b / (2 * a); - ye = (a * xe + b) * xe + h1; - d = b * b - 4 * a * h1; - roots = 0; - - if (d >= 0) { - dx = Math.sqrt(d) / (Math.abs(a) * 2); - x1 = xe - dx; - x2 = xe + dx; - if (Math.abs(x1) <= 1) roots++; - if (Math.abs(x2) <= 1) roots++; - if (x1 < -1) x1 = x2; - } - - if (roots === 1) { - if (h0 < 0) rise = i + x1; - else set = i + x1; - - } else if (roots === 2) { - rise = i + (ye < 0 ? x2 : x1); - set = i + (ye < 0 ? x1 : x2); - } - - if (rise && set) break; - - h0 = h2; - } - - var result = {}; - - if (rise) result.rise = hoursLater(t, rise); - if (set) result.set = hoursLater(t, set); - - if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true; - - return result; -}; \ No newline at end of file diff --git a/apps/hworldclock/metadata.json b/apps/hworldclock/metadata.json index e26599373..fe5865ad0 100644 --- a/apps/hworldclock/metadata.json +++ b/apps/hworldclock/metadata.json @@ -2,7 +2,7 @@ "id": "hworldclock", "name": "Hanks World Clock", "shortName": "Hanks World Clock", - "version": "0.25", + "version": "0.32", "description": "Current time zone plus up to three others", "allow_emulator":true, "icon": "app.png", @@ -12,14 +12,14 @@ "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "custom": "custom.html", + "dependencies": {"mylocation":"app"}, "storage": [ {"name":"hworldclock.app.js","url":"app.js"}, {"name":"hworldclock.img","url":"hworldclock-icon.js","evaluate":true}, - {"name":"hworldclock.settings.js","url":"settings.js"}, - {"name":"hsuncalc.js","url":"hsuncalc.js"} + {"name":"hworldclock.settings.js","url":"settings.js"} ], "data": [ {"name":"hworldclock.settings.json"}, {"name":"hworldclock.json"} ] -} \ No newline at end of file +} diff --git a/apps/hworldclock/settings.js b/apps/hworldclock/settings.js index 26c946b5f..98b91dc7b 100644 --- a/apps/hworldclock/settings.js +++ b/apps/hworldclock/settings.js @@ -36,13 +36,21 @@ "< Back": () => back(), "Seconds": stringInSettings("secondsMode", ["always", "when unlocked", "none"]), "Color w. dark": stringInSettings("colorWhenDark", ["green", "default"]), - "Show SunInfo": { - value: (settings.showSunInfo !== undefined ? settings.showSunInfo : true), - onchange: v => { - settings.showSunInfo = v; - writeSettings(); - } + "1 Offset Small": { + value: (settings.singleOffsetSmall !== undefined ? settings.singleOffsetSmall : false), + onchange: v=> { + settings.singleOffsetSmall = v; + writeSettings(); } + }, + "Show SunInfo": { + value: (settings.showSunInfo !== undefined ? settings.showSunInfo : true), + onchange: v => { + settings.showSunInfo = v; + writeSettings(); + } + }, + "Rotation": stringInSettings("rotationTarget", ["off", "90", "180", "270"]), }; E.showMenu(mainmenu); diff --git a/apps/iconlaunch/ChangeLog b/apps/iconlaunch/ChangeLog index b03599ae6..0c33a4871 100644 --- a/apps/iconlaunch/ChangeLog +++ b/apps/iconlaunch/ChangeLog @@ -17,4 +17,6 @@ 0.12: Use Bangle.load and Bangle.showClock 0.13: Fix automatic switch to clock 0.14: Revert use of Bangle.load to classic load calls since widgets would -still be loaded when they weren't supposed to. + still be loaded when they weren't supposed to. +0.15: Ensure that we hide widgets if in fullscreen mode + (So that widgets are still hidden if launcher is fast-loaded) diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index ccc39f3bb..acf695ddb 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -12,6 +12,8 @@ if (!settings.fullscreen) { Bangle.loadWidgets(); Bangle.drawWidgets(); + } else { // for fast-load, if we had widgets then we should hide them + require("widget_utils").hide(); } let launchCache = s.readJSON("iconlaunch.cache.json", true)||{}; let launchHash = s.hash(/\.info/); @@ -190,6 +192,9 @@ btn: _=> { if (settings.oneClickExit) Bangle.showClock(); }, remove: function() { if (timeout) clearTimeout(timeout); + if (settings.fullscreen) { // for fast-load, if we hid widgets then we should show them again + require("widget_utils").show(); + } } }; diff --git a/apps/iconlaunch/metadata.json b/apps/iconlaunch/metadata.json index 155e7bd9b..27f6386d3 100644 --- a/apps/iconlaunch/metadata.json +++ b/apps/iconlaunch/metadata.json @@ -2,7 +2,7 @@ "id": "iconlaunch", "name": "Icon Launcher", "shortName" : "Icon launcher", - "version": "0.14", + "version": "0.15", "icon": "app.png", "description": "A launcher inspired by smartphones, with an icon-only scrollable menu.", "tags": "tool,system,launcher", diff --git a/apps/imageclock/app.js b/apps/imageclock/app.js index 90c6163cd..dc21d7162 100644 --- a/apps/imageclock/app.js +++ b/apps/imageclock/app.js @@ -622,12 +622,12 @@ s.pl = {}; endPerfLog("fullDraw", true); if (!Bangle.uiRemove){ - setUi(); + setUi(); // Calls Bangle.setUI() (this comment also fixes lint warning) let orig = Bangle.drawWidgets; Bangle.drawWidgets = ()=>{}; Bangle.loadWidgets(); Bangle.drawWidgets = orig; - require("widget_utils").swipeOn(); + require("widget_utils").swipeOn(0); Bangle.drawWidgets(); } }).catch((e)=>{ diff --git a/apps/ios/ChangeLog b/apps/ios/ChangeLog index 85aafb34f..8ab99b4db 100644 --- a/apps/ios/ChangeLog +++ b/apps/ios/ChangeLog @@ -9,3 +9,5 @@ 0.09: Enable 'ams' on new firmwares (ams/ancs can now be enabled individually) (fix #1365) 0.10: Added more bundleIds 0.11: Added letters with caron to unicodeRemap, to properly display messages in Czech language +0.12: Use new message library +0.13: Making ANCS message receive more resilient (#2402) diff --git a/apps/ios/app.js b/apps/ios/app.js index b210886fd..2a9e8f322 100644 --- a/apps/ios/app.js +++ b/apps/ios/app.js @@ -1,2 +1,2 @@ // Config app not implemented yet -setTimeout(()=>load("messages.app.js"),10); +setTimeout(()=>require("messages").openGUI(),10); diff --git a/apps/ios/boot.js b/apps/ios/boot.js index ee19ca14b..8952a047e 100644 --- a/apps/ios/boot.js +++ b/apps/ios/boot.js @@ -26,7 +26,7 @@ E.on('ANCS',msg=>{ // not a remove - we need to get the message info first function ancsHandler() { var msg = Bangle.ancsMessageQueue[0]; - NRF.ancsGetNotificationInfo( msg.uid ).then( info => { + NRF.ancsGetNotificationInfo( msg.uid ).then( info => { // success if(msg.preExisting === true){ info.new = false; @@ -38,6 +38,10 @@ E.on('ANCS',msg=>{ Bangle.ancsMessageQueue.shift(); if (Bangle.ancsMessageQueue.length) ancsHandler(); + }, err=>{ // failure + console.log("ancsGetNotificationInfo failed",err); + if (Bangle.ancsMessageQueue.length) + ancsHandler(); }); } Bangle.ancsMessageQueue.push(msg); @@ -196,7 +200,11 @@ Bangle.messageResponse = (msg,response) => { // error/warn here? }; // remove all messages on disconnect -NRF.on("disconnect", () => require("messages").clearAll()); +NRF.on("disconnect", () => { + require("messages").clearAll(); + // Remove any messages from the ANCS queue + Bangle.ancsMessageQueue = []; +}); /* // For testing... diff --git a/apps/ios/metadata.json b/apps/ios/metadata.json index a26d22cb1..42e0060d0 100644 --- a/apps/ios/metadata.json +++ b/apps/ios/metadata.json @@ -1,11 +1,11 @@ { "id": "ios", "name": "iOS Integration", - "version": "0.11", + "version": "0.13", "description": "Display notifications/music/etc from iOS devices", "icon": "app.png", "tags": "tool,system,ios,apple,messages,notifications", - "dependencies": {"messages":"app"}, + "dependencies": {"messages":"module"}, "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "storage": [ diff --git a/apps/kbswipe/ChangeLog b/apps/kbswipe/ChangeLog index 1804c4a89..a7b2d44c2 100644 --- a/apps/kbswipe/ChangeLog +++ b/apps/kbswipe/ChangeLog @@ -3,3 +3,5 @@ 0.03: Positioning of marker now takes the height of the widget field into account. 0.04: Fix issue if going back without typing. 0.05: Keep drag-function in ram, hopefully improving performance and input reliability somewhat. +0.06: Support input of numbers and uppercase characters. +0.07: Support input of symbols. diff --git a/apps/kbswipe/README.md b/apps/kbswipe/README.md index 3f5575777..105d7cd9b 100644 --- a/apps/kbswipe/README.md +++ b/apps/kbswipe/README.md @@ -4,6 +4,10 @@ A library that provides the ability to input text by swiping PalmOS Graffiti-sty To get a legend of available characters, just tap the screen. +To switch between the input of alphabetic, numeric and symbol characters tap the widget which displays either "123", "ABC" or "?:$". + +To switch between lowercase and uppercase characters do an up swipe. + ![](key.png) ## Usage diff --git a/apps/kbswipe/lib.js b/apps/kbswipe/lib.js index fe5f7e977..ea6d78255 100644 --- a/apps/kbswipe/lib.js +++ b/apps/kbswipe/lib.js @@ -1,47 +1,101 @@ +exports.INPUT_MODE_ALPHA = 0; +exports.INPUT_MODE_NUM = 1; +exports.INPUT_MODE_SYM = 2; + /* To make your own strokes, type: Bangle.on('stroke',print) on the left of the IDE, then do a stroke and copy out the Uint8Array line */ -exports.getStrokes = function(cb) { - cb("a", new Uint8Array([58, 159, 58, 155, 62, 144, 69, 127, 77, 106, 86, 90, 94, 77, 101, 68, 108, 62, 114, 59, 121, 59, 133, 61, 146, 70, 158, 88, 169, 107, 176, 124, 180, 135, 183, 144, 185, 152])); - cb("b", new Uint8Array([51, 47, 51, 77, 56, 123, 60, 151, 65, 163, 68, 164, 68, 144, 67, 108, 67, 76, 72, 43, 104, 51, 121, 74, 110, 87, 109, 95, 131, 117, 131, 140, 109, 152, 88, 157])); - cb("c", new Uint8Array([153, 62, 150, 62, 145, 62, 136, 62, 123, 62, 106, 65, 85, 70, 65, 75, 50, 82, 42, 93, 37, 106, 36, 119, 36, 130, 40, 140, 49, 147, 61, 153, 72, 156, 85, 157, 106, 158, 116, 158])); - cb("d", new Uint8Array([57, 178, 57, 176, 55, 171, 52, 163, 50, 154, 49, 146, 47, 135, 45, 121, 44, 108, 44, 97, 44, 85, 44, 75, 44, 66, 44, 58, 44, 48, 44, 38, 46, 31, 48, 26, 58, 21, 75, 20, 99, 26, 120, 35, 136, 51, 144, 70, 144, 88, 137, 110, 124, 131, 106, 145, 88, 153])); - cb("e", new Uint8Array([150, 72, 141, 69, 114, 68, 79, 69, 48, 77, 32, 81, 31, 85, 46, 91, 73, 95, 107, 100, 114, 103, 83, 117, 58, 134, 66, 143, 105, 148, 133, 148, 144, 148])); - cb("f", new Uint8Array([157, 52, 155, 52, 148, 52, 137, 52, 124, 52, 110, 52, 96, 52, 83, 52, 74, 52, 67, 52, 61, 52, 57, 52, 55, 52, 52, 52, 52, 54, 52, 58, 52, 64, 54, 75, 58, 97, 59, 117, 60, 130])); - cb("g", new Uint8Array([160, 66, 153, 62, 129, 58, 90, 56, 58, 57, 38, 65, 31, 86, 43, 125, 69, 152, 116, 166, 145, 154, 146, 134, 112, 116, 85, 108, 97, 106, 140, 106, 164, 106])); - cb("h", new Uint8Array([58, 50, 58, 55, 58, 64, 58, 80, 58, 102, 58, 122, 58, 139, 58, 153, 58, 164, 58, 171, 58, 177, 58, 179, 58, 181, 58, 180, 58, 173, 58, 163, 59, 154, 61, 138, 64, 114, 68, 95, 72, 84, 80, 79, 91, 79, 107, 82, 123, 93, 137, 111, 145, 130, 149, 147, 150, 154, 150, 159])); - cb("i", new Uint8Array([89, 48, 89, 49, 89, 51, 89, 55, 89, 60, 89, 68, 89, 78, 89, 91, 89, 103, 89, 114, 89, 124, 89, 132, 89, 138, 89, 144, 89, 148, 89, 151, 89, 154, 89, 156, 89, 157, 89, 158])); - cb("j", new Uint8Array([130, 57, 130, 61, 130, 73, 130, 91, 130, 113, 130, 133, 130, 147, 130, 156, 130, 161, 130, 164, 130, 166, 129, 168, 127, 168, 120, 168, 110, 168, 91, 167, 81, 167, 68, 167])); - cb("k", new Uint8Array([149, 63, 147, 68, 143, 76, 136, 89, 126, 106, 114, 123, 100, 136, 86, 147, 72, 153, 57, 155, 45, 152, 36, 145, 29, 131, 26, 117, 26, 104, 27, 93, 30, 86, 35, 80, 45, 77, 62, 80, 88, 96, 113, 116, 130, 131, 140, 142, 145, 149, 148, 153])); - cb("l", new Uint8Array([42, 55, 42, 59, 42, 69, 44, 87, 44, 107, 44, 128, 44, 143, 44, 156, 44, 163, 44, 167, 44, 169, 45, 170, 49, 170, 59, 169, 76, 167, 100, 164, 119, 162, 139, 160, 163, 159])); - cb("m", new Uint8Array([49, 165, 48, 162, 46, 156, 44, 148, 42, 138, 42, 126, 42, 113, 43, 101, 45, 91, 47, 82, 49, 75, 51, 71, 54, 70, 57, 70, 61, 74, 69, 81, 75, 91, 84, 104, 94, 121, 101, 132, 103, 137, 106, 130, 110, 114, 116, 92, 125, 75, 134, 65, 139, 62, 144, 66, 148, 83, 151, 108, 155, 132, 157, 149])); - cb("n", new Uint8Array([50, 165, 50, 160, 50, 153, 50, 140, 50, 122, 50, 103, 50, 83, 50, 65, 50, 52, 50, 45, 50, 43, 52, 52, 57, 67, 66, 90, 78, 112, 93, 131, 104, 143, 116, 152, 127, 159, 135, 160, 141, 150, 148, 125, 154, 96, 158, 71, 161, 56, 162, 49])); - cb("o", new Uint8Array([107, 58, 104, 58, 97, 61, 87, 68, 75, 77, 65, 88, 58, 103, 54, 116, 53, 126, 55, 135, 61, 143, 75, 149, 91, 150, 106, 148, 119, 141, 137, 125, 143, 115, 146, 104, 146, 89, 142, 78, 130, 70, 116, 65, 104, 62])); - cb("p", new Uint8Array([52, 59, 52, 64, 54, 73, 58, 88, 61, 104, 65, 119, 67, 130, 69, 138, 71, 145, 71, 147, 71, 148, 71, 143, 70, 133, 68, 120, 67, 108, 67, 97, 67, 89, 68, 79, 72, 67, 83, 60, 99, 58, 118, 58, 136, 63, 146, 70, 148, 77, 145, 84, 136, 91, 121, 95, 106, 97, 93, 97, 82, 97])); - cb("q", new Uint8Array([95, 59, 93, 59, 88, 59, 79, 59, 68, 61, 57, 67, 50, 77, 48, 89, 48, 103, 50, 117, 55, 130, 65, 140, 76, 145, 85, 146, 94, 144, 101, 140, 105, 136, 106, 127, 106, 113, 100, 98, 92, 86, 86, 79, 84, 75, 84, 72, 91, 69, 106, 67, 126, 67, 144, 67, 158, 67, 168, 67, 173, 67, 177, 67])); - cb("r", new Uint8Array([53, 49, 53, 62, 53, 91, 53, 127, 53, 146, 53, 147, 53, 128, 53, 94, 53, 69, 62, 44, 82, 42, 94, 50, 92, 68, 82, 85, 77, 93, 80, 102, 95, 119, 114, 134, 129, 145, 137, 150])); - cb("s", new Uint8Array([159, 72, 157, 70, 155, 68, 151, 66, 145, 63, 134, 60, 121, 58, 108, 56, 96, 55, 83, 55, 73, 55, 64, 56, 57, 60, 52, 65, 49, 71, 49, 76, 50, 81, 55, 87, 71, 94, 94, 100, 116, 104, 131, 108, 141, 114, 145, 124, 142, 135, 124, 146, 97, 153, 70, 157, 52, 158])); - cb("t", new Uint8Array([45, 55, 48, 55, 55, 55, 72, 55, 96, 55, 120, 55, 136, 55, 147, 55, 152, 55, 155, 55, 157, 55, 158, 56, 158, 60, 156, 70, 154, 86, 151, 102, 150, 114, 148, 125, 148, 138, 148, 146])); - cb("u", new Uint8Array([35, 52, 35, 59, 35, 73, 35, 90, 36, 114, 38, 133, 42, 146, 49, 153, 60, 157, 73, 158, 86, 156, 100, 152, 112, 144, 121, 131, 127, 114, 132, 97, 134, 85, 135, 73, 136, 61, 136, 56])); - cb("v", new Uint8Array([36, 55, 37, 59, 40, 68, 45, 83, 51, 100, 58, 118, 64, 132, 69, 142, 71, 149, 73, 156, 76, 158, 77, 160, 77, 159, 80, 151, 82, 137, 84, 122, 86, 111, 90, 91, 91, 78, 91, 68, 91, 63, 92, 61, 97, 61, 111, 61, 132, 61, 150, 61, 162, 61])); - cb("w", new Uint8Array([33, 58, 34, 81, 39, 127, 44, 151, 48, 161, 52, 162, 57, 154, 61, 136, 65, 115, 70, 95, 76, 95, 93, 121, 110, 146, 119, 151, 130, 129, 138, 84, 140, 56, 140, 45])); - cb("x", new Uint8Array([56, 63, 56, 67, 57, 74, 60, 89, 66, 109, 74, 129, 85, 145, 96, 158, 107, 164, 117, 167, 128, 164, 141, 155, 151, 140, 159, 122, 166, 105, 168, 89, 170, 81, 170, 73, 169, 66, 161, 63, 141, 68, 110, 83, 77, 110, 55, 134, 47, 145])); - cb("y", new Uint8Array([42, 56, 42, 70, 48, 97, 62, 109, 85, 106, 109, 90, 126, 65, 134, 47, 137, 45, 137, 75, 127, 125, 98, 141, 70, 133, 65, 126, 92, 137, 132, 156, 149, 166])); - cb("z", new Uint8Array([29, 62, 35, 62, 43, 62, 63, 62, 87, 62, 110, 62, 125, 62, 134, 62, 138, 62, 136, 63, 122, 68, 103, 77, 85, 91, 70, 107, 59, 120, 50, 132, 47, 138, 43, 143, 41, 148, 42, 151, 53, 155, 80, 157, 116, 158, 146, 158, 163, 158])); +exports.getStrokes = function(mode, cb) { + if (mode === exports.INPUT_MODE_ALPHA) { + cb("a", new Uint8Array([58, 159, 58, 155, 62, 144, 69, 127, 77, 106, 86, 90, 94, 77, 101, 68, 108, 62, 114, 59, 121, 59, 133, 61, 146, 70, 158, 88, 169, 107, 176, 124, 180, 135, 183, 144, 185, 152])); + cb("b", new Uint8Array([51, 47, 51, 77, 56, 123, 60, 151, 65, 163, 68, 164, 68, 144, 67, 108, 67, 76, 72, 43, 104, 51, 121, 74, 110, 87, 109, 95, 131, 117, 131, 140, 109, 152, 88, 157])); + cb("c", new Uint8Array([153, 62, 150, 62, 145, 62, 136, 62, 123, 62, 106, 65, 85, 70, 65, 75, 50, 82, 42, 93, 37, 106, 36, 119, 36, 130, 40, 140, 49, 147, 61, 153, 72, 156, 85, 157, 106, 158, 116, 158])); + cb("d", new Uint8Array([57, 178, 57, 176, 55, 171, 52, 163, 50, 154, 49, 146, 47, 135, 45, 121, 44, 108, 44, 97, 44, 85, 44, 75, 44, 66, 44, 58, 44, 48, 44, 38, 46, 31, 48, 26, 58, 21, 75, 20, 99, 26, 120, 35, 136, 51, 144, 70, 144, 88, 137, 110, 124, 131, 106, 145, 88, 153])); + cb("e", new Uint8Array([150, 72, 141, 69, 114, 68, 79, 69, 48, 77, 32, 81, 31, 85, 46, 91, 73, 95, 107, 100, 114, 103, 83, 117, 58, 134, 66, 143, 105, 148, 133, 148, 144, 148])); + cb("f", new Uint8Array([157, 52, 155, 52, 148, 52, 137, 52, 124, 52, 110, 52, 96, 52, 83, 52, 74, 52, 67, 52, 61, 52, 57, 52, 55, 52, 52, 52, 52, 54, 52, 58, 52, 64, 54, 75, 58, 97, 59, 117, 60, 130])); + cb("g", new Uint8Array([160, 66, 153, 62, 129, 58, 90, 56, 58, 57, 38, 65, 31, 86, 43, 125, 69, 152, 116, 166, 145, 154, 146, 134, 112, 116, 85, 108, 97, 106, 140, 106, 164, 106])); + cb("h", new Uint8Array([58, 50, 58, 55, 58, 64, 58, 80, 58, 102, 58, 122, 58, 139, 58, 153, 58, 164, 58, 171, 58, 177, 58, 179, 58, 181, 58, 180, 58, 173, 58, 163, 59, 154, 61, 138, 64, 114, 68, 95, 72, 84, 80, 79, 91, 79, 107, 82, 123, 93, 137, 111, 145, 130, 149, 147, 150, 154, 150, 159])); + cb("i", new Uint8Array([89, 48, 89, 49, 89, 51, 89, 55, 89, 60, 89, 68, 89, 78, 89, 91, 89, 103, 89, 114, 89, 124, 89, 132, 89, 138, 89, 144, 89, 148, 89, 151, 89, 154, 89, 156, 89, 157, 89, 158])); + cb("j", new Uint8Array([130, 57, 130, 61, 130, 73, 130, 91, 130, 113, 130, 133, 130, 147, 130, 156, 130, 161, 130, 164, 130, 166, 129, 168, 127, 168, 120, 168, 110, 168, 91, 167, 81, 167, 68, 167])); + cb("k", new Uint8Array([149, 63, 147, 68, 143, 76, 136, 89, 126, 106, 114, 123, 100, 136, 86, 147, 72, 153, 57, 155, 45, 152, 36, 145, 29, 131, 26, 117, 26, 104, 27, 93, 30, 86, 35, 80, 45, 77, 62, 80, 88, 96, 113, 116, 130, 131, 140, 142, 145, 149, 148, 153])); + cb("l", new Uint8Array([42, 55, 42, 59, 42, 69, 44, 87, 44, 107, 44, 128, 44, 143, 44, 156, 44, 163, 44, 167, 44, 169, 45, 170, 49, 170, 59, 169, 76, 167, 100, 164, 119, 162, 139, 160, 163, 159])); + cb("m", new Uint8Array([49, 165, 48, 162, 46, 156, 44, 148, 42, 138, 42, 126, 42, 113, 43, 101, 45, 91, 47, 82, 49, 75, 51, 71, 54, 70, 57, 70, 61, 74, 69, 81, 75, 91, 84, 104, 94, 121, 101, 132, 103, 137, 106, 130, 110, 114, 116, 92, 125, 75, 134, 65, 139, 62, 144, 66, 148, 83, 151, 108, 155, 132, 157, 149])); + cb("n", new Uint8Array([50, 165, 50, 160, 50, 153, 50, 140, 50, 122, 50, 103, 50, 83, 50, 65, 50, 52, 50, 45, 50, 43, 52, 52, 57, 67, 66, 90, 78, 112, 93, 131, 104, 143, 116, 152, 127, 159, 135, 160, 141, 150, 148, 125, 154, 96, 158, 71, 161, 56, 162, 49])); + cb("o", new Uint8Array([107, 58, 104, 58, 97, 61, 87, 68, 75, 77, 65, 88, 58, 103, 54, 116, 53, 126, 55, 135, 61, 143, 75, 149, 91, 150, 106, 148, 119, 141, 137, 125, 143, 115, 146, 104, 146, 89, 142, 78, 130, 70, 116, 65, 104, 62])); + cb("p", new Uint8Array([29, 47, 29, 55, 29, 75, 29, 110, 29, 145, 29, 165, 29, 172, 29, 164, 30, 149, 37, 120, 50, 91, 61, 74, 72, 65, 85, 61, 103, 61, 118, 63, 126, 69, 129, 76, 130, 87, 126, 98, 112, 108, 97, 114, 87, 116])); + cb("q", new Uint8Array([95, 59, 93, 59, 88, 59, 79, 59, 68, 61, 57, 67, 50, 77, 48, 89, 48, 103, 50, 117, 55, 130, 65, 140, 76, 145, 85, 146, 94, 144, 101, 140, 105, 136, 106, 127, 106, 113, 100, 98, 92, 86, 86, 79, 84, 75, 84, 72, 91, 69, 106, 67, 126, 67, 144, 67, 158, 67, 168, 67, 173, 67, 177, 67])); + cb("r", new Uint8Array([53, 49, 53, 62, 53, 91, 53, 127, 53, 146, 53, 147, 53, 128, 53, 94, 53, 69, 62, 44, 82, 42, 94, 50, 92, 68, 82, 85, 77, 93, 80, 102, 95, 119, 114, 134, 129, 145, 137, 150])); + cb("s", new Uint8Array([159, 72, 157, 70, 155, 68, 151, 66, 145, 63, 134, 60, 121, 58, 108, 56, 96, 55, 83, 55, 73, 55, 64, 56, 57, 60, 52, 65, 49, 71, 49, 76, 50, 81, 55, 87, 71, 94, 94, 100, 116, 104, 131, 108, 141, 114, 145, 124, 142, 135, 124, 146, 97, 153, 70, 157, 52, 158])); + cb("t", new Uint8Array([45, 55, 48, 55, 55, 55, 72, 55, 96, 55, 120, 55, 136, 55, 147, 55, 152, 55, 155, 55, 157, 55, 158, 56, 158, 60, 156, 70, 154, 86, 151, 102, 150, 114, 148, 125, 148, 138, 148, 146])); + cb("u", new Uint8Array([35, 52, 35, 59, 35, 73, 35, 90, 36, 114, 38, 133, 42, 146, 49, 153, 60, 157, 73, 158, 86, 156, 100, 152, 112, 144, 121, 131, 127, 114, 132, 97, 134, 85, 135, 73, 136, 61, 136, 56])); + cb("v", new Uint8Array([36, 55, 37, 59, 40, 68, 45, 83, 51, 100, 58, 118, 64, 132, 69, 142, 71, 149, 73, 156, 76, 158, 77, 160, 77, 159, 80, 151, 82, 137, 84, 122, 86, 111, 90, 91, 91, 78, 91, 68, 91, 63, 92, 61, 97, 61, 111, 61, 132, 61, 150, 61, 162, 61])); + cb("w", new Uint8Array([25, 46, 25, 82, 25, 119, 33, 143, 43, 153, 60, 147, 73, 118, 75, 91, 76, 88, 85, 109, 96, 134, 107, 143, 118, 137, 129, 112, 134, 81, 134, 64, 134, 55])); + cb("x", new Uint8Array([56, 63, 56, 67, 57, 74, 60, 89, 66, 109, 74, 129, 85, 145, 96, 158, 107, 164, 117, 167, 128, 164, 141, 155, 151, 140, 159, 122, 166, 105, 168, 89, 170, 81, 170, 73, 169, 66, 161, 63, 141, 68, 110, 83, 77, 110, 55, 134, 47, 145])); + cb("y", new Uint8Array([30, 41, 30, 46, 30, 52, 30, 63, 30, 79, 33, 92, 38, 100, 47, 104, 54, 107, 66, 105, 79, 94, 88, 82, 92, 74, 94, 77, 96, 98, 96, 131, 94, 151, 91, 164, 85, 171, 75, 171, 71, 162, 74, 146, 84, 130, 95, 119, 106, 113])); + cb("z", new Uint8Array([29, 62, 35, 62, 43, 62, 63, 62, 87, 62, 110, 62, 125, 62, 134, 62, 138, 62, 136, 63, 122, 68, 103, 77, 85, 91, 70, 107, 59, 120, 50, 132, 47, 138, 43, 143, 41, 148, 42, 151, 53, 155, 80, 157, 116, 158, 146, 158, 163, 158])); + cb("SHIFT", new Uint8Array([100, 160, 100, 50])); + } else if (mode === exports.INPUT_MODE_NUM) { + cb("0", new Uint8Array([82, 50, 76, 50, 67, 50, 59, 50, 50, 51, 43, 57, 38, 68, 34, 83, 33, 95, 33, 108, 34, 121, 42, 136, 57, 148, 72, 155, 85, 157, 98, 155, 110, 149, 120, 139, 128, 127, 134, 119, 137, 114, 138, 107, 138, 98, 138, 88, 138, 77, 137, 71, 134, 65, 128, 60, 123, 58])); + cb("1", new Uint8Array([100, 50, 100, 160])); + cb("2", new Uint8Array([40, 79, 46, 74, 56, 66, 68, 58, 77, 49, 87, 45, 100, 45, 111, 46, 119, 50, 128, 58, 133, 71, 130, 88, 120, 106, 98, 128, 69, 150, 50, 162, 42, 167, 43, 168, 58, 169, 78, 170, 93, 170, 103, 170, 109, 170])); + cb("3", new Uint8Array([47, 65, 51, 60, 57, 56, 65, 51, 74, 47, 84, 45, 93, 45, 102, 45, 109, 46, 122, 51, 129, 58, 130, 65, 127, 74, 120, 85, 112, 92, 107, 96, 112, 101, 117, 105, 125, 113, 128, 123, 127, 134, 122, 145, 108, 156, 91, 161, 70, 163, 55, 163])); + cb("4", new Uint8Array([37, 58, 37, 60, 37, 64, 37, 69, 37, 75, 37, 86, 37, 96, 37, 105, 37, 112, 37, 117, 37, 122, 37, 126, 37, 128, 38, 129, 40, 129, 45, 129, 48, 129, 53, 129, 67, 129, 85, 129, 104, 129, 119, 129, 129, 129, 136, 129])); + cb("5", new Uint8Array([142, 60, 119, 60, 79, 60, 45, 60, 37, 64, 37, 86, 37, 103, 47, 107, 66, 106, 81, 103, 97, 103, 116, 103, 129, 108, 131, 130, 122, 152, 101, 168, 85, 172, 70, 172, 59, 172])); + cb("6", new Uint8Array([136, 54, 135, 49, 129, 47, 114, 47, 89, 54, 66, 66, 50, 81, 39, 95, 35, 109, 34, 128, 38, 145, 52, 158, 81, 164, 114, 157, 133, 139, 136, 125, 132, 118, 120, 115, 102, 117, 85, 123])); + cb("7", new Uint8Array([47, 38, 48, 38, 53, 38, 66, 38, 85, 38, 103, 38, 117, 38, 125, 38, 129, 38, 134, 41, 135, 47, 135, 54, 135, 66, 131, 93, 124, 126, 116, 149, 109, 161, 105, 168])); + cb("8", new Uint8Array([122, 61, 102, 61, 83, 61, 60, 61, 47, 62, 45, 78, 58, 99, 84, 112, 105, 122, 118, 134, 121, 149, 113, 165, 86, 171, 59, 171, 47, 164, 45, 144, 50, 132, 57, 125, 67, 117, 78, 109, 87, 102, 96, 94, 105, 86, 113, 85])); + cb("9", new Uint8Array([122, 58, 117, 55, 112, 51, 104, 51, 95, 51, 86, 51, 77, 51, 68, 51, 60, 51, 54, 56, 47, 64, 46, 77, 46, 89, 46, 96, 51, 103, 64, 109, 74, 110, 83, 110, 94, 107, 106, 102, 116, 94, 124, 84, 127, 79, 128, 78, 128, 94, 128, 123, 128, 161, 128, 175])); + } else if (mode === exports.INPUT_MODE_SYM) { + cb("?", new Uint8Array([36, 69, 39, 68, 44, 65, 52, 60, 61, 56, 70, 51, 78, 47, 87, 46, 96, 46, 108, 46, 121, 49, 128, 56, 129, 63, 126, 76, 119, 91, 108, 105, 103, 114, 98, 118, 93, 124, 91, 131, 91, 143, 91, 155, 91, 163])); + cb(".", new Uint8Array([105, 158, 97, 157, 80, 150, 60, 140, 44, 127, 34, 110, 31, 97, 31, 84, 35, 74, 48, 59, 78, 55, 115, 57, 145, 70, 159, 89, 162, 112, 160, 138, 153, 153, 144, 164, 125, 170, 103, 171])); + cb(",", new Uint8Array([140, 44, 139, 45, 138, 46, 137, 47, 135, 49, 132, 51, 127, 55, 123, 58, 117, 62, 112, 67, 105, 71, 100, 77, 93, 82, 86, 86, 80, 91, 74, 96, 69, 101, 64, 105, 60, 108, 57, 112, 53, 115, 51, 117, 49, 119, 48, 121, 47, 122, 46, 122, 46, 123])); + cb("'", new Uint8Array([100, 50, 100, 160])); + cb("-", new Uint8Array([34, 63, 36, 63, 40, 63, 46, 63, 54, 63, 63, 63, 72, 63, 82, 63, 92, 63, 103, 63, 113, 63, 124, 63, 132, 63, 139, 63, 143, 63, 145, 63, 147, 63, 149, 63, 152, 63])); + cb("_", new Uint8Array([34, 84, 36, 84, 40, 84, 47, 84, 56, 84, 67, 84, 81, 84, 95, 84, 108, 84, 120, 84, 131, 84, 139, 84, 146, 84, 149, 84, 151, 84, 154, 84, 155, 83, 154, 81, 150, 78, 143, 74, 130, 71, 111, 68, 90, 65, 73, 64, 60, 64, 51, 64, 46, 64])); + cb("\"", new Uint8Array([24, 168, 24, 158, 28, 132, 33, 102, 37, 82, 41, 66, 46, 54, 50, 47, 54, 46, 60, 49, 67, 64, 73, 88, 80, 114, 87, 138, 95, 149, 109, 145, 123, 128, 130, 108, 135, 87, 136, 70, 136, 57, 136, 50])); + cb(":", new Uint8Array([24, 62, 24, 63, 24, 68, 26, 73, 27, 80, 29, 88, 31, 94, 33, 101, 35, 108, 37, 114, 39, 121, 39, 127, 39, 131, 39, 134, 39, 135, 39, 133, 39, 130, 41, 125, 45, 114, 48, 100, 51, 89, 52, 81, 52, 74, 52, 70, 52, 67, 52, 63, 52, 60, 52, 57])); + cb(";", new Uint8Array([142, 58, 139, 59, 136, 61, 131, 65, 124, 71, 116, 79, 105, 87, 94, 98, 82, 109, 70, 121, 58, 132, 49, 141, 40, 149, 33, 156, 28, 160, 24, 164, 23, 166, 22, 164, 25, 156, 33, 138, 47, 111, 66, 81, 82, 58, 95, 41, 103, 30])); + cb("(", new Uint8Array([72, 51, 70, 51, 68, 51, 66, 54, 63, 56, 61, 59, 58, 61, 56, 65, 54, 70, 51, 74, 49, 79, 47, 83, 45, 87, 44, 92, 44, 94, 44, 96, 44, 99, 44, 101, 44, 104, 44, 107, 44, 114, 44, 120, 46, 127, 49, 135, 52, 141, 56, 145])); + cb(")", new Uint8Array([18, 42, 21, 43, 24, 45, 28, 47, 32, 50, 37, 53, 40, 58, 44, 62, 46, 69, 48, 76, 50, 81, 52, 85, 53, 90, 53, 94, 53, 98, 53, 103, 53, 106, 53, 111, 53, 119, 53, 129, 52, 137, 50, 142, 47, 146])); + cb("[", new Uint8Array([121, 138, 118, 143, 114, 146, 110, 149, 105, 152, 98, 152, 91, 152, 83, 152, 77, 152, 67, 151, 59, 146, 52, 138, 47, 131, 47, 124, 48, 118, 57, 115, 64, 115, 67, 113, 64, 106, 59, 95, 53, 85, 48, 80, 47, 74, 47, 64, 53, 57, 65, 56, 83, 56, 99, 61])); + cb("]", new Uint8Array([36, 136, 42, 140, 54, 145, 70, 149, 84, 151, 98, 149, 109, 143, 113, 135, 113, 127, 104, 115, 87, 105, 75, 103, 76, 98, 87, 84, 96, 67, 100, 54, 97, 48, 90, 45, 76, 45, 60, 47, 44, 52])); + cb("<", new Uint8Array([154, 122, 151, 122, 149, 121, 147, 118, 144, 116, 139, 114, 133, 112, 126, 110, 118, 107, 108, 105, 97, 102, 86, 97, 75, 93, 64, 90, 56, 88, 49, 85, 46, 84, 41, 82, 40, 80, 47, 76, 63, 69, 86, 59, 106, 50, 121, 44, 128, 40])); + cb(">", new Uint8Array([28, 115, 31, 115, 38, 113, 48, 110, 57, 107, 68, 103, 79, 98, 90, 94, 98, 92, 104, 90, 111, 88, 117, 85, 122, 83, 125, 81, 127, 80, 129, 80, 132, 80, 130, 78, 126, 75, 120, 72, 110, 69, 98, 66, 85, 63, 72, 60, 59, 57, 45, 53, 36, 49, 30, 46])); + cb("@", new Uint8Array([82, 50, 76, 50, 67, 50, 59, 50, 50, 51, 43, 57, 38, 68, 34, 83, 33, 95, 33, 108, 34, 121, 42, 136, 57, 148, 72, 155, 85, 157, 98, 155, 110, 149, 120, 139, 128, 127, 134, 119, 137, 114, 138, 107, 138, 98, 138, 88, 138, 77, 137, 71, 134, 65, 128, 60, 123, 58])); + cb("#", new Uint8Array([23, 70, 23, 76, 26, 85, 30, 97, 36, 112, 40, 129, 45, 142, 49, 152, 53, 158, 59, 161, 67, 155, 78, 130, 84, 98, 88, 76, 90, 68, 96, 62, 102, 61, 108, 61, 119, 67, 126, 80, 131, 101, 135, 129, 136, 152])); + cb("$", new Uint8Array([159, 72, 157, 70, 155, 68, 151, 66, 145, 63, 134, 60, 121, 58, 108, 56, 96, 55, 83, 55, 73, 55, 64, 56, 57, 60, 52, 65, 49, 71, 49, 76, 50, 81, 55, 87, 71, 94, 94, 100, 116, 104, 131, 108, 141, 114, 145, 124, 142, 135, 124, 146, 97, 153, 70, 157, 52, 158])); + cb("%", new Uint8Array([31, 39, 39, 54, 51, 78, 60, 97, 62, 107, 59, 118, 47, 118, 44, 109, 46, 92, 56, 73, 69, 62, 92, 61, 115, 70, 125, 90, 126, 110, 125, 122, 118, 127, 111, 127, 105, 124, 105, 115, 105, 97, 109, 75, 117, 56, 124, 45])); + cb("^", new Uint8Array([28, 175, 28, 168, 33, 156, 37, 142, 41, 128, 46, 111, 51, 95, 58, 82, 62, 75, 68, 68, 74, 57, 81, 49, 88, 44, 93, 44, 102, 56, 113, 79, 118, 95, 123, 110, 131, 130, 135, 146, 136, 158])); + cb("&", new Uint8Array([122, 61, 102, 61, 83, 61, 60, 61, 47, 62, 45, 78, 58, 99, 84, 112, 105, 122, 118, 134, 121, 149, 113, 165, 86, 171, 59, 171, 47, 164, 45, 144, 50, 132, 57, 125, 67, 117, 78, 109, 87, 102, 96, 94, 105, 86, 113, 85])); + cb("*", new Uint8Array([35, 61, 41, 62, 53, 68, 72, 78, 91, 91, 103, 99, 113, 103, 119, 106, 124, 107, 131, 107, 139, 107, 150, 107, 161, 104, 166, 97, 166, 89, 165, 78, 162, 70, 158, 61, 151, 54, 144, 51, 132, 51, 115, 57, 98, 66, 82, 78, 65, 89, 52, 100, 44, 109])); + cb("!", new Uint8Array([100, 160, 100, 50])); + cb("~", new Uint8Array([133, 40, 133, 48, 133, 65, 133, 87, 133, 105, 132, 116, 128, 125, 124, 133, 120, 140, 114, 146, 107, 148, 101, 147, 91, 139, 82, 126, 74, 108, 70, 91, 70, 82, 70, 75, 70, 65, 68, 57, 62, 51, 57, 50, 49, 57, 41, 76, 36, 96, 33, 114, 33, 132])); + cb("+", new Uint8Array([151, 41, 146, 46, 133, 55, 116, 71, 101, 87, 87, 98, 74, 105, 63, 109, 54, 110, 43, 106, 36, 94, 36, 80, 36, 68, 42, 60, 60, 58, 91, 64, 115, 77, 129, 88, 139, 99, 144, 106])); + cb("=", new Uint8Array([34, 46, 47, 46, 70, 46, 87, 46, 96, 46, 101, 46, 104, 46, 102, 50, 96, 58, 80, 78, 62, 100, 49, 117, 40, 127, 43, 132, 61, 132, 84, 132, 99, 132])); + cb("\\", new Uint8Array([25, 38, 26, 40, 30, 43, 35, 48, 43, 54, 54, 63, 65, 74, 76, 85, 87, 96, 98, 108, 108, 121, 116, 131, 123, 138, 127, 144, 131, 148, 134, 152, 136, 155])); + cb("|", new Uint8Array([66, 146, 66, 144, 66, 140, 66, 134, 66, 125, 66, 114, 66, 102, 66, 92, 66, 83, 66, 77, 66, 71, 66, 67, 66, 62, 66, 58, 66, 53, 66, 49, 66, 48, 66, 46, 64, 42, 61, 41, 58, 42, 54, 47, 51, 55, 46, 67, 40, 81, 37, 93, 34, 102, 30, 109, 28, 116])); + cb("/", new Uint8Array([24, 173, 26, 171, 30, 166, 36, 158, 44, 148, 53, 137, 63, 126, 73, 115, 82, 104, 91, 95, 99, 87, 105, 80, 112, 74, 117, 70, 122, 65, 125, 61, 127, 60, 129, 57, 133, 53, 136, 50, 137, 47])); + } cb("\b", new Uint8Array([183, 103, 182, 103, 180, 103, 176, 103, 169, 103, 159, 103, 147, 103, 133, 103, 116, 103, 101, 103, 85, 103, 73, 103, 61, 103, 52, 103, 38, 103, 34, 103, 29, 103, 27, 103, 26, 103, 25, 103, 24, 103])); - cb(" ", new Uint8Array([39, 118, 40, 118, 41, 118, 44, 118, 47, 118, 52, 118, 58, 118, 66, 118, 74, 118, 84, 118, 94, 118, 104, 117, 114, 116, 123, 116, 130, 116, 144, 116, 149, 116, 154, 116, 158, 116, 161, 116, 163, 116])); + if (mode === exports.INPUT_MODE_ALPHA || mode === exports.INPUT_MODE_NUM) { + cb(" ", new Uint8Array([39, 118, 40, 118, 41, 118, 44, 118, 47, 118, 52, 118, 58, 118, 66, 118, 74, 118, 84, 118, 94, 118, 104, 117, 114, 116, 123, 116, 130, 116, 144, 116, 149, 116, 154, 116, 158, 116, 161, 116, 163, 116])); + } }; exports.input = function(options) { options = options||{}; + let input_mode = exports.INPUT_MODE_ALPHA; var text = options.text; if ("string"!=typeof text) text=""; -Bangle.strokes = {}; -exports.getStrokes( (id,s) => Bangle.strokes[id] = Unistroke.new(s) ); + function setupStrokes() { + Bangle.strokes = {}; + exports.getStrokes(input_mode, (id,s) => Bangle.strokes[id] = Unistroke.new(s) ); + } + setupStrokes(); var flashToggle = false; const R = Bangle.appRect; @@ -49,6 +103,9 @@ exports.getStrokes( (id,s) => Bangle.strokes[id] = Unistroke.new(s) ); var Rx2; var Ry1; var Ry2; + let flashInterval; + let shift = false; + let lastDrag; function findMarker(strArr) { if (strArr.length == 0) { @@ -101,52 +158,117 @@ exports.getStrokes( (id,s) => Bangle.strokes[id] = Unistroke.new(s) ); */ function show() { + if (flashInterval) clearInterval(flashInterval); + flashInterval = undefined; + g.reset(); - g.clearRect(R).setColor("#f00"); - var n=0; - exports.getStrokes((id,s) => { - var x = n%6; - var y = (n-x)/6; + g.setFont("6x8"); + g.clearRect(R); + let n=0; + exports.getStrokes(input_mode, (id,s) => { + let x = n%6; + let y = (n-x)/6; s = g.transformVertices(s, {scale:0.16, x:R.x+x*30-4, y:R.y+y*30-4}); g.fillRect(s[0]-1,s[1]-2,s[0]+1,s[1]+1); - g.drawPoly(s); + g.setColor("#f00").drawPoly(s); + switch(id) { + case 'SHIFT': + g.setBgColor(0).setColor("#00f").drawImage(atob("CgqBAfP4fh8D4fh+H4fh+HA="), R.x+x*30+20, R.y+y*30+20); + break; + case '\b': + case '\n': + case ' ': + break; + default: + g.setColor("#00f").drawString(shift ? id.toUpperCase() : id, R.x+x*30+20, R.y+y*30+20); + } n++; }); } + function isInside(rect, e) { + return e.x>=rect.x && e.x=rect.y && e.y<=rect.y+rect.h; + } + + function isStrokeInside(rect, stroke) { + for(let i=0; i < stroke.length; i+=2) { + if (!isInside(rect, {x: stroke[i], y: stroke[i+1]})) { + return false; + } + } + return true; + } + function strokeHandler(o) { //print(o); if (!flashInterval) flashInterval = setInterval(() => { flashToggle = !flashToggle; - draw(); + draw(false); }, 1000); - if (o.stroke!==undefined) { + if (o.stroke!==undefined && o.xy.length >= 6 && isStrokeInside(R, o.xy)) { var ch = o.stroke; if (ch=="\b") text = text.slice(0,-1); - else text += ch; - g.clearRect(R); + else if (ch==="SHIFT") { shift=!shift; Bangle.drawWidgets(); } + else text += shift ? ch.toUpperCase() : ch; } + lastDrag = undefined; + g.clearRect(R); flashToggle = true; - draw(); + draw(false); } + + // Switches between alphabetic and numeric input + function cycleInput() { + input_mode++; + if (input_mode > exports.INPUT_MODE_SYM) input_mode = 0; + shift = false; + setupStrokes(); + show(); + Bangle.drawWidgets(); + } + Bangle.on('stroke',strokeHandler); g.reset().clearRect(R); show(); draw(false); - var flashInterval; + + // Create Widget to switch between alphabetic and numeric input + WIDGETS.kbswipe={ + area:"tl", + width: 36, // 3 chars, 6*2 px/char + draw: function() { + g.reset(); + g.setFont("6x8:2x3"); + g.setColor("#f00"); + if (input_mode === exports.INPUT_MODE_ALPHA) { + g.drawString(shift ? "ABC" : "abc", this.x, this.y); + } else if (input_mode === exports.INPUT_MODE_NUM) { + g.drawString("123", this.x, this.y); + } else if (input_mode === exports.INPUT_MODE_SYM) { + g.drawString("?:$", this.x, this.y); + } + } + }; return new Promise((resolve,reject) => { - var l;//last event Bangle.setUI({mode:"custom", drag:e=>{ "ram"; - if (l) g.reset().setColor("#f00").drawLine(l.x,l.y,e.x,e.y); - l = e.b ? e : 0; - },touch:() => { - if (flashInterval) clearInterval(flashInterval); - flashInterval = undefined; - show(); + if (isInside(R, e)) { + if (lastDrag) g.reset().setColor("#f00").drawLine(lastDrag.x,lastDrag.y,e.x,e.y); + lastDrag = e.b ? e : 0; + } + },touch:(n,e) => { + if (WIDGETS.kbswipe && isInside({x: WIDGETS.kbswipe.x, y: WIDGETS.kbswipe.y, w: WIDGETS.kbswipe.width, h: 24}, e)) { + // touch inside widget + cycleInput(); + } else if (isInside(R, e)) { + // touch inside app area + show(); + } }, back:()=>{ + delete WIDGETS.kbswipe; Bangle.removeListener("stroke", strokeHandler); if (flashInterval) clearInterval(flashInterval); Bangle.setUI(); diff --git a/apps/kbswipe/metadata.json b/apps/kbswipe/metadata.json index 59622cb96..6b597a371 100644 --- a/apps/kbswipe/metadata.json +++ b/apps/kbswipe/metadata.json @@ -1,6 +1,6 @@ { "id": "kbswipe", "name": "Swipe keyboard", - "version":"0.05", + "version":"0.07", "description": "A library for text input via PalmOS style swipe gestures (beta!)", "icon": "app.png", "type":"textinput", diff --git a/apps/lato/README.md b/apps/lato/README.md new file mode 100644 index 000000000..556ee6fbc --- /dev/null +++ b/apps/lato/README.md @@ -0,0 +1,54 @@ +# Lato + +A simple clock with the Lato font, with fast load and clock_info + +![](screenshot1.png) +![](screenshot2.png) +![](screenshot3.png) + +This clock is a Lato version of Simplest++. Simplest++ provided the +smallest example of a clock that supports 'fast load' and 'clock +info'. Lato takes this one step further and adds the lovely Lato +font. The clock is derived from Simplest++ and inspired by the +Pastel Clock. + +## Usage + +* When the screen is unlocked, tap at the bottom of the csreen on the information text. + It should change color showing it is selected. + +* Swipe up or down to cycle through the info screens that can be displayed + when you have finished tap again towards the centre of the screen to unselect. + +* Swipe left or right to change the type of info screens displayed (by default + there is only one type of data so this will have no effect) + +* Settings are saved automatically and reloaded along with the clock. + +## About Clock Info's + +* The clock info modules enable all clocks to add the display of information to the clock face. + +* The default clock_info module provides a display of battery %, Steps, Heart Rate and Altitude. + +* Installing the [Sunrise ClockInfo](https://banglejs.com/apps/?id=clkinfosunrise) adds Sunrise and Sunset times into the list of info's. + + +## References + +* [What is Fast Load and how does it work](http://www.espruino.com/Bangle.js+Fast+Load) + +* [Clock Info Tutorial](http://www.espruino.com/Bangle.js+Clock+Info) + +* [How to load modules through the IDE](https://github.com/espruino/BangleApps/blob/master/modules/README.md) + + +## With Thanks + +* Gordon for support +* David Peer for his work on BW Clock + + +Written by: [Hugh Barney](https://github.com/hughbarney) For support +and discussion please post in the [Bangle JS +Forum](http://forum.espruino.com/microcosms/1424/) diff --git a/apps/lato/app.js b/apps/lato/app.js new file mode 100644 index 000000000..6045d7f17 --- /dev/null +++ b/apps/lato/app.js @@ -0,0 +1,138 @@ +/** + * + * Lato Clock + * + * The entire clock code is contained within the block below this + * supports 'fast load' + * + * To add support for clock_info_supprt we add the code marked at [1] and [2] + * + */ + +Graphics.prototype.setFontLato = function(scale) { + // Actual height 50 (54 - 5) + this.setFontCustom( + E.toString(require('heatshrink').decompress(atob('ADkD8AHFg/wA4sP/AHVD44HHgPALD0OA40+F43+H4wHGn/8A4v/L4sH/5PFj//CxkD/6eFCw9/GooWHh//wAWLgP/TgoWHn5rFCw41BMYqCHaRDKGgzLYKAJgFv//LIhQBAAI7DWgIABU4adBAAJTDn4HCVAaOCQQhvDAYQuBDYaxBgJEDh4HBgYzDPgUDIYYECA5DUDgIHBg4HEEgIHfF44/EA45HDL4xvHP46PHT5CvHX47PDGYcDb4zvHf5AA/AA9wA4yoDZYq/DXAgHDXYQHEXYQHEj4HHXYQHDn6UCA4d/e4sAXYYHCd4gHCXwbADA86DFA/4HGAGA3Db40HA4UDe40Hc4YHCh7nDA4UfA4X/A4U/A4b/Cv7vGX4UB/A+CZ4YaCgf9A4sH+IHCHwfjA4JWDj/DA4s/wYHFv4kCA4f+A4pKBA4sD/AHCG4R9BA4YCBj/gA4s/4AECN4R5BA4f/gf/Mgn///+A4wZBA4d//6JBA4c/VATHEVASUEEwIHEAAbnDAGbyCAAg+DgKwDA4S4DLQSlCSYQHCn4HDFAV/bAX/4ADFCYgbCh4zHZ4SlBR4iSEA46XCe4QHCDgJWCngHOnwHGvwHRG4iFBI4ppBA4f4OIRnCN4MD9+AO4f///v8CHCDoP/54CBS4f/44CBU4f/wYHBX4f/EQLHDh6gB/6jDZQaTDAEUcA40/A4xODYoYHGgYHGh4HGNIIuG74uGz4uGj4uF/gHFh/4A4sf+AHFn/AA4q0BA4kBVgIHEFwIHFFwIHFj7jBA4guBA4rjCA4YuCA4guCA4r0BAATgBA75SEa4wHvAEEBA40DUYIAEg4HDgZ0Bh67BXAQHCZYJMBA4UHA4KPCA4SXEAgQHL4IEBgIHC/AMCgP4CQUDFgIHoIQY3DA4wCEDggHFO4YHB/iHDCQX+gE/S4IHCOIP/U4IHCv6CBA4k/A4K1CEQKpBEIIHDh//HILSDTQK+CAAd/f64Amn4GFgLxCAAZfBSIIADN4heDP4YeDR4Z5CEwN/U4IABg4NBj6ADEwLHDIoQtBVgQuCHoIHDFwIHBe4QLB/14A4kH/i1BeQQuB/AHFn/wA4pLBA4guBwAHELoMAA4o9BA4Q3BgYFBJ4gCCA4pqBvxvDf4T2Bh4HCIIc/R4MCKISfBS4aQCU4gHDX4ioBY4paBNwQAD/6uDAAUOf4wAjO4QHNdQYHYmAHGW4gUEA4kPA4z7BA4v/A4qYBY4QHCh63CA4c/V4QHDV4Y6DV4YHCDwYHDDwYHDv7ODA4MBZwgHBcwL1DA4MfdogHBDwgHB+LtFgf3DwhMCDwgHCDwhcFA4geEA4IeFA4IeFd5AArj77EsCgB/gGCg5QBOQkf/6oB/77D//DA4JrCv//44HB4DkC//n/E/MgIcCRIMPA4X8RIUHegQCBFoL8DA4cBA4QaBv4HGvwHBTgMHHQM+HgIHhF44HFJ5RfGN45/Bz6NBP4SPHT4XnT4ivHX47PHgCQCb4bvIAHxdBMgRfD/58CKgf/WgIADP4JlFR4J1ET4QHCiACBQwQEBuC/CDIIHBX4QtBn+Aa4sfZ4bvCh+Ah4HGUAUHA4d/AgIHEa4QHDwJyCA4eDKIQHDx5pCA4bPDG4c/RIRPCjwuCA4aJBUwZnCRAcBP4SgE/+D/7+ET4ImDA4jIEX4KvFh7HGgbXGgF+f6oAggZeBSgShEb4RYCagQHGh5iDA5QXEE443HADoA=='))), + 46, + atob("DhglJSUlJSUlJSUlEA=="), + 64+(scale<<8)+(1<<16) + ); + return this; +} + + +Graphics.prototype.setFontLatoSmall = function(scale) { + // Actual height 25 (24 - 0) + this.setFontCustom( + E.toString(require('heatshrink').decompress(atob('ADE//lwj/+nEP/kcDCGAgEfAQIAFgfwgEB/AZIggCB4YCBsO8gEz/0Av/8gP/DgP+jAiBhvggO/+ED//gh/9wEH+HAgEYsEAhhMJkEB8E4gf4h0B70HgPDgOA4P//f///9//4mEYjk4h0PnkDgZEBwH8FhMfAQXAiEYDoMMjE8g0MDwOOnBgBvEAnwCBgFwAQJsBgHn8ACBGIPjg0B4ODgPA4OA4FxNoIvBgEHAQIAGVIMBTAM4/6bB8PAv/gsE4+BmCJQMHhgvB50D4F2gHgLwMwh7eCFwM+JwJhBZwgAHGwMAvwNJe4QCBv4TBVYPB/EB/J0Bj1AgECC4rZC8/AgfxDAPgn//4BsCABECEQMBkCkDCgaBBGQICBhJhCwAgIUgVgAQMwAQJ4CIoMB/4pB/6uCD4QYLABMHJgMDJgT0CO48GSogxMAA0cAQMOGIQMFVoMBAQMHAQMPEoM/agcBMoJIDGYTWDTIMHXQMH4H4g+Aj0DwEHJgIBBDAPAJgNgnEAuEPXgIeBSoQCBj49BABYjBjA+BhgCBgwCBga3B/+AAQPAv5EBZZIAK4CKB8D5B+ACB/CQB9iwB8ywB8Y9B8OA8Hg4E/8FgKwMwPILkLhA/BWgM8gY0CuACBnEMAAMGAAMDg5jB4eDMYPjGIO/4EPx6dBeAYACAoTZCZATOBgPMAQPGZQPDAQOBwDEBSYKPBSoyLDOgIAJjixB/4gB/+B4FxwFgmHAmEYsEYhk4CYMcjhjB/0BwP+D4N8FZSFBgEHLQMPCoMPPgMPGIMe4FgjwzBjwzBhwuBgkPToMDFwQCCBAIAFe4prBgTgBg6gBh6EBj8AsE+gEwKAMYIYMNUgMHVQJPCXIwADEIMH5/gg///EHj0OcAeDDQPBGYNgAIM4+Fwh/8vEH841B8ICBABZBCh4RBg57Bg8HGIMBx4vB58A4FvgFwv0AngCBGIJdBAQIMBFY8CgGAcoPggPACAJPBCQ0ICYI2B2CaB+A4BExBUCcwSTBgZaBgOwAQPcBgPGAQPjB4KGBJQIkBdIJ/MAAczAQMZDwMMDwMGsA0BmAxBzAPB5gCBswYKAB7QBI4ICBjhNBgwuBge4B4PYAQN8AQMcAQMOUoYAJDoMAVIUYhk4hkPjkGh6pBxxcB/wPBbIJTBD4s/LQN/foN4jyYBV4LVCmF+nEwv+MjFw80MuEjLIMw4cYmFhx/8mHH/0wseBzC2BxkGZAMB8bLBv40Bh6VBAAb0BPoIMCc4MfI4QLB/+Agf44ED+FggPgO4P8F4M/xgdBNoQYCg/wAwIJCh4xEPAv/+Ef//4h///kGAAMDAAMBwOBwHAAANgAAM4uFwj/8nEH+/4gPx/gmBvDHGgILBgZdBg//8BuB/CpBjgCBg8BNAOA8AEBsC0CcoL4BnD4BjkHaoIQBA4NAUwIAIMYo3B/0DH4J6BAIKmBGIydBjAxEhwxDf4SPBUgKhCOQQAHN4P/gICBwACBSocwAAMYAAMMAAKuKJQIsJAAJjDGIrGBMIJkBGAJhBMgIwBgAwBJQJ9Be47KEEoOAFYPwgPgvB8CY41wSo5hBhkHgZjBwOHLwIlBuF/wEQn45IMZR7DMYIwBAQIyBMgICBeQRjBMggYBv//8DNB+D5CTxcAY4QYFCpgyDPgIGB8ACBQIMAvEPGgJjB/hjBHRpKCDAP8PgRjGXAIIBEIMD7wCB47HB4HgAQM8YQMPC4IEBSwkgGgoxFVwSWHV6QAEVxEHEYR4Cj4aCRwQJGCYIWCAQUPIYIOCnwCBvgxKcCsHUIgoDh5AEgDyCjwCBCwiVKABH8C4P/XgI3BDoN8gPAhwCBY4PgAgNwgHgVgMwVgMYh0AjkHgEOCYIEB8ACBnhRBOwIrCGgN+H5I9BCQJ8FF4bcBhjcBgzcBgb1BgPBPgPxAQM/RgMPAQJSBFgkfQoM/R4N//xKCwE4CYM4gFwjkAnBjCGYx8ICoP4g+BVAXzwF/8C1B4CLBAA75FY4RjGwBXBF4VgR4M4m+Ah/5UYP4nkB/BPBDQIqCNQIABZ4Q9BIAPwMYM4Y4MOGYMHGYOBwJjB8IzBvPgjEf8EMg6ZBE4J8BF4ZKBAYKkCsACBNoRjFg//QQIYOcQIAGTYPwfILHBfgTbEQYKBBAQL9BY4ICBg4dCCoICCg/AVwPAJQKCBE4IxDJQRuCBYUfGAMD/DLCEIXwAwK7BgEPCYQMBv4cDDASuBAQIoBg5CCPgoqCv4GBj/8AwJKBDIIGCGIU/BgQfBBYIrCn4UBj5CCGIMDLQU/AwxhCBIIcD8ClBwEHKwimDAANAY4NgVIPwGIPwgfBDwP5EgMfNQQtEJoUH74CBwfgh+A/B8Bjx8BgcBFwOAP4TbCnhgCdAStCvgJCPIJUCAQPAB4Q3CHoUPAQKuGMQYABMYUwMAMYXIMMgP8g0B+xIB8cBwfhwHD4HAs/AsE34EwdYMYJoMMg8AgxjCGAoADv///8/AQOYBAPMAQNkAQMQfhDdCgZ5CnwCBg5tBQAgXCBITLBC4JmBgIxC543B84CBEYQAKkC4EewQWBgIsCAQTEGBIQ1BABcMAQMGMQRvFZIK9CEAa9BDAwMDjh7CD4oANLYMw/gpBvwfBsYsBmOAg0Y4EDhlggPmIAN/O4MfDAIALTwPgcAIuBuBMBmBWBmBWBjBvBhhPBg8BGIP3OQIbBgE/PAQAEgY6Bgf/AQPvwEDwHgEAIuB4CIBsAxEjiBBgykCAAouCv5NBn18gE4hxKGEAJKBXoXA8E///4j//Phf+PoS5B/pjB804gFjJQMxwwxB4YxBsJ8BmPAgP4GIN8d4QABoDnEUoICCwACB4DPBE4IzBZ4IZCgUAh0T4EP3/wh//jEGmeMgcI40BwwdB4wdBu4dBn+O8Efw/gPgPgEYKXHPgqzBMAICESoLKBAQLlBLQTXBn4YBgaMCAAhcBgHjewNx//wnAYCAAliAQMzx///PHB4IYBbQIAHOoP+g6VCgCkCXoLwBAQN/HIN5ZQN4BgMwfIMQLYRJDAAd/GgL5PHAPAgZjBgB8CAQRACcAUcKIeAgIYBAQPgSoYYIj4uDDAY+JZwauBKILEDY4xrCEAVwDAhHBagR8GB4IIBn5pBnzKBnCVBjApBhgpBGIJ8BLYJjBFgN/ZoMfAQMHaZJKBagJpBHwNgO4Nghh8BFIIxFg4sBDAJeBAQUfQo8DZoKVBAoPnDAOAVwOAFwPAjAxEAQMMMwJ/B/AbBdpSuIg6rEGIKuHcAQACg+GAQPDMYPwJQMYSoOOJQPHJQNhHoM4AQMIngeDgg0FPgJWB+BWDSoxFBA4LjDVwIFBDALKBN4R5BFIZbJAQKIBDAgAEhBpCDQMDdgV/AQMP8ADBDAUeTIQCBTYMBE4IYCcoILBHgQ5CnwCBj4hBgIYBeAcBBIKcBBAY3CSIUfIgQ6Cn4YEGoUPPgQLBGIS2BAASVCY4OB8EB+IbBn4CBh4YBgYCCLoMH7wCBw4YBwAYBgEQEwdAJRHwS4N+BQMPQYRUBJoUHGgJpCj7MEfITgDwFwKQN4SoN8fIP2MYPzfIPhwAkBJQPgsCuBmAYBOghUBgHB4OB///+P/7/8HgMGYAMDkDJEABEDDYP9AQLOCABJcCmYCBjPHx8cDAP8j4CBGwIaIH4MAOQICDL4LUDRIUHQwYDBLYMATwMAUgIbDAA8gS4NwnEAvAnBv8MgH+4kA/MygP4zsB+FOwfwl1D8EUk/ghkX4EEh/AgUHWAL8BgOBwEDIQUAnEGgUYh8BxEPwF0j9Aj0fkEPn0Qh2+hEOvkEgk8gRvBgZtCXRYANg8f/kDz/+gPD/4WNdwTcBgP/LgPgT4PgRoNgRQM/BgLXB4F8WoLWB4AEBDAOAagQAEMQMARQIrBweAeYPAEgPgAoMwjkYjEMAAMGAAIXBgcB4KjBbgPAIQT9EAA8wbwJ4BPgICBgeOHQQPB4KhBsBTBnBsBj/wgEd7kAHgIqJPQInBwMggPwyEAn1IBIPkBoJjBgF/EoP//EB/VAgfxkED8GQgPADALhOj/4v/v/k/EoIALjhsBz6SB//Dwf88HBx04sHHhkwsPHjEw8f8jk//kEh+8EYpKBAYJeBgCxBAAcIAQMOPgQVCDgsH/ACBx4SBEYMGnEwg1/zECv/mgdwucB2EcwGYg1ApkDkE2gOQjeBzEE4HMgazC4ACBmA8Bh88OYLjBAAsIK4MMSAMGtARBkhQByzGCUoS1MD4MBFYMD44FBxIWBj4OBn6KBniHBhCECABcwAQMYAQJcBJAICBM4IrBIISyCsACBnxOEh4MCAA6uJGoICBjEC//2gd//cB2PAVwNgVwM4kE3n0QjP77EEvHsIwMcVx50CL554CiBWEgYPBgbHBgYwBgOGBgPDAQNhVoQCBg7KIgw5BgY5BgOBCAPAFINgsED/+wgP/7CxBF4IYMAA8MK4MOnAaBPATABgP7B4N5I4VADAhzBDAdMDAMmDAP/B4N/DAMBbZKVE8ACETYQAGn//8Ef//wh//e4LzBWY8YJIQCDDALdB/6jGg4CCHAMHEwMHdgKeBLoQXB/40Bv///gvIg4PBwYCB8ICB8BIIbQgZCFYMDCYMBO4QAMgSzBgegAwPwAQKzBAA8QJoUggEcAwMPNIgjBDA8PIYMPIAMGKgMDhBJBwgPB9y6CAQMOJ5cOQgMDzwGBGoWcFgPhOAM9AQMHOwTAGLo0cLocILoMMLoMeLoM8PYl4Qgi2BUIPgU4PQgeB5ACB40BbwI4BHYY+DgjGCOwMHgkGgf+g75Bh4NBMwUcAQI9CHQVwAQPxw0B8PHgPA4+A4FnOAM+fYMOfY0DjACBwxFByIJB5gaBv/B8Ed8YeBgZQBg7LCVwkeX4M8hqBBjmAAQNgiDxCfYICBM4J2DgaQBgbIBgOOgPHwcA8fBwFx4A4BUAICBLQLDDEoR6C/weB/kgg/4jA3Bh0/xkDn8GgHegcAl4qBgf4FQM/FQIYBEIN/AQMPGgTbCGIRtBNwISEG4JZCfoMAj/GgHfLgPvNgPnfINh/lgmEfOQg7BT4QxCbAQxCGhkDGgMDz/GgOfGgPPGgNnGgM5GgMMGiaGBcIcfQwQFB/8B4P/gHH+IuDmfAsEN/Ewh1/jEGPgQYBTYjBBMAgxCeYYxBCQUf4JvBwPA/+A8bHBgfwsEB8EwgH8jDgCg8D/0BDAJmFRgIoBGIkAGIIgBCQY3Cn4LBv/AmKSBnpjBiXwjEPv0MgbgCgJmCDAUD/DEESoQxCTwsPBAMfAoM/C4l9BAJsBgJsHCIJfBn//IQMf/EMAAMGAAMDAAMBwOBwHAAANgAAMwAAIgBAIIAFVobABFYLCBg/AjkA8ACBnEOgEco5dB0ZjB6OAgO4DoPcuAVBnEAuAVBuECgBaBABEf//4h///nH//+sZaBnJaBxxZB4ZaGAAJzDgABBABaMCGIqMChYxBhoxBxgxB5wwBswwBmQxDgABBABf///Av//8G/GgPYDYPMJoNmGgMzGgMZGgOGGgPBMwaJLFw/nMYPzzAuBxjwHSoWcMYguJ4ACBsACBnIuBxwCB4ZgCIhgABgoVBwwCB4xKCBYMDAQMBCQUgAQI7CmYVBjICBxgbCCoWAAYNAAQI7CuACBiISBwB8FGIQYBgJgCGQcYAQLPBg///0DDYMBEISFB4CFDAALpDhCeBgCeBwEHFYIEBuACBj0HBQIhBEoI8Bn6JKj///EP//8IIUA+BJBnkAh0PwEGgPgSQN4GgMeYIMDJoIWBMoM8LQZ8EY5UfI4QyBv43BngyBnEB404gFzjjvB50Ajl2OYMbUIJzD8AEBXAPgCoPgg+BKIP/FYQ9Bh62DAAcB/AjB/64CDAPA/ApBjilFwQxB4ZwBsewgEzzhKBxxKBw6OBCoMORYKJBn4rCNIMA/h8V4G4gFw7kAnB8C8x8BuZYBjJKBwxKB4J8niZ8BjoxBxgxB4x8CmEAmBKFo58m475CVwIxDgx8BgZ8EzhKB5x8YAAUYhhbBLIMDcIMA9wCBnwCBBYIeBKYMOEgMOJYIYCgRFBABJsEwF///AvhBBdIU4gfwjkD3EOMQMGg5sBg8DPoK/B94VBvxpBUwPgj+B+EfNgMMNhzmD//8gP//8QOIOMBwPnAQNxKQMYoEAhkgO4cHAQh9BMAOAcwOAj7jIAAIxB/EAGgK6BPIILBKILgBGIMcGIMGmBEBUYMDzAdB5ACBDAMDOYJdBh4CBMYKxKv/+WII0BRIQxBnBjCGIMHAQMBGIMA5wCB8YCBuB8BuAFB/AxB/CVB/BjBVBQxBwBKB+AYB/gfBhxjCzhgDgFgAQMxAQM5BIMcQYMcBAM+GIJdBAQV/+AxDaILCEKIMBBwU+YwcBD4PhZAPxAQP54APB4F8gFAYYMBAQMADwRICRgIAGTwPwNgP4NgLtB4CaBsEYgEwhkAjEGJQMHJQMDIIPnJwKVCn7cCFw7gCTAQCB/BsCEwMMgcDg8BwfBwHD+HAue4sEfx1wh+D+EBwJkCFgw3Bh/AoOH8Ex40wjnDjEPsOMgOw40Aj1mLQLdBdpkAvBzBvxMBn52BmOAi0Y4E7hlgnOGmEY+cwhE/SoMPcAIAJgY0BNQMGsP4g1xxkHmHGgcYscBxgxBs+ZwEZSoMAv7YCABEeTgMfwFjPgNzxlgmJKBPgQ0BxkDnnMgeP/5DBPgIoKGgMw/kHPgMDzhKEgx8BkZ8C81gjl/YgMfPgIAJg6xBY4J8CjnjjEfJQMIY4MG7AxB98zJoSSBPgTKLhjKBh0/JQMw4CfBsBUBmEA404gFzAQMfKAMHKAMH94CBmIYIjAYBhgYBxwSB4w3BIwIADv4CCBIN9SoNwjlAmEHkEYgfQhkBdoMA7kDwAVB4FgRY4eBJQuHJQNjJQM5JQMOJQVjJQM5JQMPJQMD8aLHQoICBTYM/SQIYCjHDgHssOA8yVBGIUh5lwgF+P4MfGgIAFgJNBgP/gER/fAjIYBjhKBhhKBg0xw0DzAxBt1jwED+JEB/CcFAAMPJoMP/EDx/egPODAJKCY4oxCh1zjkCj+MY4gABF4MAVIU5//whn//ECv5aBABE3//Amf/8AYCO4UCAQMDAQUf/8Bx//wHnDAKNBgE4AQMcAQMEGIU//0Dz4YBOg0/AQN/8EQnhNBnEcg3Yg0D9g2B80BwFzgHAvnA8BQB8C3BcASeHAAUHJQMTCQM4CIMYAQMMQwMDK4MBzACB5wYB44YBFYcf+AoHBAMHNIMH78Aw5oBsawBnOAmEO4CXBsEMQwMOcYP+HAICBgACCAAsfJYI3Cj98T4MHKgJ8Bmx8BP4IxDiBoBPgP4FwICBgYCBAA1/AQQuBvvwg1wFgMwVwMYgcBxgxB8wxBmeAPgKrDAQMPAQIAFgJ/BSQMAmP34E54FwVwMYVwMMGISuBGIPOgeA4f/wAuBAQM/AQKuKgOHVwPnVwJ8CKQLYBGIMOGISuBgKuPYYMAgwCBgZgCHoMI4kAh1nHQJ9BgeZSoIYLABM/xACBRIM8boM4n0AjE7EgLYBXYPAgdwMYPwuEA/p2B/4CBs4CBQoxpC//AWgPwiAsBJgJ8BJwKNBJwoCBDAPgDARWJj/4dILdBg4uBBQLwCmEOLYICBhh+Bg0CQYQYBwAYEAA9/EIKCCz4uBJoOABQPAsDgBbwMwjgxBFIMYDAJzBj4YBABBjBNgJmBh1//h8BhwsBZY3AVINgnACBhH/OYIYBFA0IVwQaBgaRCv0BOAPHwA4B4eAn+DwAMBwAhBwYnBgZnBXYIbBHgTZF///+YCB/EDwInBwD5BwB+B4B5BsDiBnCzBj5/BewUAAQRrCAYRQDCIMP4EjwP4+PAn5IBg/wkAMBnEfwEcv8Agl8DYKGBgEQgA='))), + 32, + atob("BQkKDw8UEgYICAoPBQkFCQ8PDw8PDw8PDw8GBg8PDwoVERAREw8OEhMICxENFxMUDxQQDQ8SERkQEBAICQgPCggNDgwODQgNDgYGDQYVDg4ODgoLCQ4NEw0NDAgICA8AAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAADAAFCQ8PDw8IDQgUCQwPABQICg8ICAgOEQcICAoMEhISChERERERERcRDw8PDwgICAgUExQUFBQUDxQSEhISEA8PDQ0NDQ0NFAwNDQ0NBgYGBg4ODg4ODg4PDg4ODg4NDg0="), + 25+(scale<<8)+(1<<16) + ); + return this; +} + + + +{ + // must be inside our own scope here so that when we are unloaded everything disappears + // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global + + let draw = function() { + var date = new Date(); + var timeStr = require("locale").time(date,1); + var h = g.getHeight(); + var w = g.getWidth(); + + g.reset(); + g.setColor(g.theme.bg); + g.fillRect(Bangle.appRect); + + //g.setFont('Vector', w/3); + g.setFontLato(); + g.setFontAlign(0, 0); + g.setColor(g.theme.fg); + g.drawString(timeStr, w/2, h/2); + clockInfoMenu.redraw(); // clock_info_support + + // schedule a draw for the next minute + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); + }; + + /** + * clock_info_support + * this is the callback function that get invoked by clockInfoMenu.redraw(); + * + * We will display the image and text on the same line and centre the combined + * length of the image+text + * + * + */ + let clockInfoDraw = (itm, info, options) => { + //g.reset().setFont('Vector',24).setBgColor(options.bg).setColor(options.fg); + g.reset().setFontLatoSmall(); + g.setBgColor(options.bg).setColor(options.fg); + + //use info.text.toString(), steps does not have length defined + var text_w = g.stringWidth(info.text.toString()); + // gap between image and text + var gap = 10; + // width of the image and text combined + var w = gap + (info.img ? 24 :0) + text_w; + // different fg color if we tapped on the menu + if (options.focus) g.setColor(options.hl); + + // clear the whole info line, allow additional 2 pixels in case LatoFont overflows area + g.clearRect(0, options.y -2, g.getWidth(), options.y+ 23 + 2); + + // draw the image if we have one + if (info.img) { + // image start + var x = (g.getWidth() / 2) - (w/2); + g.drawImage(info.img, x, options.y); + // draw the text to the side of the image (left/centre alignment) + g.setFontAlign(-1,0).drawString(info.text, x + 23 + gap, options.y+12); + } else { + // text only option, not tested yet + g.setFontAlign(0,0).drawString(info.text, g.getWidth() / 2, options.y+12); + } + + }; + + // clock_info_support + // retrieve all the clock_info modules that are installed + let clockInfoItems = require("clock_info").load(); + + // clock_info_support + // setup the way we wish to interact with the menu + // the hl property defines the color the of the info when the menu is selected after tapping on it + let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:64, y:132, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#0ff"} ); + + // timeout used to update every minute + var drawTimeout; + g.clear(); + + // Show launcher when middle button pressed, add updown button handlers + Bangle.setUI({ + mode : "clock", + remove : function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + // remove info menu + clockInfoMenu.remove(); + delete clockInfoMenu; + // delete the custom fonts + delete Graphics.prototype.setFontLato; + delete Graphics.prototype.setFontLatoSmall; + } + }); + + // Load widgets + Bangle.loadWidgets(); + draw(); + setTimeout(Bangle.drawWidgets,0); +} // end of clock diff --git a/apps/lato/app.png b/apps/lato/app.png new file mode 100644 index 000000000..02a4031a3 Binary files /dev/null and b/apps/lato/app.png differ diff --git a/apps/lato/icon.js b/apps/lato/icon.js new file mode 100644 index 000000000..746f010dc --- /dev/null +++ b/apps/lato/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///1NygH+zn/Jf4AJgdVAAnABZ8BBYtABbc1BYtcBYcVBYtUBbcC1QAEwALPgYLFQYoLWgAHBytWAYK0F1Wpv/9tQLH0v//9aBY+XBYPWBY3qz/1r/21YLGv/Vq/9BY3Vv6NB/tXBaMVBYamEBZ1fHYP1BY01r5TB+ruEBYVXNYPVBY9VBYNVBY0FqoiBqtQBY4ACBb0NBYdwBbsBBYdABYoA/AAg=")) diff --git a/apps/lato/metadata.json b/apps/lato/metadata.json new file mode 100644 index 000000000..0b5e4a0f3 --- /dev/null +++ b/apps/lato/metadata.json @@ -0,0 +1,16 @@ +{ + "id": "lato", + "name": "Lato", + "version": "0.01", + "description": "A Lato Font clock with fast load and clock_info", + "readme": "README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot3.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"lato.app.js","url":"app.js"}, + {"name":"lato.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/lato/screenshot1.png b/apps/lato/screenshot1.png new file mode 100644 index 000000000..14c8d6d04 Binary files /dev/null and b/apps/lato/screenshot1.png differ diff --git a/apps/lato/screenshot2.png b/apps/lato/screenshot2.png new file mode 100644 index 000000000..f40495c79 Binary files /dev/null and b/apps/lato/screenshot2.png differ diff --git a/apps/lato/screenshot3.png b/apps/lato/screenshot3.png new file mode 100644 index 000000000..1cf135a60 Binary files /dev/null and b/apps/lato/screenshot3.png differ diff --git a/apps/launch/ChangeLog b/apps/launch/ChangeLog index 5da1b2661..0aff8c49f 100644 --- a/apps/launch/ChangeLog +++ b/apps/launch/ChangeLog @@ -19,3 +19,4 @@ 0.17: Don't display 'Loading...' now the watch has its own loading screen 0.18: Add 'back' icon in top-left to go back to clock 0.19: Fix regression after back button added (returnToClock was called twice!) +0.20: Use Bangle.showClock for changing to clock diff --git a/apps/launch/app.js b/apps/launch/app.js index b8e598f73..36f3aaf4b 100644 --- a/apps/launch/app.js +++ b/apps/launch/app.js @@ -42,16 +42,6 @@ let apps = launchCache.apps; if (!settings.fullscreen) Bangle.loadWidgets(); -let returnToClock = function() { - // unload everything manually - // ... or we could just call `load();` but it will be slower - Bangle.setUI(); // remove scroller's handling - if (lockTimeout) clearTimeout(lockTimeout); - Bangle.removeListener("lock", lockHandler); - // now load the default clock - just call .bootcde as this has the code already - setTimeout(eval,0,s.read(".bootcde")); -} - E.showScroller({ h : 64*scaleval, c : apps.length, draw : (i, r) => { @@ -74,7 +64,12 @@ E.showScroller({ load(app.src); } }, - back : returnToClock // button press or tap in top left calls returnToClock now + back : Bangle.showClock, // button press or tap in top left shows clock now + remove : () => { + // cleanup the timeout to not leave anything behind after being removed from ram + if (lockTimeout) clearTimeout(lockTimeout); + Bangle.removeListener("lock", lockHandler); + } }); g.flip(); // force a render before widgets have finished drawing @@ -85,7 +80,7 @@ let lockHandler = function(locked) { if (lockTimeout) clearTimeout(lockTimeout); lockTimeout = undefined; if (locked) - lockTimeout = setTimeout(returnToClock, 10000); + lockTimeout = setTimeout(Bangle.showClock, 10000); } Bangle.on("lock", lockHandler); if (!settings.fullscreen) // finally draw widgets diff --git a/apps/launch/metadata.json b/apps/launch/metadata.json index ce9b1f801..85fcdd02f 100644 --- a/apps/launch/metadata.json +++ b/apps/launch/metadata.json @@ -2,11 +2,12 @@ "id": "launch", "name": "Launcher", "shortName": "Launcher", - "version": "0.19", + "version": "0.20", "description": "This is needed to display a menu allowing you to choose your own applications. You can replace this with a customised launcher.", "readme": "README.md", "icon": "app.png", "type": "launch", + "default": true, "tags": "tool,system,launcher", "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ diff --git a/apps/lcdclock/ChangeLog b/apps/lcdclock/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/lcdclock/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/lcdclock/app-icon.js b/apps/lcdclock/app-icon.js new file mode 100644 index 000000000..ed3161c41 --- /dev/null +++ b/apps/lcdclock/app-icon.js @@ -0,0 +1 @@ +atob("MDABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf///T//+f///T///Q///Qf//Sf//Qf//Pf//Qf//AP//Yf//f///c///f///f///fiD/f8f/fur/f9f/fir/f9f/f67/f9f/fy7/f8f/fz//f+//AAAAAAAAAAAAAAAAf///////TD/u///vQB/EX/9PUV/d3/9fSd/F3/9HWd/d3/9Xf9/d//9Hf///////f///////f///8Hg/fmefwPAffmecz/Offmecz/Offmecz/OffmAcwHOffngc8DPffn+c/zOffn+c/zOffn+c/zOffn+f8DAff///8Hg/f///8Pw/f///////f//////+P//////8AAAAAAAAAAAAAAAAAAAAAAAA") diff --git a/apps/lcdclock/app.js b/apps/lcdclock/app.js new file mode 100644 index 000000000..2bc23247c --- /dev/null +++ b/apps/lcdclock/app.js @@ -0,0 +1,84 @@ +Graphics.prototype.setFont7Seg = function() { + return this.setFontCustom(atob("AAAAAAAAAAAACAQCAAAAAAIAd0BgMBdwAAAAAAAADuAAAB0RiMRcAAAAAiMRiLuAAAcAQCAQdwAADgiMRiIOAAAd0RiMRBwAAAAgEAgDuAAAd0RiMRdwAADgiMRiLuAAAABsAAAd0QiEQdwAADuCIRCIOAAAd0BgMBAAAAAOCIRCLuAAAd0RiMRAAAADuiEQiAAAAAd0BgMBBwAADuCAQCDuAAAdwAAAAAAAAAAAIBALuAAAdwQCAQdwAADuAIBAIAAAAd0AgEAcEAgEAdwAd0AgEAdwAADugMBgLuAAAd0QiEQcAAADgiEQiDuAAAd0AgEAAAAADgiMRiIOAAAAEAgEAdwAADuAIBALuAAAdwBAIBdwAADuAIBAIOAIBALuADuCAQCDuAAAcAQCAQdwAAAOiMRiLgAAAA=="), 32, atob("BwAAAAAAAAAAAAAAAAcCAAcHBwcHBwcHBwcEAAAAAAAABwcHBwcHBwcHBwcHCgcHBwcHBwcHBwoHBwc="), 9); +} + + +{ // must be inside our own scope here so that when we are unloaded everything disappears + // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global +let drawTimeout; + +// Actually draw the watch face +let draw = function() { + var x = R.x + R.w/2; + var y = R.y + R.h/2; + g.reset().setColor(g.theme.bg).setBgColor(g.theme.fg); + g.clearRect(R.x,barY+2,R.x2,R.y2-8); + var date = new Date(); + var timeStr = require("locale").time(date, 1); // Hour and minute + g.setFontAlign(0, 0).setFont("7Seg:5").drawString(timeStr, x, y+39); + // Show date and day of week + g.setFontAlign(0, 0).setFont("7Seg:2"); + g.setFontAlign(-1, 0).drawString(require("locale").meridian(date).toUpperCase(), R.x+6, y); + g.setFontAlign(0, 0).drawString(require("locale").dow(date, 1).toUpperCase(), x, y); + g.setFontAlign(1, 0).drawString(date.getDate(), R.x2 - 6, y); + + // queue next draw + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +}; + +// Show launcher when middle button pressed +Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + delete Graphics.prototype.setFont7Seg; + // remove info menu + clockInfoMenu.remove(); + delete clockInfoMenu; + clockInfoMenu2.remove(); + delete clockInfoMenu2; + // reset theme + g.setTheme(oldTheme); + }}); +// Load widgets +Bangle.loadWidgets(); +var R = Bangle.appRect; +R.x+=1; +R.y+=1; +R.x2-=1; +R.y2-=1; +R.w-=2; +R.h-=2; +var midX = R.x+R.w/2; +var barY = 80; +// Clear the screen once, at startup +let oldTheme = g.theme; +g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(1); +g.fillRect({x:R.x, y:R.y, w:R.w, h:R.h, r:8}).clearRect(R.x,barY,R.w,barY+1).clearRect(midX,R.y,midX+1,barY); +draw(); +setTimeout(Bangle.drawWidgets,0); + +let clockInfoDraw = (itm, info, options) => { + let texty = options.y+41; + g.reset().setFont("7Seg").setColor(g.theme.bg).setBgColor(g.theme.fg); + if (options.focus) g.setBgColor("#FF0"); + g.clearRect({x:options.x,y:options.y,w:options.w,h:options.h,r:8}); + + if (info.img) g.drawImage(info.img, options.x+2, options.y+2); + var title = clockInfoItems[options.menuA].name; + var text = info.text.toString().toUpperCase(); + if (title!="Bangle") g.setFontAlign(1,0).drawString(title.toUpperCase(), options.x+options.w-2, options.y+14); + if (g.setFont("7Seg:2").stringWidth(text)+8>options.w) g.setFont("7Seg"); + g.setFontAlign(0,0).drawString(text, options.x+options.w/2, options.y+40); + +}; +let clockInfoItems = require("clock_info").load(); +let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:R.x, y:R.y, w:midX-2, h:barY-R.y-2, draw : clockInfoDraw}); +let clockInfoMenu2 = require("clock_info").addInteractive(clockInfoItems, { x:midX+2, y:R.y, w:midX-3, h:barY-R.y-2, draw : clockInfoDraw}); +} diff --git a/apps/lcdclock/app.png b/apps/lcdclock/app.png new file mode 100644 index 000000000..6a117b525 Binary files /dev/null and b/apps/lcdclock/app.png differ diff --git a/apps/lcdclock/metadata.json b/apps/lcdclock/metadata.json new file mode 100644 index 000000000..d7d09b106 --- /dev/null +++ b/apps/lcdclock/metadata.json @@ -0,0 +1,14 @@ +{ "id": "lcdclock", + "name": "LCD Clock", + "version":"0.01", + "description": "A Casio-style clock, with ClockInfo areas at the top and bottom. Tap them and swipe up/down to toggle between different information", + "icon": "app.png", + "screenshots": [{"url":"screenshot.png"}], + "type": "clock", + "tags": "clock,clkinfo", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"lcdclock.app.js","url":"app.js"}, + {"name":"lcdclock.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/lcdclock/screenshot.png b/apps/lcdclock/screenshot.png new file mode 100644 index 000000000..b0bb5934a Binary files /dev/null and b/apps/lcdclock/screenshot.png differ diff --git a/apps/lightswitch/ChangeLog b/apps/lightswitch/ChangeLog index 4c89bae76..c4aeb2c1e 100644 --- a/apps/lightswitch/ChangeLog +++ b/apps/lightswitch/ChangeLog @@ -3,3 +3,4 @@ 0.03: Settings page now uses built-in min/max/wrap (fix #1607) 0.04: Add masking widget input to other apps (using espruino/Espruino#2151), add a oversize option to increase the touch area. 0.05: Prevent drawing into app area. +0.06: Fix issue where .draw was being called by reference (not allowing widgets to be hidden) diff --git a/apps/lightswitch/metadata.json b/apps/lightswitch/metadata.json index b8da2f759..d1a8d6e2a 100644 --- a/apps/lightswitch/metadata.json +++ b/apps/lightswitch/metadata.json @@ -2,7 +2,7 @@ "id": "lightswitch", "name": "Light Switch Widget", "shortName": "Light Switch", - "version": "0.05", + "version": "0.06", "description": "A fast way to switch LCD backlight on/off, change the brightness and show the lock status. All in one widget.", "icon": "images/app.png", "screenshots": [ diff --git a/apps/lightswitch/widget.js b/apps/lightswitch/widget.js index d9d4d421d..9eb488aca 100644 --- a/apps/lightswitch/widget.js +++ b/apps/lightswitch/widget.js @@ -224,28 +224,20 @@ // main widget function // // display and setup/reset function - draw: function(locked) { + draw: function() { // setup shortcut to this widget var w = WIDGETS.lightswitch; - // set lcd brightness on unlocking - // all other cases are catched by the boot file - if (locked === false) Bangle.setLCDBrightness(w.isOn ? w.value : 0); - // read lock status - locked = Bangle.isLocked(); + var locked = Bangle.isLocked(); // remove listeners to prevent uncertainties - Bangle.removeListener("lock", w.draw); Bangle.removeListener("touch", w.touchListener); Bangle.removeListener("tap", require("lightswitch.js").tapListener); // draw widget icon w.drawIcon(locked); - // add lock listener - Bangle.on("lock", w.draw); - // add touch listener to control the light depending on settings at first position if (w.touchOn === "always" || !global.__FILE__ || w.touchOn.includes(__FILE__) || @@ -259,7 +251,15 @@ w = undefined; } }); + + Bangle.on("lock", locked => { + var w = WIDGETS.lightswitch; + // set lcd brightness on unlocking + // all other cases are catched by the boot file + if (locked === false) Bangle.setLCDBrightness(w.isOn ? w.value : 0); + w.draw() + }); // clear variable - settings = undefined; + delete settings; })() diff --git a/apps/linuxclock/ChangeLog b/apps/linuxclock/ChangeLog index 3f1ef5c55..1c4f7d79b 100644 --- a/apps/linuxclock/ChangeLog +++ b/apps/linuxclock/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App. -0.02: Performance improvements. \ No newline at end of file +0.02: Performance improvements. +0.03: Update clock_info to avoid a redraw diff --git a/apps/linuxclock/app.js b/apps/linuxclock/app.js index 02676310e..9470b803c 100644 --- a/apps/linuxclock/app.js +++ b/apps/linuxclock/app.js @@ -52,22 +52,22 @@ var H = g.getHeight(); items: [ { name: "time", get: () => ({ text: getTime(), img: null}), - show: function() { dateMenu.items[0].emit("redraw"); }, + show: function() {}, hide: function () {} }, { name: "day", get: () => ({ text: getDay(), img: null}), - show: function() { dateMenu.items[2].emit("redraw"); }, + show: function() {}, hide: function () {} }, { name: "date", get: () => ({ text: getDate(), img: null}), - show: function() { dateMenu.items[1].emit("redraw"); }, + show: function() {}, hide: function () {} }, { name: "week", get: () => ({ text: weekOfYear(), img: null}), - show: function() { dateMenu.items[3].emit("redraw"); }, + show: function() {}, hide: function () {} }, ] diff --git a/apps/linuxclock/metadata.json b/apps/linuxclock/metadata.json index dfb17a315..06ef66498 100644 --- a/apps/linuxclock/metadata.json +++ b/apps/linuxclock/metadata.json @@ -1,7 +1,7 @@ { "id": "linuxclock", "name": "Linux Clock", - "version": "0.02", + "version": "0.03", "description": "A Linux inspired clock.", "readme": "README.md", "icon": "app.png", diff --git a/apps/messagegui/ChangeLog b/apps/messagegui/ChangeLog new file mode 100644 index 000000000..228a952de --- /dev/null +++ b/apps/messagegui/ChangeLog @@ -0,0 +1,88 @@ +0.01: New App! +0.02: Add 'messages' library +0.03: Fixes for Bangle.js 1 +0.04: Add require("messages").clearAll() +0.05: Handling of message actions (ok/clear) +0.06: New messages now go at the start (fix #898) + Answering true/false now exits the messages app if no new messages + Back now marks a message as read + Clicking top-left opens a menu which allows you to delete a message or mark unread +0.07: Added settings menu with option to choose vibrate pattern and frequency (fix #909) +0.08: Fix rendering of long messages (fix #969) + buzz on new message (fix #999) +0.09: Message now disappears after 60s if no action taken and clock loads (fix 922) + Fix phone icon (#1014) +0.10: Respect the 'new' attribute if it was set from iOS integrations +0.11: Open app when touching the widget (Bangle.js 2 only) +0.12: Extra app-specific notification icons + New animated notification icon (instead of large blinking 'MESSAGES') + Added screenshots +0.13: Add /*LANG*/ comments for internationalisation + Add 'Delete All' option to message options + Now update correctly when 'require("messages").clearAll()' is called +0.14: Hide widget when all unread notifications are dismissed from phone +0.15: Don't buzz when Quiet Mode is active +0.16: Fix text wrapping so it fits the screen even if title is big (fix #1147) +0.17: Fix: Get dynamic dimensions of notify icon, fixed notification font +0.18: Use app-specific icon colors + Spread message action buttons out + Back button now goes back to list of messages + If showMessage called with no message (eg all messages deleted) now return to the clock (fix #1267) +0.19: Use a larger font for message text if it'll fit +0.20: Allow tapping on the body to show a scrollable view of the message and title in a bigger font (fix #1405, #1031) +0.21: Improve list readability on dark theme +0.22: Add Home Assistant icon + Allow repeat to be switched Off, so there is no buzzing repetition. + Also gave the widget a pixel more room to the right +0.23: Change message colors to match current theme instead of using green + Now attempt to use Large/Big/Medium fonts, and allow minimum font size to be configured +0.24: Remove left-over debug statement +0.25: Fix widget memory usage issues if message received and watch repeatedly calls Bangle.drawWidgets (fix #1550) +0.26: Setting to auto-open music +0.27: Add 'mark all read' option to popup menu (fix #1624) +0.28: Option to auto-unlock the watch when a new message arrives +0.29: Fix message list overwrites on Bangle.js 1 (fix #1642) +0.30: Add new Icons (Youtube, Twitch, MS TODO, Teams, Snapchat, Signal, Post & DHL, Nina, Lieferando, Kalender, Discord, Corona Warn, Bibel) +0.31: Option to disable icon flashing +0.32: Added an option to allow quiet mode to override message auto-open +0.33: Timeout from the message list screen if the message being displayed is removed and there is a timer going +0.34: Don't buzz for 'map' update messages +0.35: Reset graphics colors before rendering a message (possibly fix #1752) +0.36: Ensure a new message plus an almost immediate deletion of that message doesn't load the messages app (fix #1362) +0.37: Now use the setUI 'back' icon in the top left rather than specific buttons/menu items +0.38: Add telegram foss handling +0.39: Set default color for message icons according to theme +0.40: Use default Bangle formatter for booleans +0.41: Add notification icons in the widget +0.42: Fix messages ignoring "Vibrate: Off" setting +0.43: Add new Icons (Airbnb, warnwetter) +0.44: Separate buzz pattern for incoming calls +0.45: Added new app colors and icons +0.46: Add 'Vibrate Timer' option to set how long to vibrate for, and fix Repeat:off + Fix message removal from widget bar (previously caused exception as .hide has been removed) +0.47: Add new Icons (Nextbike, Mattermost, etc.) +0.48: When getting new message from the clock, only buzz once the messages app is loaded +0.49: Change messages icon (to fit within 24px) and ensure widget renders icons centrally +0.50: Add `getMessages` and `status` functions to library + Option to disable auto-open of messages + Option to make message icons monochrome (not colored) + messages widget buzz now returns a promise +0.51: Emit "message events" + Setting to hide widget + Add custom event handlers to prevent default app form loading + Move WIDGETS.messages.buzz() to require("messages").buzz() +0.52: Fix require("messages").buzz() regression + Fix background color in messages list after one unread message is shown +0.53: Messages now uses Bangle.load() to load messages app faster (if possible) +0.54: Move icons out to messageicons module +0.55: Rename to messagegui, move global message handling library to message module + Move widget to widmessage +0.56: Fix handling of music messages +0.57: Fix "unread Timeout" = off (previously defaulted to 60s) +0.58: Fast load messages without writing to flash + Don't write messages to flash until the app closes +0.59: Ensure we do write messages if messages app can't be fast loaded (see #2373) +0.60: Fix saving of removal messages if UI not open +0.61: Fix regression where loading into messages app stops back from working (#2398) +0.62: Remove '.show' field, tidyup and fix .open if fast load not enabled +0.63: Fix messages app loading on clock without fast load diff --git a/apps/messagegui/README.md b/apps/messagegui/README.md new file mode 100644 index 000000000..699588e1b --- /dev/null +++ b/apps/messagegui/README.md @@ -0,0 +1,68 @@ +# Messages app + +Default app to handle the display of messages and message notifications. It allows +them to be listed, viewed, and responded to. +It is installed automatically if you install `Android Integration` or `iOS Integration`. + +It is a replacement for the old `notify`/`gadgetbridge` apps. + + +## Settings + +You can change settings by going to the global `Settings` app, then `App Settings` +and `Messages`: + +* `Vibrate` - This is the pattern of buzzes that should be made when a new message is received +* `Vibrate for calls` - This is the pattern of buzzes that should be made when an incoming call is received +* `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds +* `Vibrate Timer` - When a new message is received when in a non-clock app, we display the message icon and +buzz every `Repeat` seconds. This is how long we continue to do that. +* `Unread Timer` - When a new message is received when showing the clock we go into the Messages app. +If there is no user input for this amount of time then the app will exit and return +to the clock where a ringing bell will be shown in the Widget bar. +* `Min Font` - The minimum font size used when displaying messages on the screen. A bigger font +is chosen if there isn't much message text, but this specifies the smallest the font should get before +it starts getting clipped. +* `Auto-Open Music` - Should the app automatically open when the phone starts playing music? +* `Unlock Watch` - Should the app unlock the watch when a new message arrives, so you can touch the buttons at the bottom of the app? + +## New Messages + +When a new message is received: + +* If you're in an app, the Bangle will buzz and a message icon appears in the Widget bar. You can tap this icon to view the message. +* If you're in a clock, the Messages app will automatically start and show the message + +When a message is shown, you'll see a screen showing the message title and text. + +* The 'back-arrow' button (or physical button on Bangle.js 2) goes back to Messages, marking the current message as read. +* The top-left icon shows more options, for instance deleting the message of marking unread +* On Bangle.js 2 you can tap on the message body to view a scrollable version of the title and text (or can use the top-left icon + `View Message`) +* If shown, the 'tick' button: + * **Android** opens the notification on the phone + * **iOS** responds positively to the notification (accept call/etc) +* If shown, the 'cross' button: + * **Android** dismisses the notification on the phone + * **iOS** responds negatively to the notification (dismiss call/etc) + +## Images +_1. Screenshot of a notification_ + +![](screenshot.png) + + +## Requests + +Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=messages%20app + +## Creator + +Gordon Williams + +## Contributors + +[Jeroen Peters](https://github.com/jeroenpeters1986) + +## Attributions + +Icons used in this app are from https://icons8.com diff --git a/apps/messages/app-icon.js b/apps/messagegui/app-icon.js similarity index 100% rename from apps/messages/app-icon.js rename to apps/messagegui/app-icon.js diff --git a/apps/messages/app-newmessage.js b/apps/messagegui/app-newmessage.js similarity index 50% rename from apps/messages/app-newmessage.js rename to apps/messagegui/app-newmessage.js index 328927c70..73d9a79c1 100644 --- a/apps/messages/app-newmessage.js +++ b/apps/messagegui/app-newmessage.js @@ -1,5 +1,5 @@ /* Called when we have a new message when we're in the clock... -BUZZ_ON_NEW_MESSAGE is set so when messages.app.js loads it knows +BUZZ_ON_NEW_MESSAGE is set so when messagegui.app.js loads it knows that it should buzz */ global.BUZZ_ON_NEW_MESSAGE = true; -eval(require("Storage").read("messages.app.js")); +eval(require("Storage").read("messagegui.app.js")); diff --git a/apps/messages/app.js b/apps/messagegui/app.js similarity index 90% rename from apps/messages/app.js rename to apps/messagegui/app.js index bebd92816..b158310a1 100644 --- a/apps/messages/app.js +++ b/apps/messagegui/app.js @@ -19,7 +19,6 @@ require("messages").pushMessage({"t":"add","id":1,"src":"Maps","title":"0 yd - H // call require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bob","body":"12421312",positive:true,negative:true}) */ - var Layout = require("Layout"); var settings = require('Storage').readJSON("messages.settings.json", true) || {}; var fontSmall = "6x8"; @@ -49,8 +48,16 @@ to the clock. */ var unreadTimeout; /// List of all our messages var MESSAGES = require("messages").getMessages(); -if (!Array.isArray(MESSAGES)) MESSAGES=[]; -var onMessagesModified = function(msg) { +if (Bangle.MESSAGES) { + // fast loading messages + Bangle.MESSAGES.forEach(m => require("messages").apply(m, MESSAGES)); + delete Bangle.MESSAGES; +} + +var onMessagesModified = function(type,msg) { + if (msg.handled) return; + msg.handled = true; + require("messages").apply(msg, MESSAGES); // TODO: if new, show this new one if (msg && msg.id!=="music" && msg.new && active!="map" && !((require('Storage').readJSON('setting.json', 1) || {}).quiet)) { @@ -62,9 +69,12 @@ var onMessagesModified = function(msg) { } showMessage(msg&&msg.id); }; +Bangle.on("message", onMessagesModified); + function saveMessages() { - require("Storage").writeJSON("messages.json",MESSAGES) + require("messages").write(MESSAGES); } +E.on("kill", saveMessages); function showMapMessage(msg) { active = "map"; @@ -97,16 +107,18 @@ function showMapMessage(msg) { layout.render(); function back() { // mark as not new and return to menu msg.new = false; - saveMessages(); layout = undefined; checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:0}); } Bangle.setUI({mode:"updown", back: back}, back); // any input takes us back } -var updateLabelsInterval; +let updateLabelsInterval; + function showMusicMessage(msg) { active = "music"; + // defaults, so e.g. msg.xyz.length doesn't error. `msg` should contain up to date info + msg = Object.assign({artist: "", album: "", track: "Music"}, msg); openMusic = msg.state=="play"; var trackScrollOffset = 0; var artistScrollOffset = 0; @@ -130,7 +142,6 @@ function showMusicMessage(msg) { openMusic = false; var wasNew = msg.new; msg.new = false; - saveMessages(); layout = undefined; if (wasNew) checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:0,openMusic:0}); else checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); @@ -213,24 +224,20 @@ function showMessageSettings(msg) { }, /*LANG*/"Delete" : () => { MESSAGES = MESSAGES.filter(m=>m.id!=msg.id); - saveMessages(); checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); }, /*LANG*/"Mark Unread" : () => { msg.new = true; - saveMessages(); checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); }, /*LANG*/"Mark all read" : () => { MESSAGES.forEach(msg => msg.new = false); - saveMessages(); checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); }, /*LANG*/"Delete all messages" : () => { E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Delete All Messages"}).then(isYes => { if (isYes) { MESSAGES = []; - saveMessages(); } checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); }); @@ -285,7 +292,7 @@ function showMessage(msgid) { } function goBack() { layout = undefined; - msg.new = false; saveMessages(); // read mail + msg.new = false; // read mail cancelReloadTimeout(); // don't auto-reload to clock now checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic}); } @@ -293,7 +300,7 @@ function showMessage(msgid) { ]; if (msg.positive) { buttons.push({type:"btn", src:atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA=="), cb:()=>{ - msg.new = false; saveMessages(); + msg.new = false; cancelReloadTimeout(); // don't auto-reload to clock now Bangle.messageResponse(msg,true); checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic}); @@ -302,7 +309,7 @@ function showMessage(msgid) { if (msg.negative) { if (buttons.length) buttons.push({width:32}); // nasty hack... buttons.push({type:"btn", src:atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA="), cb:()=>{ - msg.new = false; saveMessages(); + msg.new = false; cancelReloadTimeout(); // don't auto-reload to clock now Bangle.messageResponse(msg,false); checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic}); @@ -340,6 +347,7 @@ function showMessage(msgid) { clockIfNoMsg : bool clockIfAllRead : bool showMsgIfUnread : bool + openMusic : bool // open music if it's playing } */ function checkMessages(options) { @@ -357,10 +365,11 @@ function checkMessages(options) { var newMessages = MESSAGES.filter(m=>m.new&&m.id!="music"); // If we have a new message, show it if (options.showMsgIfUnread && newMessages.length) { + delete newMessages[0].show; // stop us getting stuck here if we're called a second time showMessage(newMessages[0].id); // buzz after showMessage, so being busy during layout doesn't affect the buzz pattern if (global.BUZZ_ON_NEW_MESSAGE) { - // this is set if we entered the messages app by loading `messages.new.js` + // this is set if we entered the messages app by loading `messagegui.new.js` // ... but only buzz the first time we view a new message global.BUZZ_ON_NEW_MESSAGE = false; // messages.buzz respects quiet mode - no need to check here @@ -368,8 +377,8 @@ function checkMessages(options) { } return; } - // no new messages: show playing music? (only if we have playing music to show) - if (options.openMusic && MESSAGES.some(m=>m.id=="music" && m.track && m.state=="play")) + // no new messages: show playing music? Only if we have playing music, or state=="show" (set by messagesmusic) + if (options.openMusic && MESSAGES.some(m=>m.id=="music" && ((m.track && m.state=="play") || m.state=="show"))) return showMessage('music'); // no new messages - go to clock? if (options.clockIfAllRead && newMessages.length==0) @@ -428,14 +437,16 @@ function cancelReloadTimeout() { g.clear(); Bangle.loadWidgets(); +require("messages").toggleWidget(false); Bangle.drawWidgets(); setTimeout(() => { - var unreadTimeoutMillis = (settings.unreadTimeout || 60) * 1000; - if (unreadTimeoutMillis) { - unreadTimeout = setTimeout(load, unreadTimeoutMillis); - } - // only openMusic on launch if music is new - var newMusic = MESSAGES.some(m => m.id === "music" && m.new); - checkMessages({ clockIfNoMsg: 0, clockIfAllRead: 0, showMsgIfUnread: 1, openMusic: newMusic && settings.openMusic }); + if (!isFinite(settings.unreadTimeout)) settings.unreadTimeout=60; + if (settings.unreadTimeout) + unreadTimeout = setTimeout(load, settings.unreadTimeout*1000); + // only openMusic on launch if music is new, or state=="show" (set by messagesmusic) + var musicMsg = MESSAGES.find(m => m.id === "music"); + checkMessages({ + clockIfNoMsg: 0, clockIfAllRead: 0, showMsgIfUnread: 1, + openMusic: ((musicMsg&&musicMsg.new) && settings.openMusic) || (musicMsg&&musicMsg.state=="show") }); }, 10); // if checkMessages wants to 'load', do that diff --git a/apps/messagegui/app.png b/apps/messagegui/app.png new file mode 100644 index 000000000..c9177692e Binary files /dev/null and b/apps/messagegui/app.png differ diff --git a/apps/messagegui/boot.js b/apps/messagegui/boot.js new file mode 100644 index 000000000..ce7f1b99c --- /dev/null +++ b/apps/messagegui/boot.js @@ -0,0 +1 @@ +Bangle.on("message", (type, msg) => require("messagegui").listener(type, msg)); diff --git a/apps/messagegui/lib.js b/apps/messagegui/lib.js new file mode 100644 index 000000000..a9436a77b --- /dev/null +++ b/apps/messagegui/lib.js @@ -0,0 +1,101 @@ +// Will calling Bangle.load reset everything? if false, we fast load +function loadWillReset() { + return Bangle.load === load || !Bangle.uiRemove; + /* FIXME: Maybe we need a better way of deciding if an app will + be fast loaded than just hard-coding a Bangle.uiRemove check. + Bangle.load could return a bool (as the load doesn't happen immediately). */ +} + +/** + * Listener set up in boot.js, calls into here to keep boot.js short + */ +exports.listener = function(type, msg) { + // Default handler: Launch the GUI for all unhandled messages (except music if disabled in settings) + if (msg.handled || (global.__FILE__ && __FILE__.startsWith('messagegui.'))) return; // already handled or app open + + // if no new messages now, make sure we don't load the messages app + if (exports.messageTimeout && !msg.new && require("messages").status(msg) !== "new") { + clearTimeout(exports.messageTimeout); + delete exports.messageTimeout; + } + if (msg.t==="remove") { + // we won't open the UI for removed messages, so make sure to delete it from flash + if (Bangle.MESSAGES) { + // we were waiting for exports.messageTimeout + require("messages").apply(msg, Bangle.MESSAGES); + if (!Bangle.MESSAGES.length) delete Bangle.MESSAGES; + } + return require("messages").save(msg); // always write removal to flash + } + + const appSettings = require("Storage").readJSON("messages.settings.json", 1) || {}; + let loadMessages = (Bangle.CLOCK || event.important); // should we load the messages app? + if (type==="music") { + if (Bangle.CLOCK && msg.state && msg.title && appSettings.openMusic) loadMessages = true; + else return; + } + // Write the message to Bangle.MESSAGES. We'll deal with it in messageTimeout below + if (!Bangle.MESSAGES) Bangle.MESSAGES = []; + require("messages").apply(msg, Bangle.MESSAGES); + if (!Bangle.MESSAGES.length) delete Bangle.MESSAGES; + const saveToFlash = () => { + // save messages from RAM to flash if we decide not to launch app + // We apply all of Bangle.MESSAGES here in one write + if (!Bangle.MESSAGES || !Bangle.MESSAGES.length) return; + let messages = require("messages").getMessages(msg); + (Bangle.MESSAGES || []).forEach(m => require("messages").apply(m, messages)); + require("messages").write(messages); + delete Bangle.MESSAGES; + } + msg.handled = true; + if ((msg.t!=="add" || !msg.new) && (type!=="music")) // music always has t:"modify" + return saveToFlash(); + + const quiet = (require("Storage").readJSON("setting.json", 1) || {}).quiet; + const unlockWatch = appSettings.unlockWatch; + // don't auto-open messages in quiet mode if quietNoAutOpn is true + if ((quiet && appSettings.quietNoAutOpn) || appSettings.noAutOpn) + loadMessages = false; + // after a delay load the app, to ensure we have all the messages + if (exports.messageTimeout) clearTimeout(exports.messageTimeout); + exports.messageTimeout = setTimeout(function() { + delete exports.messageTimeout; + if (!Bangle.MESSAGES) return; // message was removed during the delay + if (type!=="music") { + if (!loadMessages) { + // not opening the app, just buzz + saveToFlash(); + return require("messages").buzz(msg.src); + } + if (!quiet && unlockWatch) { + Bangle.setLocked(false); + Bangle.setLCDPower(1); // turn screen on + } + } + // if loading the gui would reload everything, we must save our messages + if (loadWillReset()) saveToFlash(); + exports.open(msg); + }, 500); +}; + +/** + * Launch GUI app with given message + * @param {object} msg + */ +exports.open = function(msg) { + if (msg && msg.id) { + // force a display by setting it as new and ensuring it ends up at the beginning of messages list + msg.new = 1; + if (loadWillReset()) { + // no fast loading: store message to load in flash - `msg` will be put in first + require("messages").save(msg, {force: 1}); + } else { + // fast load - putting it at the end of Bangle.MESSAGES ensures it goes at the start of messages list + if (!Bangle.MESSAGES) Bangle.MESSAGES=[]; + Bangle.MESSAGES = Bangle.MESSAGES.filter(m => m.id!=msg.id) + Bangle.MESSAGES.push(msg); // putting at the + } + } + + Bangle.load((msg && msg.new && msg.id!=="music") ? "messagegui.new.js" : "messagegui.app.js"); +}; diff --git a/apps/messagegui/metadata.json b/apps/messagegui/metadata.json new file mode 100644 index 000000000..1a7a6c750 --- /dev/null +++ b/apps/messagegui/metadata.json @@ -0,0 +1,24 @@ +{ + "id": "messagegui", + "name": "Message UI", + "shortName": "Messages", + "version": "0.63", + "description": "Default app to display notifications from iOS and Gadgetbridge/Android", + "icon": "app.png", + "type": "app", + "tags": "tool,system", + "supports": ["BANGLEJS","BANGLEJS2"], + "dependencies" : { "messageicons":"module" }, + "provides_modules": ["messagegui"], + "default": true, + "readme": "README.md", + "storage": [ + {"name":"messagegui","url":"lib.js"}, + {"name":"messagegui.app.js","url":"app.js"}, + {"name":"messagegui.new.js","url":"app-newmessage.js"}, + {"name":"messagegui.boot.js","url":"boot.js"}, + {"name":"messagegui.img","url":"app-icon.js","evaluate":true} + ], + "screenshots": [{"url":"screenshot.png"}], + "sortorder": -9 +} diff --git a/apps/messages/screenshot.png b/apps/messagegui/screenshot.png similarity index 100% rename from apps/messages/screenshot.png rename to apps/messagegui/screenshot.png diff --git a/apps/messageicons/ChangeLog b/apps/messageicons/ChangeLog index 52a4b35a7..c923b169f 100644 --- a/apps/messageicons/ChangeLog +++ b/apps/messageicons/ChangeLog @@ -1 +1,5 @@ 0.01: Moved message icons from messages into standalone library +0.02: Added several new icons and colors +0.03: Fix icons broken in 0v02 (#2386) + Store all icons in a separate binary file (much faster lookup) + diff --git a/apps/messageicons/icons.img b/apps/messageicons/icons.img new file mode 100644 index 000000000..104168357 Binary files /dev/null and b/apps/messageicons/icons.img differ diff --git a/apps/messageicons/icons/bibel.png b/apps/messageicons/icons/bibel.png new file mode 100644 index 000000000..053fcf178 Binary files /dev/null and b/apps/messageicons/icons/bibel.png differ diff --git a/apps/messageicons/icons/bring.png b/apps/messageicons/icons/bring.png new file mode 100644 index 000000000..673d1b7be Binary files /dev/null and b/apps/messageicons/icons/bring.png differ diff --git a/apps/messageicons/icons/default.png b/apps/messageicons/icons/default.png new file mode 100644 index 000000000..1f85079df Binary files /dev/null and b/apps/messageicons/icons/default.png differ diff --git a/apps/messageicons/icons/etar.png b/apps/messageicons/icons/etar.png new file mode 100644 index 000000000..24f0cc587 Binary files /dev/null and b/apps/messageicons/icons/etar.png differ diff --git a/apps/messageicons/icons/generate.js b/apps/messageicons/icons/generate.js new file mode 100755 index 000000000..e857032af --- /dev/null +++ b/apps/messageicons/icons/generate.js @@ -0,0 +1,143 @@ +#!/usr/bin/node + +// Creates lib.js from icons +// npm install png-js + +// default icon must come first in icon_names + +var imageconverter = require("../../../webtools/imageconverter.js"); +var icons = JSON.parse(require("fs").readFileSync(__dirname+"/icon_names.json")); +const imgOptions = { + mode : "1bit", + inverted : true, + transparent : true, + output: "raw" +}; +var PNG = require('png-js'); +var IMAGE_BYTES = 76; + +var iconTests = []; +var iconImages = []; // array of converted icons +var iconIndices = {}; // maps filename -> index in iconImages + +var promises = []; + +icons.forEach(icon => { + var index = iconIndices[icon.icon]; + if (index===undefined) { // need a new icon + index = iconImages.length; + iconIndices[icon.icon] = index; + iconImages.push(""); // placeholder + // create image + console.log("Loading "+icon.icon); + var png = new PNG(require("fs").readFileSync(__dirname+"/"+icon.icon)); + if (png.width!=24 || png.height!=24) { + console.warn(icon.icon+" should be 24x24px"); + } + + promises.push(new Promise(r => { + png.decode(function (pixels) { + var rgba = new Uint8Array(pixels); + var isTransparent = false; + for (var i=0;i { + // Yay, more JS. Why is it so hard to get the bytes??? + iconData.set(Array.prototype.slice.call(Buffer.from(img,"binary")), idx*IMAGE_BYTES) + }); + + console.log("Saving images"); + require("fs").writeFileSync(__dirname+"/../icons.img", Buffer.from(iconData,"binary")); + + console.log("Saving library"); + require("fs").writeFileSync(__dirname+"/../lib.js", `exports.getImage = function(msg) { + if (msg.img) return atob(msg.img); + let s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase(); + if (msg.id=="music") s="music"; + let match = ${JSON.stringify(","+icons.map(icon=>icon.app+"|"+icon.index).join(",")+",")}.match(new RegExp(\`,\${s}\\\\|(\\\\d+)\`)) + return require("Storage").read("messageicons.img", (match===null)?0:match[1]*${IMAGE_BYTES}, ${IMAGE_BYTES}); +}; + +exports.getColor = function(msg,options) { + options = options||{}; + var st = options.settings || require('Storage').readJSON("messages.settings.json", 1) || {}; + if (options.default===undefined) options.default=g.theme.fg; + if (st.iconColorMode == 'mono') return options.default; + const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase(); + return { + // generic colors, using B2-safe colors + // DO NOT USE BLACK OR WHITE HERE, just leave the declaration out and then the theme's fg color will be used + "airbnb": "#ff385c", // https://news.airbnb.com/media-assets/category/brand/ + "mail": "#ff0", + "music": "#f0f", + "phone": "#0f0", + "sms message": "#0ff", + // brands, according to https://www.schemecolor.com/?s (picking one for multicolored logos) + // all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?) + "bibel": "#54342c", + "bring": "#455a64", + "discord": "#5865f2", // https://discord.com/branding + "etar": "#36a18b", + "facebook": "#1877f2", // https://www.facebook.com/brand/resources/facebookapp/logo + "gmail": "#ea4335", + "gmx": "#1c449b", + "google": "#4285F4", + "google home": "#fbbc05", +// "home assistant": "#41bdf5", // ha-blue is #41bdf5, but that's the background + "instagram": "#ff0069", // https://about.instagram.com/brand/gradient + "lieferando": "#ff8000", + "linkedin": "#0a66c2", // https://brand.linkedin.com/ + "messenger": "#0078ff", + "mastodon": "#563acc", // https://www.joinmastodon.org/branding + "mattermost": "#00f", + "n26": "#36a18b", + "nextbike": "#00f", + "newpipe": "#f00", + "nina": "#e57004", + "opentasks": "#409f8f", + "outlook mail": "#0078d4", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products + "paypal": "#003087", + "pocket": "#ef4154f", // https://blog.getpocket.com/press/ + "post & dhl": "#f2c101", + "reddit": "#ff4500", // https://www.redditinc.com/brand + "signal": "#3a76f0", // https://github.com/signalapp/Signal-Desktop/blob/main/images/signal-logo.svg + "skype": "#0078d4", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products + "slack": "#e51670", + "snapchat": "#ff0", + "steam": "#171a21", + "teams": "#6264a7", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products + "telegram": "#0088cc", + "telegram foss": "#0088cc", + "to do": "#3999e5", + "twitch": "#9146ff", // https://brand.twitch.tv/ + "twitter": "#1d9bf0", // https://about.twitter.com/en/who-we-are/brand-toolkit + "vlc": "#ff8800", + "whatsapp": "#4fce5d", + "wordfeud": "#e7d3c7", + "youtube": "#f00", // https://www.youtube.com/howyoutubeworks/resources/brand-resources/#logos-icons-and-colors + }[s]||options.default; +}; + `); +}); diff --git a/apps/messageicons/icons/gmx.png b/apps/messageicons/icons/gmx.png new file mode 100644 index 000000000..185c90aa3 Binary files /dev/null and b/apps/messageicons/icons/gmx.png differ diff --git a/apps/messageicons/icons/home assistant.png b/apps/messageicons/icons/home assistant.png new file mode 100644 index 000000000..d08932ae8 Binary files /dev/null and b/apps/messageicons/icons/home assistant.png differ diff --git a/apps/messageicons/icons/icon_names.json b/apps/messageicons/icons/icon_names.json new file mode 100644 index 000000000..0085731cc --- /dev/null +++ b/apps/messageicons/icons/icon_names.json @@ -0,0 +1,111 @@ +[ + { "app":"default", "icon":"default.png" }, + { "app":"airbnb", "icon":"airbnb.png" }, + { "app":"alarm", "icon":"alarm.png" }, + { "app":"alarmclockreceiver", "icon":"alarm.png" }, + { "app":"amazon shopping", "icon":"amazon.png" }, + { "app":"bibel", "icon":"bibel.png" }, + { "app":"bitwarden", "icon":"security.png" }, + { "app":"1password", "icon":"security.png" }, + { "app":"lastpass", "icon":"security.png" }, + { "app":"dashlane", "icon":"security.png" }, + { "app":"bring", "icon":"bring.png" }, + { "app":"calendar", "icon":"etar.png" }, + { "app":"etar", "icon":"etar.png" }, + { "app":"chat", "icon":"google chat.png" }, + { "app":"chrome", "icon":"chrome.png" }, + { "app":"corona-warn", "icon":"coronavirus.png" }, + { "app":"bmo", "icon":"bank.png" }, + { "app":"desjardins", "icon":"bank.png" }, + { "app":"rbc mobile", "icon":"bank.png" }, + { "app":"nbc", "icon":"bank.png" }, + { "app":"rabobank", "icon":"bank.png" }, + { "app":"scotiabank", "icon":"bank.png" }, + { "app":"td (canada)", "icon":"bank.png" }, + { "app":"discord", "icon":"discord.png" }, + { "app":"drive", "icon":"google drive.png" }, + { "app":"element", "icon":"matrix element.png" }, + { "app":"facebook", "icon":"facebook.png" }, + { "app":"messenger", "icon":"facebook messenger.png" }, + { "app":"firefox", "icon":"firefox.png" }, + { "app":"firefox beta", "icon":"firefox.png" }, + { "app":"firefox nightly", "icon":"firefox.png" }, + { "app":"f-droid", "icon":"security.png" }, + { "app":"neo store", "icon":"security.png" }, + { "app":"aurora droid", "icon":"security.png" }, + { "app":"github", "icon":"github.png" }, + { "app":"gitlab", "icon":"gitlab.png" }, + { "app":"gmx", "icon":"gmx.png" }, + { "app":"google", "icon":"google.png" }, + { "app":"google home", "icon":"google home.png" }, + { "app":"google play store", "icon":"google play store.png" }, + { "app":"home assistant", "icon":"home assistant.png" }, + { "app":"instagram", "icon":"instagram.png" }, + { "app":"kalender", "icon":"kalender.png" }, + { "app":"keep notes", "icon":"google keep.png" }, + { "app":"lieferando", "icon":"lieferando.png" }, + { "app":"linkedin", "icon":"linkedin.png" }, + { "app":"maps", "icon":"map.png" }, + { "app":"organic maps", "icon":"map.png" }, + { "app":"osmand", "icon":"map.png" }, + { "app":"mastodon", "icon":"mastodon.png" }, + { "app":"fedilab", "icon":"mastodon.png" }, + { "app":"tooot", "icon":"mastodon.png" }, + { "app":"tusky", "icon":"mastodon.png" }, + { "app":"mattermost", "icon":"mattermost.png" }, + { "app":"n26", "icon":"n26.png" }, + { "app":"netflix", "icon":"netflix.png" }, + { "app":"news", "icon":"news.png" }, + { "app":"cbc news", "icon":"news.png" }, + { "app":"rc info", "icon":"news.png" }, + { "app":"reuters", "icon":"news.png" }, + { "app":"ap news", "icon":"news.png" }, + { "app":"la presse", "icon":"news.png" }, + { "app":"nbc news", "icon":"news.png" }, + { "app":"nextbike", "icon":"nextbike.png" }, + { "app":"nina", "icon":"nina.png" }, + { "app":"outlook mail", "icon":"outlook.png" }, + { "app":"paypal", "icon":"paypal.png" }, + { "app":"phone", "icon":"phone.png" }, + { "app":"plex", "icon":"plex.png" }, + { "app":"pocket", "icon":"pocket.png" }, + { "app":"post & dhl", "icon":"delivery.png" }, + { "app":"proton mail", "icon":"protonmail.png" }, + { "app":"reddit", "icon":"reddit.png" }, + { "app":"sync pro", "icon":"reddit.png" }, + { "app":"sync dev", "icon":"reddit.png" }, + { "app":"boost", "icon":"reddit.png" }, + { "app":"infinity", "icon":"reddit.png" }, + { "app":"slide", "icon":"reddit.png" }, + { "app":"signal", "icon":"signal.png" }, + { "app":"skype", "icon":"skype.png" }, + { "app":"slack", "icon":"slack.png" }, + { "app":"snapchat", "icon":"snapchat.png" }, + { "app":"starbucks", "icon":"cafe.png" }, + { "app":"steam", "icon":"steam.png" }, + { "app":"teams", "icon":"teams.png" }, + { "app":"telegram", "icon":"telegram.png" }, + { "app":"telegram foss", "icon":"telegram.png" }, + { "app":"threema", "icon":"threema.png" }, + { "app":"tiktok", "icon":"tiktok.png" }, + { "app":"to do", "icon":"task.png" }, + { "app":"opentasks", "icon":"task.png" }, + { "app":"tasks", "icon":"task.png" }, + { "app":"transit", "icon":"transit.png" }, + { "app":"twitch", "icon":"twitch.png" }, + { "app":"twitter", "icon":"twitter.png" }, + { "app":"uber", "icon":"taxi.png" }, + { "app":"lyft", "icon":"taxi.png" }, + { "app":"vlc", "icon":"vlc.png" }, + { "app":"warnapp", "icon":"warnapp.png" }, + { "app":"whatsapp", "icon":"whatsapp.png" }, + { "app":"wordfeud", "icon":"wordfeud.png" }, + { "app":"youtube", "icon":"youtube.png" }, + { "app":"newpipe", "icon":"youtube.png" }, + { "app":"zoom", "icon":"videoconf.png" }, + { "app":"meet", "icon":"videoconf.png" }, + { "app":"music", "icon":"music.png" }, + { "app":"sms message", "icon":"default.png" }, + { "app":"mail", "icon":"default.png" }, + { "app":"gmail", "icon":"default.png" } +] diff --git a/apps/messageicons/icons/kalender.png b/apps/messageicons/icons/kalender.png new file mode 100644 index 000000000..dd807dd9e Binary files /dev/null and b/apps/messageicons/icons/kalender.png differ diff --git a/apps/messageicons/icons/music.png b/apps/messageicons/icons/music.png new file mode 100644 index 000000000..62f7acfee Binary files /dev/null and b/apps/messageicons/icons/music.png differ diff --git a/apps/messageicons/icons/n26.png b/apps/messageicons/icons/n26.png new file mode 100644 index 000000000..aa441ab8b Binary files /dev/null and b/apps/messageicons/icons/n26.png differ diff --git a/apps/messageicons/icons/nextbike.png b/apps/messageicons/icons/nextbike.png new file mode 100644 index 000000000..467bed8ac Binary files /dev/null and b/apps/messageicons/icons/nextbike.png differ diff --git a/apps/messageicons/icons/nina.png b/apps/messageicons/icons/nina.png new file mode 100644 index 000000000..2669b6401 Binary files /dev/null and b/apps/messageicons/icons/nina.png differ diff --git a/apps/messageicons/icons/warnapp.png b/apps/messageicons/icons/warnapp.png new file mode 100644 index 000000000..988485053 Binary files /dev/null and b/apps/messageicons/icons/warnapp.png differ diff --git a/apps/messageicons/icons/wordfeud.png b/apps/messageicons/icons/wordfeud.png new file mode 100644 index 000000000..83963d4d4 Binary files /dev/null and b/apps/messageicons/icons/wordfeud.png differ diff --git a/apps/messageicons/lib.js b/apps/messageicons/lib.js index ff9f4b680..c5be21bb0 100644 --- a/apps/messageicons/lib.js +++ b/apps/messageicons/lib.js @@ -1,51 +1,9 @@ exports.getImage = function(msg) { - /* - * icons should be 24x24px or less with 1bpp colors and 'Transparency to Color' - * http://www.espruino.com/Image+Converter - */ if (msg.img) return atob(msg.img); - const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase(); - if (s=="airbnb") return atob("GBgBAAAAAAAAAAAAADwAAH4AAGYAAMMAAIEAAYGAAYGAAzzAA2bABmZgBmZgDGYwDDwwCDwQCBgQDDwwB+fgA8PAAAAAAAAAAAAA"); - if (s=="alarm" || s =="alarmclockreceiver") return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA="); - if (s=="bibel") return atob("GBgBAAAAA//wD//4D//4H//4H/f4H/f4H+P4H4D4H4D4H/f4H/f4H/f4H/f4H/f4H//4H//4H//4GAAAEAAAEAAACAAAB//4AAAA"); - if (s=="bring") return atob("GBgBAAAAAAAAAAAAAAAAAHwAAFoAAf+AA/+AA/+AA/+AA/eAA+eAA0+AAx+AA7+AA/+AA//AA/+AAf8AAAIAAAAAAAAAAAAAAAAA"); - if (s=="calendar" || s=="etar") return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA=="); - if (s=="corona-warn") return atob("GBgBAAAAABwAAP+AAf/gA//wB/PwD/PgDzvAHzuAP8EAP8AAPAAAPMAAP8AAH8AAHzsADzuAB/PAB/PgA//wAP/gAH+AAAwAAAAA"); - if (s=="discord") return atob("GBgBAAAAAAAAAAAAAIEABwDgDP8wH//4H//4P//8P//8P//8Pjx8fhh+fzz+f//+f//+e//ePH48HwD4AgBAAAAAAAAAAAAAAAAA"); - if (s=="facebook" || s=="messenger") return atob("GBiBAAAAAAAAAAAYAAD/AAP/wAf/4A/48A/g8B/g+B/j+B/n+D/n/D8A/B8A+B+B+B/n+A/n8A/n8Afn4APnwADnAAAAAAAAAAAAAA=="); - if (s=="gmx") return atob("GBgBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEJmfmd8Zuc85v847/88Z9s8fttmHIHiAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"); - if (s=="google") return atob("GBiBAAAAAAD/AAP/wAf/4A/D4B8AwDwAADwAAHgAAHgAAHAAAHAH/nAH/nAH/ngH/ngAHjwAPDwAfB8A+A/D8Af/4AP/wAD/AAAAAA=="); - if (s=="google home") return atob("GBiCAAAAAAAAAAAAAAAAAAAAAoAAAAAACqAAAAAAKqwAAAAAqroAAAACquqAAAAKq+qgAAAqr/qoAACqv/6qAAKq//+qgA6r///qsAqr///6sAqv///6sAqv///6sAqv///6sA6v///6sA6v///qsA6qqqqqsA6qqqqqsA6qqqqqsAP7///vwAAAAAAAAAAAAAAAAA=="); // 2 bit unpaletted - if (s=="home assistant") return atob("FhaBAAAAAADAAAeAAD8AAf4AD/3AfP8D7fwft/D/P8ec572zbzbNsOEhw+AfD8D8P4fw/z/D/P8P8/w/z/AAAAA="); - if (s=="instagram") return atob("GBiBAAAAAAAAAAAAAAAAAAP/wAYAYAwAMAgAkAh+EAjDEAiBEAiBEAiBEAiBEAjDEAh+EAgAEAwAMAYAYAP/wAAAAAAAAAAAAAAAAA=="); - if (s=="kalender") return atob("GBgBBgBgBQCgff++RQCiRgBiQAACf//+QAACQAACR//iRJkiRIEiR//iRNsiRIEiRJkiR//iRIEiRIEiR//iQAACQAACf//+AAAA"); - if (s=="lieferando") return atob("GBgBABgAAH5wAP9wAf/4A//4B//4D//4H//4P/88fV8+fV4//V4//Vw/HVw4HVw4HBg4HBg4HBg4HDg4Hjw4Hj84Hj44Hj44Hj44"); - if (s=="mattermost") return atob("GBgBAAAAAPAAA+EAB4MADgcYHAcYOA8MOB8OeD8GcD8GcH8GcD8HcD8HeBwHeAAOfAAOfgAePwA8P8D8H//4D//wB//gAf/AAH4A"); - if (s=="n26") return atob("GBgBAAAAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAOIAAOIAAPIAANoAANoAAM4AAMYAAMYAAAAAAAAAAAAAAP8AAAAAAAAAAAAAAAAA"); - if (s=="nextbike") return atob("GBgBAAAAAAAAAAAAAAAAAAAAAACAfgDAPwDAP4HAH4N4H8f8D82GMd8CMDsDMGMDMGGGGMHOD4D8AAAAAAAAAAAAAAAAAAAAAAAA"); - if (s=="nina") return atob("GBgBAAAABAAQCAAICAAIEAAEEgAkJAgSJBwSKRxKSj4pUn8lVP+VVP+VUgAlSgApKQBKJAASJAASEgAkEAAECAAICAAIBAAQAAAA"); - if (s=="outlook mail") return atob("HBwBAAAAAAAAAAAIAAAfwAAP/gAB/+AAP/5/A//v/D/+/8P/7/g+Pv8Dye/gPd74w5znHDnOB8Oc4Pw8nv/Dwe/8Pj7/w//v/D/+/8P/7/gf/gAA/+AAAfwAAACAAAAAAAAAAAA="); - if (s=="paypal") return atob("GBgBAAAAAAAAAAAAAf+AAf/AAf/gA//gA//gA//wA//wA//wA//wB//wB//wB//gB/+AB/gAB/gAB/gAAPgAAPgAAAAAAAAAAAAA"); - if (s=="phone") return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA="); - if (s=="post & dhl") return atob("GBgBAPgAE/5wMwZ8NgN8NgP4NgP4HgP4HgPwDwfgD//AB/+AAf8AAAAABs7AHcdgG4MwAAAAGESAFESAEkSAEnyAEkSAFESAGETw"); - if (s=="signal") return atob("GBgBAAAAAGwAAQGAAhggCP8QE//AB//oJ//kL//wD//0D//wT//wD//wL//0J//kB//oA//ICf8ABfxgBYBAADoABMAABAAAAAAA"); - if (s=="skype") return atob("GhoBB8AAB//AA//+Af//wH//+D///w/8D+P8Afz/DD8/j4/H4fP5/A/+f4B/n/gP5//B+fj8fj4/H8+DB/PwA/x/A/8P///B///gP//4B//8AD/+AAA+AA=="); - if (s=="slack") return atob("GBiBAAAAAAAAAABAAAHvAAHvAADvAAAPAB/PMB/veD/veB/mcAAAABzH8B3v+B3v+B3n8AHgAAHuAAHvAAHvAADGAAAAAAAAAAAAAA=="); - if (s=="snapchat") return atob("GBgBAAAAAAAAAH4AAf+AAf+AA//AA//AA//AA//AA//AH//4D//wB//gA//AB//gD//wH//4f//+P//8D//wAf+AAH4AAAAAAAAA"); - if (s=="steam") return atob("GBgBAAAAAAAAAAAAAAAAAAAAAAfgAAwwAAvQABvQABvQADvQgDww4H/g+f8A/zwAf9gAH9AAB8AAACAAAcAAAAAAAAAAAAAAAAAA"); - if (s=="teams") return atob("GBgBAAAAAAAAAAQAAB4AAD8IAA8cP/M+f/scf/gIeDgAfvvefvvffvvffvvffvvff/vff/veP/PeAA/cAH/AAD+AAD8AAAQAAAAA"); - if (s=="telegram" || s=="telegram foss") return atob("GBiBAAAAAAAAAAAAAAAAAwAAHwAA/wAD/wAf3gD/Pgf+fh/4/v/z/P/H/D8P/Acf/AM//AF/+AF/+AH/+ADz+ADh+ADAcAAAMAAAAA=="); - if (s=="threema") return atob("GBjB/4Yx//8AAAAAAAAAAAAAfgAB/4AD/8AH/+AH/+AP//AP2/APw/APw/AHw+AH/+AH/8AH/4AH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="); - if (s=="to do" || s=="opentasks") return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA"); - if (s=="twitch") return atob("GBgBH//+P//+P//+eAAGeAAGeAAGeDGGeDOGeDOGeDOGeDOGeDOGeDOGeAAOeAAOeAAcf4/4f5/wf7/gf//Af/+AA/AAA+AAAcAA"); - if (s=="twitter") return atob("GhYBAABgAAB+JgA/8cAf/ngH/5+B/8P8f+D///h///4f//+D///g///wD//8B//+AP//gD//wAP/8AB/+AB/+AH//AAf/AAAYAAA"); - if (s=="warnapp") return atob("GBgBAAAAAAAAAAAAAH4AAP8AA//AA//AD//gP//gf//4f//+/+P+/8H//8n//4n/fxh/fzg+Pj88Dn44AA4AAAwAAAwAAAgAAAAA"); - if (s=="whatsapp") return atob("GBiBAAB+AAP/wAf/4A//8B//+D///H9//n5//nw//vw///x///5///4///8e//+EP3/APn/wPn/+/j///H//+H//8H//4H//wMB+AA=="); - if (s=="wordfeud") return atob("GBgCWqqqqqqlf//////9v//////+v/////++v/////++v8///Lu+v8///L++v8///P/+v8v//P/+v9v//P/+v+fx/P/+v+Pk+P/+v/PN+f/+v/POuv/+v/Ofdv/+v/NvM//+v/I/Y//+v/k/k//+v/i/w//+v/7/6//+v//////+v//////+f//////9Wqqqqqql"); - if (s=="youtube" || s=="newpipe") return atob("GBgBAAAAAAAAAAAAAAAAAf8AH//4P//4P//8P//8P5/8P4/8f4P8f4P8P4/8P5/8P//8P//8P//4H//4Af8AAAAAAAAAAAAAAAAA"); - if (msg.id=="music") return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A="); - // if (s=="sms message" || s=="mail" || s=="gmail") // .. default icon (below) - return atob("FhKBAH//+P//yf/+c//z5/+fz/z/n+f/Pz/+ef/8D///////////////////////f//4///A"); + let s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase(); + if (msg.id=="music") s="music"; + let match = ",default|0,airbnb|1,alarm|2,alarmclockreceiver|2,amazon shopping|3,bibel|4,bitwarden|5,1password|5,lastpass|5,dashlane|5,bring|6,calendar|7,etar|7,chat|8,chrome|9,corona-warn|10,bmo|11,desjardins|11,rbc mobile|11,nbc|11,rabobank|11,scotiabank|11,td (canada)|11,discord|12,drive|13,element|14,facebook|15,messenger|16,firefox|17,firefox beta|17,firefox nightly|17,f-droid|5,neo store|5,aurora droid|5,github|18,gitlab|19,gmx|20,google|21,google home|22,google play store|23,home assistant|24,instagram|25,kalender|26,keep notes|27,lieferando|28,linkedin|29,maps|30,organic maps|30,osmand|30,mastodon|31,fedilab|31,tooot|31,tusky|31,mattermost|32,n26|33,netflix|34,news|35,cbc news|35,rc info|35,reuters|35,ap news|35,la presse|35,nbc news|35,nextbike|36,nina|37,outlook mail|38,paypal|39,phone|40,plex|41,pocket|42,post & dhl|43,proton mail|44,reddit|45,sync pro|45,sync dev|45,boost|45,infinity|45,slide|45,signal|46,skype|47,slack|48,snapchat|49,starbucks|50,steam|51,teams|52,telegram|53,telegram foss|53,threema|54,tiktok|55,to do|56,opentasks|56,tasks|56,transit|57,twitch|58,twitter|59,uber|60,lyft|60,vlc|61,warnapp|62,whatsapp|63,wordfeud|64,youtube|65,newpipe|65,zoom|66,meet|66,music|67,sms message|0,mail|0,gmail|0,".match(new RegExp(`,${s}\\|(\\d+)`)) + return require("Storage").read("messageicons.img", (match===null)?0:match[1]*76, 76); }; exports.getColor = function(msg,options) { @@ -57,7 +15,7 @@ exports.getColor = function(msg,options) { return { // generic colors, using B2-safe colors // DO NOT USE BLACK OR WHITE HERE, just leave the declaration out and then the theme's fg color will be used - "airbnb": "#f00", + "airbnb": "#ff385c", // https://news.airbnb.com/media-assets/category/brand/ "mail": "#ff0", "music": "#f0f", "phone": "#0f0", @@ -66,39 +24,45 @@ exports.getColor = function(msg,options) { // all dithered on B2, but we only use the color for the icons. (Could maybe pick the closest 3-bit color for B2?) "bibel": "#54342c", "bring": "#455a64", - "discord": "#738adb", + "discord": "#5865f2", // https://discord.com/branding "etar": "#36a18b", - "facebook": "#4267b2", + "facebook": "#1877f2", // https://www.facebook.com/brand/resources/facebookapp/logo "gmail": "#ea4335", "gmx": "#1c449b", "google": "#4285F4", "google home": "#fbbc05", // "home assistant": "#41bdf5", // ha-blue is #41bdf5, but that's the background - "instagram": "#dd2a7b", - "lieferando": "#ee5c00", + "instagram": "#ff0069", // https://about.instagram.com/brand/gradient + "lieferando": "#ff8000", + "linkedin": "#0a66c2", // https://brand.linkedin.com/ "messenger": "#0078ff", + "mastodon": "#563acc", // https://www.joinmastodon.org/branding "mattermost": "#00f", "n26": "#36a18b", "nextbike": "#00f", "newpipe": "#f00", "nina": "#e57004", "opentasks": "#409f8f", - "outlook mail": "#0072c6", + "outlook mail": "#0078d4", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products "paypal": "#003087", + "pocket": "#ef4154f", // https://blog.getpocket.com/press/ "post & dhl": "#f2c101", - "signal": "#00f", - "skype": "#00aff0", + "reddit": "#ff4500", // https://www.redditinc.com/brand + "signal": "#3a76f0", // https://github.com/signalapp/Signal-Desktop/blob/main/images/signal-logo.svg + "skype": "#0078d4", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products "slack": "#e51670", "snapchat": "#ff0", "steam": "#171a21", - "teams": "#464eb8", + "teams": "#6264a7", // https://developer.microsoft.com/en-us/fluentui#/styles/web/colors/products "telegram": "#0088cc", "telegram foss": "#0088cc", "to do": "#3999e5", - "twitch": "#6441A4", - "twitter": "#1da1f2", + "twitch": "#9146ff", // https://brand.twitch.tv/ + "twitter": "#1d9bf0", // https://about.twitter.com/en/who-we-are/brand-toolkit + "vlc": "#ff8800", "whatsapp": "#4fce5d", "wordfeud": "#e7d3c7", - "youtube": "#f00", + "youtube": "#f00", // https://www.youtube.com/howyoutubeworks/resources/brand-resources/#logos-icons-and-colors }[s]||options.default; }; + \ No newline at end of file diff --git a/apps/messageicons/metadata.json b/apps/messageicons/metadata.json index eb907f893..079835a0b 100644 --- a/apps/messageicons/metadata.json +++ b/apps/messageicons/metadata.json @@ -1,14 +1,16 @@ { "id": "messageicons", "name": "Message Icons", - "version": "0.01", + "version": "0.03", "description": "Library containing a list of icons and colors for apps", "icon": "app.png", "type": "module", "tags": "tool,system", "supports": ["BANGLEJS","BANGLEJS2"], "provides_modules" : ["messageicons"], + "default": true, "storage": [ - {"name":"messageicons","url":"lib.js"} + {"name":"messageicons","url":"lib.js"}, + {"name":"messageicons.img","url":"icons.img"} ] } diff --git a/apps/messagelist/ChangeLog b/apps/messagelist/ChangeLog new file mode 100644 index 000000000..759f68777 --- /dev/null +++ b/apps/messagelist/ChangeLog @@ -0,0 +1 @@ +0.01: New app! \ No newline at end of file diff --git a/apps/messagelist/README.md b/apps/messagelist/README.md new file mode 100644 index 000000000..776d0d0e6 --- /dev/null +++ b/apps/messagelist/README.md @@ -0,0 +1,69 @@ +# Message List + +Display messages inline as a single list: +Displays one message at a time, if it doesn't fit on the screen you can scroll +up/down. When you reach the bottom, you can scroll on to the next message. + +## Installation +**First** uninstall the default [Message UI](/?id=messagegui) app (`messagegui`, +not the library!). +Then install this app. + +## Screenshots + +### Main menu: +![Screenshot](screenshot0.png) + +### Unread message: +![Screenshot](screenshot1.png) +The chevrons are hints for swipe actions: +- Swipe right to go back +- Swipe left for the message-actions menu +- Swipe down to show the previous message: We are currently viewing message 2 of 2, + so message 1 is "above" this one. + +### Long (read) message: +![Screenshot](screenshot2.png) +The button is disabled until you scroll all the way to the bottom. + +### Music: +![Screenshot](screenshot3.png) +Minimal setup: album name and buttons disabled through settings. +Swipe for next/previous song, tap to pause/resume. + +## Settings + +### Interface +* `Font size` - The font size used when displaying messages/music. +* `On Tap` - If messages are too large to fit on the screen, tapping the screen scrolls down. + This is the action to take when tapping a message after reaching the bottom: + - `Message menu`: Open menu with message actions + - `Dismiss`: Dismiss message right away + - `Back`: Go back to clock/main menu + - `Nothing`: Do nothing +* `Dismiss button` - Show inline button to dismiss message right away + +### Behaviour +* `Vibrate` - The pattern of buzzes when a new message is received. +* `Vibrate for calls` - The pattern of buzzes for incoming calls. +* `Vibrate for alarms` - The pattern of buzzes for (phone) alarms. +* `Repeat` - How often buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds. +* `Unread timer` - When a new message is received the Messages app is opened. + If there is no user input for this amount of time then the app will exit and return to the clock. +* `Auto-open` - Automatically open app when a new message arrives. +* `Respect quiet mode` - Prevent auto-opening during quiet mode. + +### Music +* `Auto-open` - Automatically open app when music starts playing. +* `Always visible` - Show "music" in the main menu even when nothing is playing. +* `Buttons` - Show `previous`/`play/pause`/`next` buttons on music screen. +* `Show album` - Display album names? + + +### Util +* `Delete all` - Erase all messages. + + +## Attributions + +Some icons used in this app are from https://icons8.com diff --git a/apps/messagelist/TODO.txt b/apps/messagelist/TODO.txt new file mode 100644 index 000000000..3a6d7b664 --- /dev/null +++ b/apps/messagelist/TODO.txt @@ -0,0 +1,17 @@ +## Nice to have: +* Add labels to B1 music HW buttons +* Add volume buttons to B2 music screen (when controls are enabled) +* Draw messages ourselves instead of piling hacks on Layout +* Make sure all icons are 24x24px: icon sizes affect layout +* Check/optimize layout for B1, other fonts (scrolling for just 5px is a shame) + +## Wishlist: +* Option to swipe-dismiss (instead of action menu) +* Maybe refactor showGrid() out into a general-use module? + +* Message replies (needs `android` support) +* Customize replies +* Custom replies (i.e. `textinput`) +* Hooks to add custom replies/actions, + e.g. external code could add "Send intent" option to Home Assistant messages + Maybe just use this for all replies, so we don't hardcode anything in "messages"? diff --git a/apps/messagelist/app-icon.js b/apps/messagelist/app-icon.js new file mode 100644 index 000000000..6ed3c1141 --- /dev/null +++ b/apps/messagelist/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UA///rkcAYP9ohL/ABMBqoAEoALDioLFqgLDBQoABERIkEBZcFBY9QBed61QAC1oLF7wLD24LF24LD7wLF1vqBQOrvQLFA4IuC9QLFD4IuC1QLGGAQOBBYwgBEwQLHvQBBEZHVq4jI7wWBHY5TLNZaDLTZazLffMBBY9ABZsABY4KCgEVBQtUBYYkGEQYA/AAwA=")) diff --git a/apps/messagelist/app.js b/apps/messagelist/app.js new file mode 100644 index 000000000..ebd5d4217 --- /dev/null +++ b/apps/messagelist/app.js @@ -0,0 +1,1208 @@ +/* MESSAGES is a list of: + {id:int, + src, + title, + subject, + body, + sender, + tel:string, + new:true // not read yet + } +*/ + +/* For example for maps: + +// a message +{"t":"add","id":1575479849,"src":"Hangouts","title":"A Name","body":"message contents"} +// maps +{"t":"add","id":1,"src":"Maps","title":"0 yd - High St","body":"Campton - 11:48 ETA","img":"GhqBAAAMAAAHgAAD8AAB/gAA/8AAf/gAP/8AH//gD/98B//Pg/4B8f8Afv+PP//n3/f5//j+f/wfn/4D5/8Aef+AD//AAf/gAD/wAAf4AAD8AAAeAAADAAA="} +// call +{"t":"add","id":"call","src":"Phone","name":"Bob","number":"12421312",positive:true,negative:true} +*/ +{ + const B2 = process.env.HWVERSION>1, // Bangle.js 2? + RIGHT = 1, LEFT = -1, // swipe directions + UP = -1, DOWN = 1; // updown directions + const Layout = require("Layout"); + + const settings = () => require("messagegui").settings(); + const fontTiny = "6x8"; // fixed size, don't use this for important things + let fontNormal; + // setFont() is also called after we close the settings screen + const setFont = function() { + const fontSize = settings().fontSize; + if (fontSize===0) // small + fontNormal = g.getFonts().includes("6x15") ? "6x15" : "6x8:2"; + else if (fontSize===2) // large + fontNormal = g.getFonts().includes("6x15") ? "6x15:2" : "6x8:4"; + else // medium + fontNormal = g.getFonts().includes("12x20") ? "12x20" : "6x8:3"; + }; + setFont(); + + let active, back; // active screen, last active screen + + /// List of all our messages + let MESSAGES; + const saveMessages = function() { + const noSave = ["alarm", "call", "music"]; // assume these are outdated once we close the app + noSave.forEach(id => remove({id: id})); + require("messages").write(MESSAGES + .filter(m => m.id && !noSave.includes(m.id)) + .map(m => { + delete m.show; + return m; + }) + ); + }; + const uiRemove = function() { + if (musicTimeout) clearTimeout(musicTimeout); + layout = undefined; + Bangle.removeListener("message", onMessage); + saveMessages(); + clearUnreadStuff(); + delete Bangle.appRect; + }; + const quitApp = () => load(); // TODO: revert to Bangle.showClock after fixing memory leaks + try { + MESSAGES = require("messages").getMessages(); + // Apply fast loaded messages + (Bangle.MESSAGES || []).forEach(m => require("messages").apply(m, MESSAGES)); + delete Bangle.MESSAGES; + // Write them back to storage when we're done + E.on("kill", saveMessages); + } catch(e) { + g.reset().clear(); + E.showPrompt(/*LANG*/"Message file corrupt, erase all messages?", {title:/*LANG*/"Delete All Messages"}).then(isYes => { + // We are troubleshooting, so do a clean "load" in both cases (instead of Bangle.load) + if (isYes) { // OK: erase message file and reload this app + require("messages").clearAll(); + load("messagelist.app.js"); + } else { + load(); // well, this app won't work... let's go back to the clock + } + }); + } + + const setUI = function(options, cb) { + options = Object.assign({remove: () => uiRemove()}, options); + Bangle.setUI(options, cb); + Bangle.on("message", onMessage); + }; + + const remove = function(msg) { + if (msg.id==="call") call = undefined; + else if (msg.id==="map") map = undefined; + else if (msg.id==="alarm") alarm = undefined; + else if (msg.id==="music") music = undefined; + else MESSAGES = MESSAGES.filter(m => m.id!==msg.id); + }; + const buzz = function(msg) { + return require("messages").buzz(msg.src); + }; + const show = function(msg) { + delete msg.show; // don't show this again + if (msg.id==="call") showCall(msg); + else if (msg.id==="map") showMap(msg); + else if (msg.id==="alarm") showAlarm(msg); + else if (msg.id==="music") showMusic(msg); + else showMessage(msg); + }; + + const onMessage = function(type, msg) { + if (msg.handled) return; + msg.handled = true; + switch(type) { + case "call": + return onCall(msg); + case "music": + return onMusic(msg); + case "map": + return onMap(msg); + case "alarm": + return onAlarm(msg); + case "text": + return onText(msg); + case "clearAll": + MESSAGES = []; + if (["messages", "menu"].includes(active)) showMenu(); + break; + default: + E.showAlert(/*LANG*/"Unknown message type:"+"\n"+type).then(goBack); + } + }; + Bangle.on("message", onMessage); + + const onCall = function(msg) { + if (msg.t==="remove") { + call = undefined; + return exitScreen("call"); + } + // incoming call: show it + call = msg; + buzz(call); + showCall(); + }; + const onAlarm = function(msg) { + if (msg.t==="remove") { + alarm = undefined; + return exitScreen("alarm"); + } + alarm = msg; + buzz(alarm); + showAlarm(); + }; + let musicTimeout; + const onMusic = function(msg) { + const hadMusic = !!music; + if (musicTimeout) clearTimeout(musicTimeout); + musicTimeout = undefined; + if (msg.t==="remove") { + music = undefined; + if (active==="main" && hadMusic) return showMain(); // refresh menu: remove "Music" entry (if not always visible) + else return exitScreen("music"); + } + + music = Object.assign({}, music, msg); + + // auto-close after being paused + if (music.state!=="play") musicTimeout = setTimeout(function() { + musicTimeout = undefined; + if (active==="music" && (!music || music.state!=="play")) quitApp(); + }, 60*1000); // paused for 1 minute + // auto-close after "playing" way beyond song duration (because "stop" messages don't seem to exist) + else musicTimeout = setTimeout(function() { + musicTimeout = undefined; + if (active==="music" && (!music || music.state==="play")) quitApp(); + }, 2*Math.max(music.dur || 0, 5*60)*1000); // playing: assume ended after twice song duration, or at least 10 minutes + + if (active==="music") showMusic(); // update music screen + else if (active==="main" && !hadMusic) { + if (settings().openMusic && music.state==="play" && music.track) showMusic(); + else showMain(); // refresh menu: add "Music" entry + } + }; + const onMap = function(msg) { + const hadMap = !!map; + if (msg.t==="remove") { + map = undefined; + if (back==="map") back = undefined; + if (active==="main" && hadMap) return showMain(); // refresh menu: remove "Map" entry + else return exitScreen("map"); + } + map = msg; + if (["map", "music"].includes(active)) showMap(); // update map screen, or switch away from music (not other screens) + else if (active==="main" && !hadMap) showMain(); // refresh menu: add "Map" entry + }; + const onText = function(msg) { + require("messages").apply(msg, MESSAGES); + const mIdx = MESSAGES.findIndex(m => m.id===msg.id); + if (!MESSAGES[mIdx]) if (back==="messages") back = undefined; + if (active==="main") showMain(); // update message count + if (MESSAGES.length===0) exitScreen("messages"); // removed last message + else if (active==="messages") showMessage(messageNum); + if (msg.new) buzz(msg); + if (active!=="call") {// don't switch away from incoming call + if (active!=="messages" || messageNum===mIdx) showMessage(mIdx); + } + if (active==="messages") drawFooter(); // update footer with new number of messages + }; + + const getImage = function(msg, def) { + // app icons, provided by `messages` app + return require("messageicons").getImage(msg); + }; + const getImageColor = function(msg, def) { + // app colors, provided by `messages` app + return require("messageicons").getColor(msg, {default: def}); + }; + const getIcon = function(icon) { + return require("messagegui").getIcon(icon); + }; + const getIconColor = function(icon) { + return require("messagegui").getColor(icon); + }; + + /* + * icons should be 24x24px with 1bpp colors and transparancy + */ + const getMessageImage = function(msg) { + if (msg.img) return atob(msg.img); + if (msg.id==="music") return getIcon("Music"); + if (msg.id==="back") return getIcon("Back"); + const s = (msg.src || "").toLowerCase(); + + return getImage(s, "notification"); + }; + + const showMap = function() { + setActive("map"); + delete map.new; + let m, distance, street, target, eta; + m = map.title.match(/(.*) - (.*)/); + if (m) { + distance = m[1]; + street = m[2]; + } else { + street = map.title; + } + m = map.body.match(/(.*) - (.*)/); + if (m) { + target = m[1]; + eta = m[2]; + } else { + target = map.body; + } + let layout = new Layout({ + type: "v", c: [ + {type: "txt", font: fontNormal, label: target, bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, pad: 2}, + { + type: "h", bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, c: [ + {type: "txt", font: "6x8", label: "Towards"}, + {type: "txt", font: fontNormal, label: street}, + ] + }, + { + type: "h", fillx: 1, filly: 1, c: [ + map.img ? {type: "img", src: () => atob(map.img), scale: 2} : {}, + { + type: "v", fillx: 1, c: [ + {type: "txt", font: fontNormal, label: distance || ""}, + ] + }, + ] + }, + {type: "txt", font: "6x8:2", label: eta} + ] + }); + layout.render(); + // go back on any input + setUI({ + mode: "custom", + back: goBack, + btn: b => { + if (B2 || b===2) goBack(); + }, + swipe: dir => { + if (dir===RIGHT) showMain(); + }, + }); + }; + + const toggleMusic = function() { + const mc = cmd => { + if (Bangle.musicControl) Bangle.musicControl(cmd); + }; + if (!music) { + music = {state: "play"}; + mc("play"); + } else if (music.state==="play") { + music.state = "pause"; + mc("pause"); + } else { + music.state = "play"; + mc("play"); + } + if (layout && layout.musicIcon) { + // musicIcon/musicToggle .src returns icon based on current music.state + layout.update(layout.musicIcon); + if (layout.musicToggle) layout.update(layout.musicToggle); + layout.render(); + } + }; + + const doMusic = function(action) { + if (!Bangle.musicControl) return; + Bangle.buzz(50); + if (action==="toggle") toggleMusic(); + else Bangle.musicControl(action); + }; + const showMusic = function() { + if (active!==music) setActive("music"); + if (!music) music = {track: "", artist: "", album: "", state: "pause"}; + delete music.new; + const w = Bangle.appRect.w-50; // title/album need to leave room for icon + let artist, album; + if (music.album && settings().showAlbum) { + // max 2 lines for artist/album + artist = g.setFont(fontNormal).wrapString(music.artist, w).slice(0, 2).join("\n"); + album = g.wrapString(music.album, w).slice(0, 2).join("\n"); + } else { + // no album: artist gets 3 lines + artist = g.setFont(fontNormal).wrapString(music.artist, w).slice(0, 3).join("\n"); + album = ""; + } + // place (subtitle) on a new line + let track = music.track.replace(/ \(/, "\n("); + track = g.wrapString(track, Bangle.appRect.w).slice(0, 5).join("\n"); + // "unknown" n/c/dur can show up as -1 + let num, dur; + if ("n" in music && music.n>0) { + num = "#"+music.n; + if ("c" in music && music.c>0) { + num += "/"+music.c; + } + num = {type: "txt", font: fontTiny, bgCol: g.theme.bg, label: num}; + } + if ("dur" in music && music.dur>0) { + dur = Math.floor(music.dur/60)+":"+(music.dur%60).toString().padStart(2, "0"); + dur = {type: "txt", font: fontTiny, bgCol: g.theme.bg, label: dur}; + } + let info; + if (num && dur) info = {type: "h", fillx: 1, c: [{fillx: 1}, dur, {fillx: 1}, num, {fillx: 1},]}; + else if (num) info = num; + else if (dur) info = dur; + else info = {}; + + layout = new Layout({ + type: "v", c: [ + { + type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [ + { + id: "musicIcon", type: "img", pad: 10, bgCol: g.theme.bg2, col: g.theme.fg2 + , src: () => getIcon((music.state==="play") ? "music" : "pause") + }, + { + type: "v", fillx: 1, c: [ + {type: "txt", font: fontNormal, col: g.theme.fg2, bgCol: g.theme.bg2, label: artist, pad: 2, id: "artist"}, + album ? {type: "txt", font: fontNormal, col: g.theme.fg2, bgCol: g.theme.bg2, label: album, pad: 2, id: "album"} : {}, + ] + } + ] + }, + {type: "txt", halign: 0, font: fontNormal, bgCol: g.theme.bg, label: track, fillx: 1, filly: 1, pad: 2, id: "track"}, + settings().musicButtons ? { + type: "h", fillx: 1, c: [ + B2 ? {} : {width: 4}, + { + type: "btn", id: "previous", cb: () => doMusic("previous") + , src: () => getIcon("previous") + }, + {fillx: 1}, + { + type: "btn", id: "musicToggle", cb: () => doMusic("toggle") + , src: () => getIcon((music.state==="play") ? "pause" : "play") + }, + {fillx: 1}, + { + type: "btn", id: "next", cb: () => doMusic("next") + , src: () => getIcon("next") + }, + B2 ? {} : {width: 4}, + ] + } : {}, + info, + ] + }); + layout.render(); + let options = {mode: "updown"}; + // B1 with buttons: left hand side of screen is used for "previous" + if (B2 || !settings().musicButtons) options.back = goBack; + setUI(options, ud => { + if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup"); + else { + if (B2 || settings().musicButtons) goBack(); // B1 left-hand touch is "previous", so we need a way to go back + else doMusic("toggle"); + } + }); + + Bangle.swipeHandler = dir => { + if (dir!==0) doMusic(dir===RIGHT ? "previous" : "next"); + }; + Bangle.on("swipe", Bangle.swipeHandler); + + if (Bangle.touchHandler) Bangle.removeListener("touch", Bangle.touchHandler); + if (settings().musicButtons) { + // visible buttons + // left = previous, middle = toggle, right = next + if (B2) Bangle.touchHandler = (_side, xy) => { + // accept touches on the whole bottom and pick the closest button + if (xy.y2*Bangle.appRect.w/3) doMusic("next"); + else doMusic("toggle"); + }; + else Bangle.touchHandler = (side) => { + if (side===1) doMusic("previous"); + if (side===2) doMusic("next"); + if (side===3) doMusic("toggle"); + }; + } else { + // no buttons: touch = toggle + // B2 setUI sets touchHandler, override that (we only want up/down swipes from the UI) + Bangle.touchHandler = (side, e) => { + // B1: side 1 (left) = back, B2: only toggle for e outside widget area + if ((!B2 && side>1) || (B2 && e.y>Bangle.appRect.y)) doMusic("toggle"); + }; + } + Bangle.on("touch", Bangle.touchHandler); + }; + + let layout; + + const clearStuff = function() { + delete Bangle.appRect; + layout = undefined; + setUI(); + g.reset().clearRect(Bangle.appRect); + }; + const setActive = function(screen, args) { + clearStuff(); + if (active && screen!==active) back = active; + if (screen==="messages") messageNum = args; + active = screen; + }; + /** + * Go back to previous screen, preserving history + */ + const goBack = function() { + if (back==="call" && call) showCall(); + else if (back==="map" && map) showMap(); + else if (back==="music" && music) showMusic(); + else if (back==="messages" && MESSAGES.length) showMessage(); + else if (back) showMain(); // previous screen was "main", or no longer valid + else quitApp(); // no previous screen: go back to clock + }; + /** + * Leave screen, and make sure goBack() won't take us there anymore; + * @param {string} screen + */ + const exitScreen = function(screen) { + if (back===screen) back = (active==="main") ? undefined : "main"; + if (active===screen) { + active = undefined; + goBack(); + } + }; + const showMain = function() { + setActive("main"); + let grid = {"": {title:/*LANG*/"Messages", align: 0, back: load}}; + if (call) grid[/*LANG*/"Incoming Call"] = {icon: "Phone", cb: showCall}; + if (alarm) grid[/*LANG*/"Alarm"] = {icon: "Alarm", cb: showAlarm}; + const unread = MESSAGES.filter(m => m.new).length; + if (unread) { + grid[unread+" "+/*LANG*/"New"] = {icon: "Unread", cb: () => showMessage(MESSAGES.findIndex(m => m.new))}; + grid[/*LANG*/"All"+` (${MESSAGES.length})`] = {icon: "Notification", cb: showMessage}; + } else { + const allLabel = MESSAGES.length+" "+(MESSAGES.length===1 ?/*LANG*/"Message" :/*LANG*/"Messages"); + if (MESSAGES.length) grid[allLabel] = {icon: "Notification", cb: showMessage}; + else grid[/*LANG*/"No Messages"] = {icon: "Neg", cb: load}; + } + if (unread { + E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Dismiss Read Messages"}).then(isYes => { + if (isYes) { + MESSAGES.filter(m => !m.new).forEach(msg => { + Bangle.messageResponse(msg, false); + remove(msg); + }); + } + showMain(); + }); + } + }; + } + if (map) grid[/*LANG*/"Map"] = {icon: "Map", cb: showMap}; + if (music || settings().alwaysShowMusic) grid[/*LANG*/"Music"] = {icon: "Music", cb: showMusic}; + grid[/*LANG*/"settings"] = {icon: "settings", cb: showSettings}; + showGrid(grid); + }; + const clamp = function(val, min, max) { + if (valmax) return max; + return val; + }; + /** + * Show grid of labeled buttons, + * + * items: + * { + * cb: callback, + * img: button image, + * icon: icon name, // string, use getIcon(icon) instead of img + * col: icon color, // optional: defaults to getColor(icon) + * } + * "" item is options: + * { + * title: string, + * back: callback, + * rows/cols: (optional) fit to this many columns/rows, omit for automatic fit + * align: bottom row alignment if items don't fit perfectly into a grid + * -1: left + * 1: right + * 0: left, but move final button to the right + * undefined: spread (can be unaligned with rest of grid!) + * } + * @param items + */ + const showGrid = function(items) { + clearStuff(); + const options = items[""] || {}, + back = options.back || items["< Back"]; + const keys = Object.keys(items).filter(k => k!=="" && k!=="< Back"); + let cols; + if (options.cols) { + cols = options.cols; + } else if (options.rows) { + cols = Math.ceil(keys.length/options.rows); + } else { + const rows = Math.round(Math.sqrt(keys.length)); + cols = Math.ceil(keys.length/rows); + } + + let l = {type: "v", c: []}; + if (options.title) { + l.c.push({id: "title", type: "txt", label: options.title, font: (B2 ? "12x20" : "6x8:2"), fillx: 1}); + } + const w = Bangle.appRect.w/cols, // set explicit width, because labels can stick out + bgs = [g.theme.bgH, g.theme.bg2], // background colors used for buttons + newRow = () => ({type: "h", filly: 1, c: []}); + let row = newRow(), + cbs = [[]]; // callbacks for Bangle.js 2 touchHandler below + keys.forEach(key => { + const item = items[key], + label = g.setFont(fontTiny).wrapString(key, w).join("\n"); + let color = "col" in item ? item.col : getIconColor(item.icon || "Unknown"); + if (color && bgs.includes(g.setColor(color).getColor())) color = undefined; // make sure button is not invisible + row.c.push({ + type: "v", pad: 2, width: w, c: [ + { + type: "btn", + src: item.img || (() => getIcon(item.icon || "Unknown")), + col: color, + cb: B2 + ? undefined // We handle B2 touches below + : () => setTimeout(item.cb), // prevent MEMORY error from running cb() inside the Layout touchHandler + }, + {height: 2}, + {type: "txt", label: label, font: fontTiny}, + ] + }); + if (B2) cbs[cbs.length-1].push(item.cb); + if (row.c.length>=cols) { + l.c.push(row); + row = newRow(); + if (B2) cbs.push([]); + } + }); + if (row.c.length) { + if (options.align!==undefined) { + const filler = {width: w*(cols-row.c.length)}; + if (options.align=== -1) row.c.unshift(filler); // left + else if (options.align===1) row.c.push(filler); // right + else if (options.align===0) row.c.splice(row.c.length-1, 0, filler); // left, but final item on right + } + l.c.push(row); + } + layout = new Layout(l, {back: back}); + layout.render(); + + if (B2) { + // override touchHandler: no need to hit buttons exactly, just pick the nearest + if (Bangle.touchHandler) Bangle.removeListener("touch", Bangle.touchHandler); + Bangle.touchHandler = (side, xy) => { + if (xy.y<=Bangle.appRect.y) return; // widgetbar: ignore + let rows = l.c.length, + y = Bangle.appRect.y, h = Bangle.appRect.h; + if (options.title) { + rows--; + y += layout.title.h; + h -= layout.title.h; + } + const r = clamp(Math.floor(rows*(xy.y-y)/h), 0, rows-1); // row (0-indexed) + let c; // column (0-indexed) + if (rcbs[r].length-2) return; // gap before final item + } else { // spread + c = clamp(Math.floor(cbs[r].length*(xy.x-Bangle.appRect.x)/Bangle.appRect.w), 0, cols-1); + } + } + if (r { + setFont(); + showMain(); + }); + }; + const showCall = function() { + setActive("call"); + delete call.new; + Bangle.setLocked(false); + Bangle.setLCDPower(1); + + const w = g.getWidth()-48, + lines = g.setFont(fontNormal).wrapString(call.title, w), + title = (lines.length>2) ? lines.slice(0, 2).join("\n")+"..." : lines.join("\n"); + const respond = function(accept) { + Bangle.buzz(50); + Bangle.messageResponse(call, accept); + remove(call); + call = undefined; + goBack(); + }; + let options = {}; + if (!B2) { + options.btns = [ + { + label:/*LANG*/"accept", + cb: () => respond(true), + }, { + label:/*LANG*/"ignore", + cb: goBack, + }, { + label:/*LANG*/"reject", + cb: () => respond(false), + } + ]; + } + + layout = new Layout({ + type: "v", c: [ + { + type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [ + {type: "img", pad: 10, src: () => getIcon("phone"), col: getIconColor("phone")}, + { + type: "v", fillx: 1, c: [ + {type: "txt", font: fontTiny, label: call.src ||/*LANG*/"Incoming Call", bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, pad: 2, halign: 1}, + title ? {type: "txt", font: fontNormal, label: title, bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, pad: 2} : {}, + ] + }, + ] + }, + {type: "txt", font: fontNormal, label: call.body, fillx: 1, filly: 1, pad: 2, wrap: true}, + { + type: "h", fillx: 1, c: [ + // button callbacks won't actually be used: setUI below overrides the touchHandler set by Layout + {type: B2 ? "btn" : "img", src: () => getIcon("Neg"), cb: () => respond(false)}, + {fillx: 1}, + {type: B2 ? "btn" : "img", src: () => getIcon("Pos"), cb: () => respond(true)}, + ] + } + ] + }, options); + layout.render(); + setUI({ + mode: "custom", + back: goBack, + touch: (side, xy) => { + if (B2 && xy.y { + if (B2 || b===2) goBack(); + else if (b===1) respond(true); + else respond(false); + }, + swipe: dir => { + if (dir===RIGHT) showMain(); + }, + }); + }; + const showAlarm = function() { + // dismissing alarms doesn't seem to work, so this is simple */ + setActive("alarm"); + delete alarm.new; + Bangle.setLocked(false); + Bangle.setLCDPower(1); + + const w = g.getWidth()-48, + lines = g.setFont(fontNormal).wrapString(alarm.title, w), + title = (lines.length>2) ? lines.slice(0, 2).join("\n")+"..." : lines.join("\n"); + layout = new Layout({ + type: "v", c: [ + { + type: "h", fillx: 1, bgCol: g.theme.bg2, col: g.theme.fg2, c: [ + alarm.body ? {type: "img", pad: 10, src: () => getIcon("alarm"), col: getIconColor("alarm")} : {}, + {type: "txt", font: fontNormal, label: title ||/*LANG*/"Alarm", bgCol: g.theme.bg2, col: g.theme.fg2, fillx: 1, pad: 2, halign: 1}, + ] + }, + alarm.body + ? {type: "txt", font: fontNormal, label: alarm.body, fillx: 1, filly: 1, pad: 2, wrap: true} + : {type: "img", pad: 10, scale: 3, src: () => getIcon("alarm"), col: getIconColor("alarm")}, + ] + }); + layout.render(); + setUI({ + mode: "custom", + back: goBack, + btn: b => { + if (B2 || b===2) goBack(); + }, + swipe: dir => { + if (dir===RIGHT) showMain(); + }, + }); + }; + /** + * Send message response, and delete it from list + * @param {string|boolean} reply Response text, false to dismiss (true to open on phone) + */ + const respondToMessage = function(reply) { + const msg = MESSAGES[messageNum]; + if (msg) { + Bangle.messageResponse(msg, reply); + if (reply===false) remove(msg); + } + if (MESSAGES.length<1) goBack(); // no more messages + else showMessage((msg && reply===false) ? messageNum : messageNum+1); // show next message + }; + const showMessageActions = function() { + let title = MESSAGES[messageNum].title || ""; + if (g.setFont(fontNormal).stringMetrics(title).width>Bangle.appRect.w-(B2 ? 0 : 20)) { + title = g.wrapString("..."+title, Bangle.appRect.w-(B2 ? 0 : 20))[0].substring(3)+"..."; + } + clearStuff(); + let grid = { + "": { + title: title ||/*LANG*/"Message", + back: () => showMessage(messageNum), + cols: 3, // fit all replies on first row, dismiss on bottom + } + }; + // Text replies don't work (yet) + // grid[/*LANG*/"OK"] = {icon: "Ok", col: "#0f0", cb: () => respondToMessage("\u{1F44D}")}; // "Thumbs up" emoji + // grid[/*LANG*/"Nak"] = {icon: "Nak", col: "#f00", cb: () => respondToMessage("\u{1F44E}")}; // "Thumbs down" emoji + // grid[/*LANG*/"No Phone"] = {icon: "NoPhone", col: "#f0f", cb: () => respondToMessage("\u{1F4F5}")}; // "No Mobile Phones" emoji + + grid[/*LANG*/"Dismiss"] = {icon: "Trash", col: "#ff0", cb: () => respondToMessage(false)}; + showGrid(grid); + }; + /** + * Show message + * + * @param {number} [num=0] Message to show + * @param {boolean} [bottom=false] Scroll message to bottom right away + */ + let buzzing = false, moving = false, switching = false; + let h, fh, offset; + + /** + * draw (sticky) footer + */ + const drawFooter = function() { + // left hint: swipe from left for main menu + g.reset().clearRect(Bangle.appRect.x, Bangle.appRect.y2-fh, Bangle.appRect.x2, Bangle.appRect.y2) + .setFont(fontTiny) + .setFontAlign(-1, 1) // bottom left + .drawString( + "\0"+atob("CAiBACBA/EIiAnwA")+ // back + "\0"+atob("CAiBAEgkEgkSJEgA"), // >> + Bangle.appRect.x+(B2 ? 1 : 28), Bangle.appRect.y2 + ); + // center message count+hints: swipe up/down for next/prev message + const footer = ` ${messageNum+1}/${MESSAGES.length} `, + fw = g.stringWidth(footer); + g.setFontAlign(0, 1); // bottom center + if (B2 && messageNum>0 && offset<=0) + g.drawString("\0"+atob("CAiBAABBIhRJIhQI"), Bangle.appRect.x+Bangle.appRect.w/2-fw/2, Bangle.appRect.y2); // ^ swipe to prev + g.drawString(footer, Bangle.appRect.x+Bangle.appRect.w/2, Bangle.appRect.y2); + if (B2 && messageNum=h-(Bangle.appRect.h-fh)) + g.drawString("\0"+atob("CAiBABAoRJIoRIIA"), Bangle.appRect.x+Bangle.appRect.w/2+fw/2, Bangle.appRect.y2); // v swipe to next + // right hint: swipe from right for message actions + g.setFontAlign(1, 1) // bottom right + .drawString( + "\0"+atob("CAiBABIkSJBIJBIA")+ // << + "\0"+atob("CAiBAP8AAP8AAP8A"), // = ("hamburger menu") + Bangle.appRect.x2-(B2 ? 1 : 28), Bangle.appRect.y2 + ); + }; + const showMessage = function(num, bottom) { + if (num<0) num = 0; + if (!num) num = 0; // no number: show first + if (num>=MESSAGES.length) num = MESSAGES.length-1; + setActive("messages", num); + if (!MESSAGES.length) { + // I /think/ this should never happen... + return E.showPrompt(/*LANG*/"No Messages", { + title:/*LANG*/"Messages", + img: require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")), + buttons: {/*LANG*/"Ok": 1} + }).then(showMain); + } + Bangle.setLocked(false); + Bangle.setLCDPower(1); + // only clear msg.new on user input + const msg = MESSAGES[messageNum]; // message + fh = 10; // footer height + offset = 0; + let oldOffset = 0; + const move = (dy) => { + offset = Math.max(0, Math.min(h-(Bangle.appRect.h-fh), offset+dy)); // clip at message height + dy = oldOffset-offset; // real dy + // move all elements to new offset + const offsetRecurser = function(l) { + if (l.y) l.y += dy; + if (l.c) l.c.forEach(offsetRecurser); + }; + offsetRecurser(layout.l); + oldOffset = offset; + draw(); + }; + const draw = () => { + g.reset() + .clearRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2-fh) + .setClipRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2-fh); + g.reset = () => g.setColor(g.theme.fg).setBgColor(g.theme.bg); // stop Layout resetting ClipRect + layout.render(); + if (layout.button && h>Bangle.appRect.h-fh && offset(Bangle.appRect.h-fh)) { + const sbh = (Bangle.appRect.h-fh)/h*(Bangle.appRect.h-fh), // scrollbar height + y1 = Bangle.appRect.y+offset/h*(Bangle.appRect.h-fh), y2 = y1+sbh; + g.setColor(g.theme.bg).drawLine(Bangle.appRect.x2, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y2-fh); + g.setColor(g.theme.fg).drawLine(Bangle.appRect.x2, y1, Bangle.appRect.x2, y2); + } + drawFooter(); + }; + const buzzOnce = () => { + if (buzzing) return; + buzzing = true; + Bangle.buzz(50).then(() => setTimeout(() => {buzzing = false;}, 500)); + }; + + layout = getMessageLayout(msg); + h = layout.l.h; // message height + if (bottom) move(h); // scrolling backwards: jump to bottom of message + else draw(); + const PAGE_SIZE = Bangle.appRect.h-fh; + const // shared B1/B2 handlers + back = () => { + delete msg.new; // we mark messages as read on any input + goBack(); + }, + swipe = dir => { + delete msg.new; + if (dir===RIGHT) showMain(); + else if (dir===LEFT) showMessageActions(); + }, + touch = (side, xy) => { + delete msg.new; + if (h<=Bangle.appRect.h-fh || offset>=h-(Bangle.appRect.h-fh)) { // already at bottom + // B2: check for button-press + // setUI overrides Layout listeners, so we need to check for button presses ourselves + if (B2 && layout.button) { + const b = layout.button; + // the button is at the bottom of the screen, so we accept touches all the way down + if (xy.x>=b.x && xy.y>=b.y && xy.x<=b.x+b.w /*&& xy.y<=b.y+b.h*/) return b.cb(); + } + if (B2 && xy.yBangle.appRect.h-fh && offset { + delete msg.new; + if (!switching) { + const dy = -e.dy; + if (dy>0) { // up + if (h>Bangle.appRect.h-fh && offset0) { + moving = true; // prevent scrolling right into prev message + move(dy); + } else if (messageNum>0) { // already at top: show prev + if (!moving) { // don't scroll right through to previous message + Bangle.buzz(30); + switching = true; // don't process any more drag events until we lift our finger + showMessage(messageNum-1, true); + } + } else { // already at top of first message + buzzOnce(); + } + } + } + if (!e.b) { + // touch end: we can swipe to another message (if we reached the top/bottom) or move the new message + moving = false; + switching = false; + } + }, + touch: touch, + }); + } else { // Bangle.js 1 + setUI({ + mode: "updown", + back: back, + }, dir => { + delete msg.new; + if (dir===DOWN) { + if (h>Bangle.appRect.h-fh && offset0) { + move(-PAGE_SIZE); + } else if (messageNum>0) { // top reached: show previous + Bangle.buzz(30); + showMessage(messageNum-1, true); + } else { + buzzOnce(); // already at top of first message + } + } else { // button + showMessageActions(); + } + }); + Bangle.swipeHandler = swipe; + Bangle.on("swipe", Bangle.swipeHandler); + Bangle.touchHandler = touch; + Bangle.on("touch", Bangle.touchHandler); + } // Bangle.js 1/2 + }; + /** + * Determine message layout information: size, fonts, and wrapped title/body texts + * + * @param msg + * @returns {{h: number, w: number, + * src: (string), + * title: (string), titleFont: (string), + * body: (string), bodyFont: (string)}} + */ + const getMessageLayoutInfo = function(msg) { + // header: [icon][title] + // [ src] + // + // But: no title? -> use src as title + let w, src = msg.src || "", + title = msg.title || "", + body = msg.body || "", + h = 0, // total height + th = 0, // title height + ih = 46; // icon height: // icon(24) + internal padding(20) + icon<->src spacer(2) + if (!title) { + title = src; + src = ""; + } + + // top bar + if (title) { + w = Bangle.appRect.w-59; // icon(24) + padding:left(5) + padding:btn-txt(5) + internal btn padding(20) + padding:right(5) + title = g.setFont(fontNormal).wrapString(title, w).join("\n"); + th += 2+g.stringMetrics(title).height; // 2px padding + } + if (src) { + w = 59; // icon(24) + padding:left(5) + padding:btn-txt(5) + internal btn padding(20) + padding:right(5) + src = g.setFont(fontTiny).wrapString(src, w).join("\n"); + ih += g.stringMetrics(src).height; + } + + h = Math.max(ih, th); // maximum of icon/title + + // body + w = Bangle.appRect.w-4; // padding(2x2) + body = g.setFont(fontNormal).wrapString(msg.body, w).join("\n"); + h += 4+g.stringMetrics(body).height; // padding(2x2) + + if (settings().button) h += 44; // icon(24) + padding(2x2) + internal btn padding(16) + + w = Bangle.appRect.w; + // always expand to -<(10x)footer> + h = Math.max(h, Bangle.appRect.h-10); + + return { + src: src, + title: title, + body: body, + h: h, + w: w, + }; + }; + + const getMessageLayout = function(msg) { + // Crafted so that on B2, with "medium" font, a message with + // icon + src + 2-line title + 2-line body + button + // fits exactly, i.e. no need for scrolling + const info = getMessageLayoutInfo(msg); + const hCol = msg.new ? g.theme.fgH : g.theme.fg2, + hBg = msg.new ? g.theme.bgH : g.theme.bg2; + + // lie to Layout library about available space + Bangle.appRect = Object.assign({}, Bangle.appRect, + {w: info.w, h: info.h, x2: Bangle.appRect.x+info.w-1, y2: Bangle.appRect.y+info.h-1}); + + // make sure icon is not invisible + let imageCol = getImageColor(msg); + if (g.setColor(imageCol).getColor()==hBg) imageCol = hCol; + + layout = new Layout({ + type: "v", c: [ + { + type: "h", fillx: 1, bgCol: hBg, col: hCol, c: [ + {width: 3}, + { + type: "v", c: [ + {type: "img", /*pad: 2,*/ src: () => getMessageImage(msg), col: imageCol}, + {height: 2}, + info.src ? {type: "txt", font: fontTiny, label: info.src, bgCol: hBg, col: hCol} : {}, + ] + }, + info.title ? {type: "txt", font: fontNormal, label: info.title, bgCol: hBg, col: hCol, fillx: 1, pad: 2} : {}, + {width: 3}, + ] + }, + {type: "txt", font: fontNormal, label: info.body, fillx: 1, filly: 1, pad: 2}, + {filly: 1}, + settings().button ? { + type: "h", c: [ + B2 ? {} : {fillx: 1}, // Bangle.js 1: touching right side = press button + {id: "button", type: "btn", pad: 2, src: () => getIcon("trash"), cb: () => respondToMessage(false)}, + ] + } : {}, + ] + }); + layout.update(); + delete Bangle.appRect; + return layout; + }; + + /** this is a timeout if the app has started and is showing a single message + but the user hasn't seen it (e.g. no user input) - in which case + we should start a timeout for settings().unreadTimeout to return + to the clock. */ + let unreadTimeout; + /** + * Stop auto-unload timeout and buzzing, remove listeners for this function + */ + const clearUnreadStuff = function() { + require("messages").stopBuzz(); + if (unreadTimeout) clearTimeout(unreadTimeout); + unreadTimeout = undefined; + ["touch", "drag", "swipe"].forEach(l => Bangle.removeListener(l, clearUnreadStuff)); + watches.forEach(w => clearWatch(w)); + watches = []; + }; + + let messageNum, // currently visible message + watches = [], // button watches + savedMusic = false; // did we find a stored "music" message when loading? +// special messages + let call, music, map, alarm; + /** + * Find special messages, and remove them from MESSAGES + */ + const findSpecials = function() { + let idx = MESSAGES.findIndex(m => m.id==="call"); + if (idx>=0) call = MESSAGES.splice(idx, 1)[0]; + idx = MESSAGES.findIndex(m => m.id==="music"); + if (idx>=0) { + music = MESSAGES.splice(idx, 1)[0]; + savedMusic = true; + } + idx = MESSAGES.findIndex(m => m.id==="map"); + if (idx>=0) map = MESSAGES.splice(idx, 1)[0]; + idx = MESSAGES.findIndex(m => m.src && m.src.toLowerCase().startsWith("alarm")); + if (idx>=0) alarm = MESSAGES.splice(idx, 1)[0]; + }; + if (MESSAGES!==undefined) { // only if loading MESSAGES worked + g.reset().clear(); + Bangle.loadWidgets(); + require("messages").toggleWidget(false); + Bangle.drawWidgets(); + findSpecials(); // sets global vars for special messages + // any message we asked to show? + const showIdx = MESSAGES.findIndex(m => m.show); + // any new text messages? + const newIdx = MESSAGES.findIndex(m => m.new); + + // figure out why the app was loaded + if (showIdx>=0) show(showIdx); + else if (call && call.new) showCall(); + else if (alarm && alarm.new) showAlarm(); + else if (map && map.new) showMap(); + else if (music && music.new && settings().openMusic) { + if (settings().alwaysShowMusic===undefined) { + // if not explicitly disabled, enable this the first time we see music + let s = settings(); + s.alwaysShowMusic = true; + require("Storage").writeJSON("messages.settings.json", s); + } + showMusic(); + } + // check for new message last: Maybe we already showed it, but timed out before + // if that happened, and we're loading for e.g. music now, we want to show the music screen + else if (newIdx>=0) { + showMessage(newIdx); + // auto-loaded for message(s): auto-close after timeout + let unreadTimeoutSecs = settings().unreadTimeout; + if (unreadTimeoutSecs===undefined) unreadTimeoutSecs = 60; + if (unreadTimeoutSecs) { + unreadTimeout = setTimeout(load, unreadTimeoutSecs*1000); + } + } else if (MESSAGES.length) { // not autoloaded, but we have messages to show + back = "main"; // prevent "back" from loading clock + showMessage(); + } else showMain(); + + // stop buzzing, auto-close timeout on input + ["touch", "drag", "swipe"].forEach(l => Bangle.on(l, clearUnreadStuff)); + (B2 ? [BTN1] : [BTN1, BTN2, BTN3]).forEach(b => watches.push(setWatch(clearUnreadStuff, b, false))); + } +} \ No newline at end of file diff --git a/apps/messagelist/app.png b/apps/messagelist/app.png new file mode 100644 index 000000000..6eae4bb96 Binary files /dev/null and b/apps/messagelist/app.png differ diff --git a/apps/messagelist/boot.js b/apps/messagelist/boot.js new file mode 100644 index 000000000..994a2cfed --- /dev/null +++ b/apps/messagelist/boot.js @@ -0,0 +1,3 @@ +(function() { + Bangle.on("message", require("messagegui").messageListener); +})(); \ No newline at end of file diff --git a/apps/messagelist/lib.js b/apps/messagelist/lib.js new file mode 100644 index 000000000..33b6d9d69 --- /dev/null +++ b/apps/messagelist/lib.js @@ -0,0 +1,246 @@ +// Handle incoming messages while the app is not loaded +// The messages app overrides Bangle.messageListener +// (placed in separate file, so we don't read this all at boot time) +exports.messageListener = function(type, msg) { + if (msg.handled || (global.__FILE__ && __FILE__.startsWith("messagelist."))) return; // already handled/app open + // clean up, in case previous message didn't load the app after all + if (exports.loadTimeout) clearTimeout(exports.loadTimeout); + delete exports.loadTimeout; + delete exports.buzz; + const quiet = () => (require("Storage").readJSON("setting.json", 1) || {}).quiet; + /** + * Quietly load the app for music/map, if not already loading + */ + function loadQuietly(msg) { + if (exports.loadTimeout) return; // already loading + exports.loadTimeout = setTimeout(function() { + Bangle.load("messagelist.app.js"); + }, 500); + } + function loadNormal(msg) { + if (exports.loadTimeout) clearTimeout(exports.loadTimeout); // restart timeout + exports.loadTimeout = setTimeout(function() { + delete exports.loadTimeout; + // check there are still new messages (for #1362) + let messages = require("messages").getMessages(msg); + (Bangle.MESSAGES || []).forEach(m => require("messages").apply(m, messages)); + if (!messages.some(m => m.new)) return; // don't use `status()`: also load for new music! + // if we're in a clock, or it's important, open app + if (Bangle.CLOCK || msg.important) { + if (exports.buzz) require("messages").buzz(msg.src); + Bangle.load("messagelist.app.js"); + } + }, 500); + } + + /** + * Mark message as handled, and save it for the app + */ + const handled = () => { + if (!Bangle.MESSAGES) Bangle.MESSAGES = []; + require("messages").apply(msg, Bangle.MESSAGES); + if (!Bangle.MESSAGES.length) delete Bangle.MESSAGES; + if (msg.t==="remove") require("messages").save(msg); + else msg.handled = true; + }; + /** + * Write messages to flash after all, when not laoding the app + */ + const saveToFlash = () => { + (Bangle.MESSAGES||[]).forEach(m=>require("messages").save(m)); + delete Bangle.MESSAGES; + } + + switch(type) { + case "music": + if (!Bangle.CLOCK) return; + // only load app if we are playing, and we know which song + if (msg.state!=="play" || !msg.title) return; + if (exports.openMusic===undefined) { + // only read settings for first music message + exports.openMusic = !!(exports.settings().openMusic); + } + if (!exports.openMusic) return; // we don't care about music + if (quiet()) return; + msg.new = true; + handled(); + return loadQuietly(); + + case "map": + handled(); + if (msg.t!=="remove" && Bangle.CLOCK) loadQuietly(); + else saveToFlash(); + return; + + case "text": + handled(); + if (exports.settings().autoOpen===false) return saveToFlash(); + if (quiet()) return saveToFlash(); + if (msg.t!=="add" || !msg.new || !(Bangle.CLOCK || msg.important)) { + // not important enough to load the app + if (msg.t==="add" && msg.new) require("messages").buzz(msg); + return saveToFlash(); + } + if (msg.t==="add" && msg.new) exports.buzz = true; + return loadNormal(msg); + + case "alarm": + if (quiet()<2) return saveToFlash(); + // fall through + case "call": + handled(); + exports.buzz = true; + return loadNormal(msg); + + // case "clearAll": do nothing + } +}; + +exports.settings = function() { + return Object.assign({ + // Interface // + fontSize: 1, + onTap: 0, // [Message menu, Dismiss, Back, Nothing] + button: true, + + // Behaviour // + vibrate: ":", + vibrateCalls: ":", + vibrateAlarms: ":", + repeat: 4, + vibrateTimeout: 60, + unreadTimeout: 60, + autoOpen: true, + + // Music // + openMusic: true, + // no default: alwaysShowMusic (auto-enabled by app when music happens) + showAlbum: true, + musicButtons: false, + + // Widget // + flash: true, + // showRead: false, + + // Utils // + }, + // fall back to default app settings if not set for messagelist + (require("Storage").readJSON("messages.settings.json", true) || {}), + (require("Storage").readJSON("messagelist.settings.json", true) || {})); +}; + +/** + * @param {string} icon Icon name + * @returns string Icon image string, for use with g.drawImage() + */ +exports.getIcon = function(icon) { + // TODO: icons should be 24x24px with 1bpp colors + switch(icon.toLowerCase()) { + // generic icons: + case "alert": + return atob("GBgBAAAAAP8AA//AD8PwHwD4HBg4ODwcODwccDwOcDwOYDwGYDwGYBgGYBgGcBgOcAAOOBgcODwcHDw4Hxj4D8PwA//AAP8AAAAA"); + case "alarm": + case "alarmclockreceiver": + return atob("GBjBAP////8AAAAAAAACAEAHAOAefng5/5wTgcgHAOAOGHAMGDAYGBgYGBgYGBgYGBgYDhgYBxgMATAOAHAHAOADgcAB/4AAfgAAAAAAAAA="); + case "back": // TODO: 22x22 + return atob("FhYBAAAAEAAAwAAHAAA//wH//wf//g///BwB+DAB4EAHwAAPAAA8AADwAAPAAB4AAHgAB+AH/wA/+AD/wAH8AA=="); + case "calendar": + return atob("GBiBAAAAAAAAAAAAAA//8B//+BgAGBgAGBgAGB//+B//+B//+B9m2B//+B//+Btm2B//+B//+Btm+B//+B//+A//8AAAAAAAAAAAAA=="); + case "mail": // TODO: 28x18 + case "sms message": + case "notification": + return atob("HBKBAD///8H///iP//8cf//j4//8f5//j/x/8//j/H//H4//4PB//EYj/44HH/Hw+P4//8fH//44///xH///g////A=="); + case "map": // TODO: 25x25, + return atob("GRmBAAAAAAAAAAAAAAIAYAHx/wH//+D/+fhz75w/P/4f//8P//uH///D///h3f/w4P+4eO/8PHZ+HJ/nDu//g///wH+HwAYAIAAAAAAAAAAAAAA="); + case "menu": + return atob("GBiBAAAAAAAAAAAAAAAAAP///////wAAAAAAAAAAAAAAAAAAAP///////wAAAAAAAAAAAAAAAAAAAP///////wAAAAAAAAAAAAAAAA=="); + case "music": // TODO: 22x22 + return atob("FhaBAH//+/////////////h/+AH/4Af/gB/+H3/7/f/v9/+/3/7+f/vB/w8H+Dwf4PD/x/////////////3//+A="); + case "nak": // TODO: 22x25 + return atob("FhmBAA//wH//j//+P//8///7///v//+///7//////////////v//////////z//+D8AAPwAAfgAB+AAD4AAPgAAeAAB4AAHAAA=="); + case "neg": // TODO: 22x22 + return atob("FhaBADAAMeAB78AP/4B/fwP4/h/B/P4D//AH/4AP/AAf4AB/gAP/AB/+AP/8B/P4P4fx/A/v4B//AD94AHjAAMA="); + case "next": + return atob("GBiBAAAAAAAAAAAAAAwAcB8A+B+A+B/g+B/4+B/8+B//+B//+B//+B//+B//+B//+B/8+B/4+B/g+B+A+B8A+AwAcAAAAAAAAAAAAA=="); + case "nophone": // TODO: 30x30 + return atob("Hh6BAAAAAAGAAAAHAAAADgAAABwADwA4Af8AcA/8AOB/+AHH/+ADv/8AB//wAA/HAAAeAAACOAAADHAAAHjgAAPhwAAfg4AAfgcAAfwOAA/wHAA/wDgA/gBwA/gA4AfAAcAfAAOAGAAHAAAADgAAABgAAAAA"); + case "ok": // TODO: 22x25 + return atob("FhmBAAHAAAeAAB4AAPgAA+AAH4AAfgAD8AAPwAD//+//////////////7//////////////v//+///7///v//8///gf/+A//wA=="); + case "pause": + return atob("GBiBAAAAAAAAAAAAAAOBwAfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AfD4AOBwAAAAAAAAAAAAA=="); + case "phone": // TODO: 23x23 + case "call": + return atob("FxeBABgAAPgAAfAAB/AAD+AAH+AAP8AAP4AAfgAA/AAA+AAA+AAA+AAB+AAB+AAB+OAB//AB//gB//gA//AA/8AAf4AAPAA="); + case "play": + return atob("GBiBAAAAAAAAAAAAAAcAAA+AAA/gAA/4AA/8AA//AA//wA//4A//8A//8A//4A//wA//AA/8AA/4AA/gAA+AAAcAAAAAAAAAAAAAAA=="); + case "pos": // TODO: 25x20 + return atob("GRSBAAAAAYAAAcAAAeAAAfAAAfAAAfAAAfAAAfAAAfBgAfA4AfAeAfAPgfAD4fAA+fAAP/AAD/AAA/AAAPAAADAAAA=="); + case "previous": + return atob("GBiBAAAAAAAAAAAAAA4AMB8A+B8B+B8H+B8f+B8/+B//+B//+B//+B//+B//+B//+B8/+B8f+B8H+B8B+B8A+A4AMAAAAAAAAAAAAA=="); + case "settings": // TODO: 20x20 + return atob("FBSBAAAAAA8AAPABzzgf/4H/+A//APnwfw/n4H5+B+fw/g+fAP/wH/+B//gc84APAADwAAAA"); + case "to do": + return atob("GBgBAAAAAAAAAAAwAAB4AAD8AAH+AAP/DAf/Hg//Px/+f7/8///4///wf//gP//AH/+AD/8AB/4AA/wAAfgAAPAAAGAAAAAAAAAA"); + case "trash": + return atob("GBiBAAAAAAAAAAB+AA//8A//8AYAYAYAYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAZmYAYAYAYAYAf/4AP/wAAAAAAAAA=="); + case "unknown": // TODO: 30x30 + return atob("Hh6BAAAAAAAAAAAAAAAAAAPwAAA/8AAB/+AAD//AAD4fAAHwPgAHwPgAAAPgAAAfAAAA/AAAD+AAAH8AAAHwAAAPgAAAPgAAAPgAAAAAAAAAAAAAAAAAAHAAAAPgAAAPgAAAPgAAAHAAAAAAAAAAAAAAAAAA"); + case "unread": // TODO: 29x24 + return atob("HRiBAAAAH4AAAf4AAB/4AAHz4AAfn4AA/Pz/5+fj/z8/j/n5/j/P//j/Pn3j+PPPx+P8fx+Pw/x+AF/B4A78RiP3xwOPvHw+Pcf/+Ox//+NH//+If//+B///+A=="); + default: //should never happen + return exports.getIcon("unknown"); + } +}; +/** + * @param {string} icon Icon + * @returns {string} Color to use with g.setColor() + */ +exports.getColor = function(icon) { + switch(icon.toLowerCase()) { + // generic colors, using B2-safe colors + case "alert": + return "#ff0"; + case "alarm": + return "#fff"; + case "calendar": + return "#f00"; + case "mail": + return "#ff0"; + case "map": + return "#f0f"; + case "music": + return "#f0f"; + case "neg": + return "#f00"; + case "notification": + return "#0ff"; + case "phone": + case "call": + return "#0f0"; + case "settings": + return "#000"; + case "sms message": + return "#0ff"; + case "trash": + return "#f00"; + case "unknown": + return g.theme.fg; + case "unread": + return "#ff0"; + default: + return g.theme.fg; + } +}; + +/** + * Launch GUI app with given message + * @param {object} msg + */ +exports.open = function(msg) { + if (msg && msg.id && !msg.show) { + // store which message to load + msg.show = 1; + } + + Bangle.load((msg && msg.new && msg.id!=="music") ? "messagelist.new.js" : "messagelist.app.js"); +}; diff --git a/apps/messagelist/metadata.json b/apps/messagelist/metadata.json new file mode 100644 index 000000000..7947e2db4 --- /dev/null +++ b/apps/messagelist/metadata.json @@ -0,0 +1,28 @@ +{ + "id": "messagelist", + "name": "Message List", + "version": "0.01", + "description": "Display notifications from iOS and Gadgetbridge/Android as a list", + "icon": "app.png", + "type": "app", + "tags": "tool,system", + "screenshots": [ + {"url": "screenshot0.png"}, + {"url": "screenshot1.png"}, + {"url": "screenshot2.png"}, + {"url": "screenshot3.png"} + ], + "supports": ["BANGLEJS","BANGLEJS2"], + "dependencies" : { "messageicons":"module" }, + "provides_modules": ["messagegui"], + "readme": "README.md", + "storage": [ + {"name":"messagelist.boot.js","url":"boot.js"}, + {"name":"messagegui","url":"lib.js"}, + {"name":"messagelist.app.js","url":"app.js"}, + {"name":"messagelist.settings.js","url":"settings.js"}, + {"name":"messagelist.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"messagelist.settings.json"}], + "sortorder": -9 +} diff --git a/apps/messagelist/screenshot0.png b/apps/messagelist/screenshot0.png new file mode 100644 index 000000000..b6f37c053 Binary files /dev/null and b/apps/messagelist/screenshot0.png differ diff --git a/apps/messagelist/screenshot1.png b/apps/messagelist/screenshot1.png new file mode 100644 index 000000000..f4d4db9fa Binary files /dev/null and b/apps/messagelist/screenshot1.png differ diff --git a/apps/messagelist/screenshot2.png b/apps/messagelist/screenshot2.png new file mode 100644 index 000000000..67c192a1c Binary files /dev/null and b/apps/messagelist/screenshot2.png differ diff --git a/apps/messagelist/screenshot3.png b/apps/messagelist/screenshot3.png new file mode 100644 index 000000000..02fed81a7 Binary files /dev/null and b/apps/messagelist/screenshot3.png differ diff --git a/apps/messagelist/settings.js b/apps/messagelist/settings.js new file mode 100644 index 000000000..cd2767336 --- /dev/null +++ b/apps/messagelist/settings.js @@ -0,0 +1,139 @@ +(function(back) { + let settings = require("messagegui").settings(); + const inApp = (global.__FILE__ && __FILE__.startsWith("messagelist.")); + + function updateSetting(setting, value) { + settings[setting] = value; + let file; + switch(setting) { + case "flash": + case "showRead": + case "iconColorMode": + case "maxMessages": + case "maxUnreadTimeout": + case "openMusic": + case "repeat": + case "unlockWatch": + case "unreadTimeout": + case "vibrate": + case "vibrateCalls": + case "vibrateTimeout": + // Default app has this setting: update that file + file = "messages"; + break; + default: + // write to our own settings file + file = "messagelist"; + } + file += ".settings.json"; + let saved = require("Storage").readJSON(file, true) || {}; + saved[setting] = value; + require("Storage").writeJSON(file, saved); + } + + function toggler(setting) { + return { + value: !!settings[setting], + onchange: v => updateSetting(setting, v) + }; + } + + function showIfMenu() { + const tapOptions = [/*LANG*/"Message menu",/*LANG*/"Dismiss",/*LANG*/"Back",/*LANG*/"Nothing"]; + E.showMenu({ + "": {"title": /*LANG*/"Interface"}, + "< Back": () => showMainMenu(), + /*LANG*/"Font size": { + value: 0|settings.fontSize, + min: 0, max: 2, + format: v => [/*LANG*/"Small",/*LANG*/"Medium",/*LANG*/"Large",/*LANG*/"Huge"][v], + onchange: v => updateSetting("fontSize", v) + }, + /*LANG*/"On Tap": { + value: settings.onTap, + min: 0, max: tapOptions.length-1, wrap: true, + format: v => tapOptions[v], + onchange: v => updateSetting("onTap", v) + }, + /*LANG*/"Dismiss button": toggler("button"), + }); + } + + function showBMenu() { + E.showMenu({ + "": {"title": /*LANG*/"Behaviour"}, + "< Back": () => showMainMenu(), + /*LANG*/"Vibrate": require("buzz_menu").pattern(settings.vibrate, v => updateSetting("vibrate", v)), + /*LANG*/"Vibrate for calls": require("buzz_menu").pattern(settings.vibrateCalls, v => updateSetting("vibrateCalls", v)), + /*LANG*/"Vibrate for alarms": require("buzz_menu").pattern(settings.vibrateAlarms, v => updateSetting("vibrateAlarms", v)), + /*LANG*/"Repeat": { + value: settings.repeat, + min: 0, max: 10, + format: v => v ? v+"s" :/*LANG*/"Off", + onchange: v => updateSetting("repeat", v) + }, + /*LANG*/"Vibrate timer": { + value: settings.vibrateTimeout, + min: 0, max: 240, step: 10, + format: v => v ? v+"s" :/*LANG*/"Forever", + onchange: v => updateSetting("vibrateTimeout", v) + }, + /*LANG*/"Unread timer": { + value: settings.unreadTimeout, + min: 0, max: 240, step: 10, + format: v => v ? v+"s" :/*LANG*/"Off", + onchange: v => updateSetting("unreadTimeout", v) + }, + /*LANG*/"Auto-open": toggler("autoOpen"), + }); + } + + function showMusicMenu() { + E.showMenu({ + "": {"title": /*LANG*/"Music"}, + "< Back": () => showMainMenu(), + /*LANG*/"Auto-open": toggler("openMusic"), + /*LANG*/"Always visible": toggler("alwaysShowMusic"), + /*LANG*/"Buttons": toggler("musicButtons"), + /*LANG*/"Show album": toggler("showAlbum"), + }); + } + + function showWidMenu() { + E.showMenu({ + "": {"title": /*LANG*/"Widget"}, + "< Back": () => showMainMenu(), + /*LANG*/"Flash icon": toggler("flash"), + // /*LANG*/"Show Read": toggler("showRead"), + }); + } + + function showUtilsMenu() { + let m = E.showMenu({ + "": {"title": /*LANG*/"Utilities"}, + "< Back": () => showMainMenu(), + /*LANG*/"Delete all": () => { + E.showPrompt(/*LANG*/"Are you sure?", + {title:/*LANG*/"Delete All Messages"}) + .then(isYes => { + if (isYes) require("messages").write([]); + showUtilsMenu(); + }); + } + }); + } + + function showMainMenu() { + E.showMenu({ + "": {"title": inApp ?/*LANG*/"Settings" :/*LANG*/"Messages"}, + "< Back": back, + /*LANG*/"Interface": () => showIfMenu(), + /*LANG*/"Behaviour": () => showBMenu(), + /*LANG*/"Music": () => showMusicMenu(), + /*LANG*/"Widget": () => showWidMenu(), + /*LANG*/"Utils": () => showUtilsMenu(), + }); + } + + showMainMenu(); +}); diff --git a/apps/messages/ChangeLog b/apps/messages/ChangeLog index ee27d41c6..7d3414c1a 100644 --- a/apps/messages/ChangeLog +++ b/apps/messages/ChangeLog @@ -1,77 +1,4 @@ -0.01: New App! -0.02: Add 'messages' library -0.03: Fixes for Bangle.js 1 -0.04: Add require("messages").clearAll() -0.05: Handling of message actions (ok/clear) -0.06: New messages now go at the start (fix #898) - Answering true/false now exits the messages app if no new messages - Back now marks a message as read - Clicking top-left opens a menu which allows you to delete a message or mark unread -0.07: Added settings menu with option to choose vibrate pattern and frequency (fix #909) -0.08: Fix rendering of long messages (fix #969) - buzz on new message (fix #999) -0.09: Message now disappears after 60s if no action taken and clock loads (fix 922) - Fix phone icon (#1014) -0.10: Respect the 'new' attribute if it was set from iOS integrations -0.11: Open app when touching the widget (Bangle.js 2 only) -0.12: Extra app-specific notification icons - New animated notification icon (instead of large blinking 'MESSAGES') - Added screenshots -0.13: Add /*LANG*/ comments for internationalisation - Add 'Delete All' option to message options - Now update correctly when 'require("messages").clearAll()' is called -0.14: Hide widget when all unread notifications are dismissed from phone -0.15: Don't buzz when Quiet Mode is active -0.16: Fix text wrapping so it fits the screen even if title is big (fix #1147) -0.17: Fix: Get dynamic dimensions of notify icon, fixed notification font -0.18: Use app-specific icon colors - Spread message action buttons out - Back button now goes back to list of messages - If showMessage called with no message (eg all messages deleted) now return to the clock (fix #1267) -0.19: Use a larger font for message text if it'll fit -0.20: Allow tapping on the body to show a scrollable view of the message and title in a bigger font (fix #1405, #1031) -0.21: Improve list readability on dark theme -0.22: Add Home Assistant icon - Allow repeat to be switched Off, so there is no buzzing repetition. - Also gave the widget a pixel more room to the right -0.23: Change message colors to match current theme instead of using green - Now attempt to use Large/Big/Medium fonts, and allow minimum font size to be configured -0.24: Remove left-over debug statement -0.25: Fix widget memory usage issues if message received and watch repeatedly calls Bangle.drawWidgets (fix #1550) -0.26: Setting to auto-open music -0.27: Add 'mark all read' option to popup menu (fix #1624) -0.28: Option to auto-unlock the watch when a new message arrives -0.29: Fix message list overwrites on Bangle.js 1 (fix #1642) -0.30: Add new Icons (Youtube, Twitch, MS TODO, Teams, Snapchat, Signal, Post & DHL, Nina, Lieferando, Kalender, Discord, Corona Warn, Bibel) -0.31: Option to disable icon flashing -0.32: Added an option to allow quiet mode to override message auto-open -0.33: Timeout from the message list screen if the message being displayed is removed and there is a timer going -0.34: Don't buzz for 'map' update messages -0.35: Reset graphics colors before rendering a message (possibly fix #1752) -0.36: Ensure a new message plus an almost immediate deletion of that message doesn't load the messages app (fix #1362) -0.37: Now use the setUI 'back' icon in the top left rather than specific buttons/menu items -0.38: Add telegram foss handling -0.39: Set default color for message icons according to theme -0.40: Use default Bangle formatter for booleans -0.41: Add notification icons in the widget -0.42: Fix messages ignoring "Vibrate: Off" setting -0.43: Add new Icons (Airbnb, warnwetter) -0.44: Separate buzz pattern for incoming calls -0.45: Added new app colors and icons -0.46: Add 'Vibrate Timer' option to set how long to vibrate for, and fix Repeat:off - Fix message removal from widget bar (previously caused exception as .hide has been removed) -0.47: Add new Icons (Nextbike, Mattermost, etc.) -0.48: When getting new message from the clock, only buzz once the messages app is loaded -0.49: Change messages icon (to fit within 24px) and ensure widget renders icons centrally -0.50: Add `getMessages` and `status` functions to library - Option to disable auto-open of messages - Option to make message icons monochrome (not colored) - messages widget buzz now returns a promise -0.51: Emit "message events" - Setting to hide widget - Add custom event handlers to prevent default app form loading - Move WIDGETS.messages.buzz() to require("messages").buzz() -0.52: Fix require("messages").buzz() regression - Fix background color in messages list after one unread message is shown -0.53: Messages now uses Bangle.load() to load messages app faster (if possible) -0.54: Move icons out to messageicons module +0.55: Moved messages library into standalone library +0.56: Fix handling of music messages +0.57: Optimize saving empty message list +0.58: show/hide "messages" widget directly, instead of through library stub diff --git a/apps/messages/README.md b/apps/messages/README.md index 72a989146..83524d7c8 100644 --- a/apps/messages/README.md +++ b/apps/messages/README.md @@ -1,62 +1,26 @@ -# Messages app +# Messages library -This app handles the display of messages and message notifications. It stores -a list of currently received messages and allows them to be listed, viewed, -and responded to. +This library handles the passing of messages. It can storess a list of messages +and allows them to be retrieved by other apps. -It is a replacement for the old `notify`/`gadgetbridge` apps. +## Example -## Settings +Assuming you are using GadgetBridge and "overlay notifications": -You can change settings by going to the global `Settings` app, then `App Settings` -and `Messages`: - -* `Vibrate` - This is the pattern of buzzes that should be made when a new message is received -* `Vibrate for calls` - This is the pattern of buzzes that should be made when an incoming call is received -* `Repeat` - How often should buzzes repeat - the default of 4 means the Bangle will buzz every 4 seconds -* `Vibrate Timer` - When a new message is received when in a non-clock app, we display the message icon and -buzz every `Repeat` seconds. This is how long we continue to do that. -* `Unread Timer` - When a new message is received when showing the clock we go into the Messages app. -If there is no user input for this amount of time then the app will exit and return -to the clock where a ringing bell will be shown in the Widget bar. -* `Min Font` - The minimum font size used when displaying messages on the screen. A bigger font -is chosen if there isn't much message text, but this specifies the smallest the font should get before -it starts getting clipped. -* `Auto-Open Music` - Should the app automatically open when the phone starts playing music? -* `Unlock Watch` - Should the app unlock the watch when a new message arrives, so you can touch the buttons at the bottom of the app? -* `Flash Icon` - Toggle flashing of the widget icon. -* `Widget messages` - The maximum amount of message icons to show on the widget, or `Hide` the widget completely. - -## New Messages - -When a new message is received: - -* If you're in an app, the Bangle will buzz and a message icon appears in the Widget bar. You can tap this icon to view the message. -* If you're in a clock, the Messages app will automatically start and show the message - -When a message is shown, you'll see a screen showing the message title and text. - -* The 'back-arrow' button (or physical button on Bangle.js 2) goes back to Messages, marking the current message as read. -* The top-left icon shows more options, for instance deleting the message of marking unread -* On Bangle.js 2 you can tap on the message body to view a scrollable version of the title and text (or can use the top-left icon + `View Message`) -* If shown, the 'tick' button: - * **Android** opens the notification on the phone - * **iOS** responds positively to the notification (accept call/etc) -* If shown, the 'cross' button: - * **Android** dismisses the notification on the phone - * **iOS** responds negatively to the notification (dismiss call/etc) - -## Images -_1. Screenshot of a notification_ - -![](screenshot.png) - -_2. What the notify icon looks like (it's touchable on Bangle.js2!)_ - -![](screenshot-notify.gif) +1. Gadgetbridge sends an event to your watch for an incoming message +2. The `android` app parses the message, and calls `require("messages").pushMessage({/** the message */})` +3. `require("messages")` calls `Bangle.emit("message", "text", {/** the message */})` +4. Overlay Notifications shows the message in an overlay, and marks it as `handled` +5. The default UI app (Message UI, `messagegui`) sees the event is marked as `handled`, so does nothing. +6. The default widget (`widmessages`) does nothing with `handled`, and shows a notification icon. +7. You tap the notification, in order to open the full GUI: Overlay Notifications + calls `require("messages").openGUI({/** the message */})` +8. `openGUI` calls `require("messagegui").open(/** copy of the message */)`. +9. The `messagegui` library loads the Message UI app. -## Events (for app/widget developers) + +## Events When a new message arrives, a `"message"` event is emitted, you can listen for it like this: @@ -64,8 +28,7 @@ it like this: ```js myMessageListener = Bangle.on("message", (type, message)=>{ if (message.handled) return; // another app already handled this message - // is one of "text", "call", "alarm", "map", "music", or "clearAll" - if (type === "clearAll") return; // not a message + // is one of "text", "call", "alarm", "map", or "music" // see `messages/lib.js` for possible formats // message.t could be "add", "modify" or "remove" E.showMessage(`${message.title}\n${message.body}`, `${message.t} ${type} message`); @@ -74,10 +37,23 @@ myMessageListener = Bangle.on("message", (type, message)=>{ }); ``` +Apps can launch the full GUI by calling `require("messages").openGUI()`, if you +want to write your own GUI, it should include boot code that listens for +`"messageGUI"` events: + +```js +Bangle.on("messageGUI", message=>{ + if (message.handled) return; // another app already opened it's GUI + message.handled = true; // prevent other apps form launching + Bangle.load("my_message_gui.app.js"); +}) + +``` + ## Requests -Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=messages%20app +Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=[messages]%20library ## Creator diff --git a/apps/messages/TEST_ME.txt b/apps/messages/TEST_ME.txt new file mode 100644 index 000000000..8ce50d8b6 --- /dev/null +++ b/apps/messages/TEST_ME.txt @@ -0,0 +1,7 @@ +We need automated tests for this. Specifically: + + +* send notification in clock with fast load -> messagesgui appears +* send notification in clock without fast load -> messagesgui appears +* send notification and delete notification quick -> messagesgui doesn't load +* music? diff --git a/apps/messages/lib.js b/apps/messages/lib.js index 0ed03e4b6..f301a91cd 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -1,10 +1,15 @@ -function openMusic() { - // only read settings file for first music message - if ("undefined"==typeof exports._openMusic) { - exports._openMusic = !!((require('Storage').readJSON("messages.settings.json", true) || {}).openMusic); - } - return exports._openMusic; +exports.music = {}; +/** + * Emit "message" event with appropriate type from Bangle + * @param {object} msg + */ +function emit(msg) { + let type = "text"; + if (["call", "music", "map"].includes(msg.id)) type = msg.id; + if (msg.src && msg.src.toLowerCase().startsWith("alarm")) type = "alarm"; + Bangle.emit("message", type, msg); } + /* Push a new message onto messages queue, event is: {t:"add",id:int, src,title,subject,body,sender,tel, important:bool, new:bool} {t:"add",id:int, id:"music", state, artist, track, etc} // add new @@ -12,125 +17,180 @@ function openMusic() { {t:"modify",id:int, title:string} // modified */ exports.pushMessage = function(event) { - var messages = exports.getMessages(); // now modify/delete as appropriate - var mIdx = messages.findIndex(m=>m.id==event.id); - if (event.t=="remove") { - if (mIdx>=0) messages.splice(mIdx, 1); // remove item - mIdx=-1; + if (event.t==="remove") { + if (event.id==="music") exports.music = {}; } else { // add/modify - if (event.t=="add"){ - if(event.new === undefined ) { // If 'new' has not been set yet, set it - event.new=true; // Assume it should be new - } + if (event.t==="add") { + if (event.new===undefined) event.new = true; // Assume it should be new + } else if (event.t==="modify") { + const old = exports.getMessages().find(m => m.id===event.id); + if (old) event = Object.assign(old, event); } - if (mIdx<0) { - mIdx=0; - messages.unshift(event); // add new messages to the beginning - } - else Object.assign(messages[mIdx], event); - if (event.id=="music" && messages[mIdx].state=="play") { - messages[mIdx].new = true; // new track, or playback (re)started - type = 'music'; + + // combine musicinfo and musicstate events + if (event.id==="music") { + if (event.state==="play") event.new = true; // new track, or playback (re)started + event = Object.assign(exports.music, event); } } - require("Storage").writeJSON("messages.json",messages); - var message = mIdx<0 ? {id:event.id, t:'remove'} : messages[mIdx]; - // if in app, process immediately - if ("undefined"!=typeof MESSAGES) return onMessagesModified(message); - // emit message event - var type = 'text'; - if (["call", "music", "map"].includes(message.id)) type = message.id; - if (message.src && message.src.toLowerCase().startsWith("alarm")) type = "alarm"; - Bangle.emit("message", type, message); - // update the widget icons shown - if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages,true); - var handleMessage = () => { - // if no new messages now, make sure we don't load the messages app - if (event.t=="remove" && exports.messageTimeout && !messages.some(m => m.new)) { - clearTimeout(exports.messageTimeout); - delete exports.messageTimeout; - } - // ok, saved now - if (event.id=="music" && Bangle.CLOCK && messages[mIdx].new && openMusic()) { - // just load the app to display music: no buzzing - Bangle.load("messages.app.js"); - } else if (event.t!="add") { - // we only care if it's new - return; - } else if (event.new==false) { - return; - } - // otherwise load messages/show widget - var loadMessages = Bangle.CLOCK || event.important; - var quiet = (require('Storage').readJSON('setting.json', 1) || {}).quiet; - var appSettings = require('Storage').readJSON('messages.settings.json', 1) || {}; - var unlockWatch = appSettings.unlockWatch; - // don't auto-open messages in quiet mode if quietNoAutOpn is true - if ((quiet && appSettings.quietNoAutOpn) || appSettings.noAutOpn) - loadMessages = false; - delete appSettings; - // after a delay load the app, to ensure we have all the messages - if (exports.messageTimeout) clearTimeout(exports.messageTimeout); - exports.messageTimeout = setTimeout(function() { - exports.messageTimeout = undefined; - // if we're in a clock or it's important, go straight to messages app - if (loadMessages) { - if (!quiet && unlockWatch) { - Bangle.setLocked(false); - Bangle.setLCDPower(1); // turn screen on - } - // we will buzz when we enter the messages app - return Bangle.load("messages.new.js"); - } - if (global.WIDGETS && WIDGETS.messages) WIDGETS.messages.update(messages); - exports.buzz(message.src); - }, 500); - }; - setTimeout(()=>{ - if (!message.handled) handleMessage(); - },0); -} -/// Remove all messages + // reset state (just in case) + delete event.handled; + delete event.saved; + emit(event); +}; + +/** + * Save a single message to flash + * Also sets msg.saved=true + * + * @param {object} msg + * @param {object} [options={}] Options: + * {boolean} [force=false] Force save even if msg.saved is already set + */ +exports.save = function(msg, options) { + if (!options) options = {}; + if (msg.saved && !options.force) return; //already saved + let messages = exports.getMessages(); + exports.apply(msg, messages); + exports.write(messages); + msg.saved = true; +}; + +/** + * Apply incoming event to array of messages + * + * @param {object} event Event to apply + * @param {array} messages Array of messages, *will be modified in-place* + * @return {array} Modified messages array + */ +exports.apply = function(event, messages) { + if (!event || !event.id) return messages; + const mIdx = messages.findIndex(m => m.id===event.id); + if (event.t==="remove") { + if (mIdx<0) return messages; // already gone -> nothing to do + messages.splice(mIdx, 1); + } else if (event.t==="add") { + if (mIdx>=0) messages.splice(mIdx, 1); // duplicate ID! erase previous version + messages.unshift(event); // add at the beginning + } else if (event.t==="modify") { + if (mIdx>=0) messages[mIdx] = Object.assign(messages[mIdx], event); + else messages.unshift(event); + } + return messages; +}; + +/** + * Accept a call (or other acceptable event) + * @param {object} msg + */ +exports.accept = function(msg) { + if (msg.positive) Bangle.messageResponse(msg, true); +}; + +/** + * Dismiss a message (if applicable), and erase it from flash + * Emits a "message" event with t="remove", only if message existed + * + * @param {object} msg + */ +exports.dismiss = function(msg) { + if (msg.negative) Bangle.messageResponse(msg, false); + let messages = exports.getMessages(); + const mIdx = messages.findIndex(m=>m.id===msg.id); + if (mIdx<0) return; + messages.splice(mIdx, 1); + exports.write(messages); + if (msg.t==="remove") return; // already removed, don't re-emit + msg.t = "remove"; + emit(msg); // emit t="remove", so e.g. widgets know to update +}; + +/** + * Emit a "type=openGUI" event, to open GUI app + * + * @param {object} [msg={}] Message the app should show + */ +exports.openGUI = function(msg) { + if (!require("Storage").read("messagegui")) return; // "messagegui" module is missing! + // Mark the event as unhandled for GUI, but leave passed arguments intact + let copy = Object.assign({}, msg); + delete copy.handled; + require("messagegui").open(copy); +}; + +/** + * Show/hide the messages widget + * + * @param {boolean} show + */ +exports.toggleWidget = function(show) { + if (!global.WIDGETS || !WIDGETS["messages"]) return; // widget is missing! + const method = WIDGETS["messages"][show ? "show" : "hide"]; + /* if (typeof(method)!=="function") return; // widget must always have show()+hide(), fail hard rather than hide problems */ + method.apply(WIDGETS["messages"]); +}; + +/** + * Replace all stored messages + * @param {array} messages Messages to save + */ +exports.write = function(messages) { + if (!messages.length) require("Storage").erase("messages.json"); + else require("Storage").writeJSON("messages.json", messages.map(m => { + // we never want to save saved/handled status to file; + delete m.saved; + delete m.handled; + return m; + })); +}; +/** + * Erase all messages + */ exports.clearAll = function() { - if ("undefined"!= typeof MESSAGES) { // we're in a messages app, clear that as well - MESSAGES = []; - } - // Clear all messages - require("Storage").writeJSON("messages.json", []); - // if we have a widget, update it - if (global.WIDGETS && WIDGETS.messages) - WIDGETS.messages.update([]); - // let message listeners know - Bangle.emit("message", "clearAll", {}); // guarantee listeners an object as `message` - // clearAll cannot be marked as "handled" - // update app if in app - if ("function"== typeof onMessagesModified) onMessagesModified(); + exports.write([]); + Bangle.emit("message", "clearAll", {}); } /** + * Get saved messages + * + * Optionally pass in a message to apply to the list, this is for event handlers: + * By passing the message from the event, you can make sure the list is up-to-date, + * even if the message has not been saved (yet) + * + * Example: + * Bangle.on("message", (type, msg) => { + * console.log("All messages:", require("messages").getMessages(msg)); + * }); + * + * @param {object} [withMessage] Apply this event to messages * @returns {array} All messages */ -exports.getMessages = function() { - if ("undefined"!=typeof MESSAGES) return MESSAGES; // loaded/managed by app - return require("Storage").readJSON("messages.json",1)||[]; -} +exports.getMessages = function(withMessage) { + let messages = require("Storage").readJSON("messages.json", true); + messages = Array.isArray(messages) ? messages : []; // make sure we always return an array + if (withMessage && withMessage.id) exports.apply(withMessage, messages); + return messages; +}; /** * Check if there are any messages + * + * @param {object} [withMessage] Apply this event to messages, see getMessages * @returns {string} "new"/"old"/"none" */ - exports.status = function() { +exports.status = function(withMessage) { try { - let status= "none"; - for(const m of exports.getMessages()) { + let status = "none"; + for(const m of exports.getMessages(withMessage)) { if (["music", "map"].includes(m.id)) continue; if (m.new) return "new"; status = "old"; } return status; } catch(e) { - return "none"; // don't bother e.g. the widget with errors + return "none"; // don't bother callers with errors } }; @@ -141,24 +201,24 @@ exports.getMessages = function() { */ exports.buzz = function(msgSrc) { exports.stopBuzz(); // cancel any previous buzz timeouts - if ((require('Storage').readJSON('setting.json',1)||{}).quiet) return Promise.resolve(); // never buzz during Quiet Mode - var msgSettings = require('Storage').readJSON("messages.settings.json", true) || {}; - var pattern; - if (msgSrc && msgSrc.toLowerCase() === "phone") { + if ((require("Storage").readJSON("setting.json", 1) || {}).quiet) return Promise.resolve(); // never buzz during Quiet Mode + const msgSettings = require("Storage").readJSON("messages.settings.json", true) || {}; + let pattern; + if (msgSrc && msgSrc.toLowerCase()==="phone") { // special vibration pattern for incoming calls pattern = msgSettings.vibrateCalls; } else { pattern = msgSettings.vibrate; } - if (pattern === undefined) { pattern = ":"; } // pattern may be "", so we can't use || ":" here + if (pattern===undefined) { pattern = ":"; } // pattern may be "", so we can't use || ":" here if (!pattern) return Promise.resolve(); - var repeat = msgSettings.repeat; - if (repeat===undefined) repeat=4; // repeat may be zero + let repeat = msgSettings.repeat; + if (repeat===undefined) repeat = 4; // repeat may be zero if (repeat) { - exports.buzzTimeout = setTimeout(()=>require("buzz").pattern(pattern), repeat*1000); - var vibrateTimeout = msgSettings.vibrateTimeout; - if (vibrateTimeout===undefined) vibrateTimeout=60; + exports.buzzTimeout = setTimeout(() => require("buzz").pattern(pattern), repeat*1000); + let vibrateTimeout = msgSettings.vibrateTimeout; + if (vibrateTimeout===undefined) vibrateTimeout = 60; if (vibrateTimeout && !exports.stopTimeout) exports.stopTimeout = setTimeout(exports.stopBuzz, vibrateTimeout*1000); } return require("buzz").pattern(pattern); diff --git a/apps/messages/metadata.json b/apps/messages/metadata.json index f3051958e..9c7c8b49e 100644 --- a/apps/messages/metadata.json +++ b/apps/messages/metadata.json @@ -1,23 +1,22 @@ { "id": "messages", "name": "Messages", - "version": "0.54", - "description": "App to display notifications from iOS and Gadgetbridge/Android", + "version": "0.58", + "description": "Library to handle, load and store message events received from Android/iOS", "icon": "app.png", - "type": "app", + "type": "module", "tags": "tool,system", "supports": ["BANGLEJS","BANGLEJS2"], - "dependencies" : { "messageicons":"module" }, + "provides_modules" : ["messages"], + "dependencies" : { + "messagegui":"module", + "message":"widget" + }, + "default": true, "readme": "README.md", "storage": [ - {"name":"messages.app.js","url":"app.js"}, - {"name":"messages.new.js","url":"app-newmessage.js"}, - {"name":"messages.settings.js","url":"settings.js"}, - {"name":"messages.img","url":"app-icon.js","evaluate":true}, - {"name":"messages.wid.js","url":"widget.js"}, - {"name":"messages","url":"lib.js"} + {"name":"messages","url":"lib.js"}, + {"name":"messages.settings.js","url":"settings.js"} ], - "data": [{"name":"messages.json"},{"name":"messages.settings.json"}], - "screenshots": [{"url":"screenshot.png"},{"url":"screenshot-notify.gif"}], - "sortorder": -9 + "data": [{"name":"messages.json"},{"name":"messages.settings.json"}] } diff --git a/apps/messages/widget.js b/apps/messages/widget.js deleted file mode 100644 index c0dcd132f..000000000 --- a/apps/messages/widget.js +++ /dev/null @@ -1,56 +0,0 @@ -(() => { -if ((require('Storage').readJSON("messages.settings.json", true) || {}).maxMessages===0) return; - -function filterMessages(msgs) { - return msgs.filter(msg => msg.new && msg.id != "music") - .map(m => m.src) // we only need this for icon/color - .filter((msg, i, arr) => arr.findIndex(nmsg => msg.src == nmsg.src) == i); -} - -WIDGETS["messages"]={area:"tl", width:0, draw:function(recall) { - // If we had a setTimeout queued from the last time we were called, remove it - if (WIDGETS["messages"].i) { - clearTimeout(WIDGETS["messages"].i); - delete WIDGETS["messages"].i; - } - Bangle.removeListener('touch', this.touch); - if (!this.width) return; - let settings = Object.assign({flash:true, maxMessages:3},require('Storage').readJSON("messages.settings.json", true) || {}); - if (recall !== true || settings.flash) { - var msgsShown = E.clip(this.msgs.length, 0, settings.maxMessages); - g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+23); - for(let i = 0;i < msgsShown;i++) { - const msg = this.msgs[i]; - const colors = [g.theme.bg, - require("messageicons").getColor(msg, {settings:settings})]; - if (settings.flash && ((Date.now()/1000)&1)) { - if (colors[1] == g.theme.fg) { - colors.reverse(); - } else { - colors[1] = g.theme.fg; - } - } - g.setColor(colors[1]).setBgColor(colors[0]); - // draw the icon, or '...' if too many messages - g.drawImage(i == (settings.maxMessages - 1) && this.msgs.length > settings.maxMessages ? atob("EASBAGGG88/zz2GG") : require("messageicons").getImage(msg), - this.x + 12 + i * 24, this.y + 12, {rotate:0/*force centering*/}); - } - } - WIDGETS["messages"].i=setTimeout(()=>WIDGETS["messages"].draw(true), 1000); - if (process.env.HWVERSION>1) Bangle.on('touch', this.touch); -},update:function(rawMsgs) { - const settings = Object.assign({maxMessages:3},require('Storage').readJSON("messages.settings.json", true) || {}); - this.msgs = filterMessages(rawMsgs); - this.width = 24 * E.clip(this.msgs.length, 0, settings.maxMessages); - Bangle.drawWidgets(); -},touch:function(b,c) { - var w=WIDGETS["messages"]; - if (!w||!w.width||c.xw.x+w.width||c.yw.y+24) return; - load("messages.app.js"); -}}; - -/* We might have returned here if we were in the Messages app for a -message but then the watch was never viewed. */ -if (global.MESSAGES===undefined) - WIDGETS["messages"].update(require("messages").getMessages()); -})(); diff --git a/apps/messagesmusic/ChangeLog b/apps/messagesmusic/ChangeLog index 9f4cafb0e..cd1c49b60 100644 --- a/apps/messagesmusic/ChangeLog +++ b/apps/messagesmusic/ChangeLog @@ -1,2 +1,6 @@ 0.01: New App! 0.02: Remove one line of code that didn't do anything other than in some instances hinder the function of the app. +0.03: Use the new messages library +0.04: Fix dependency on messages library + Fix loading message UI +0.05: Ensure we don't clear artist info diff --git a/apps/messagesmusic/README.md b/apps/messagesmusic/README.md index 85608118d..9a50de93e 100644 --- a/apps/messagesmusic/README.md +++ b/apps/messagesmusic/README.md @@ -1,15 +1,9 @@ Hacky app that uses Messages app and it's library to push a message that triggers the music controls. It's nearly not an app, and yet it moves. -This app require Messages setting 'Auto-open Music' to be 'Yes'. If it isn't, the app will change it to 'Yes' and let it stay that way. - Making the music controls accessible this way lets one start a music stream on the phone in some situations even though the message app didn't receive a music message from gadgetbridge to begin with. (I think.) It is suggested to use Messages Music along side the app Quick Launch. -Messages Music v0.02 has been verified to work with Messages v0.41 on Bangle.js 2 fw2v14. - -Messages Music should work with forks of the original Messages app. At least as long as functions pushMessage() in the library and showMusicMessage() in app.js hasn't been changed too much. - Messages app is created by Gordon Williams with contributions from [Jeroen Peters](https://github.com/jeroenpeters1986). The icon used for this app is from [https://icons8.com](https://icons8.com). diff --git a/apps/messagesmusic/app.js b/apps/messagesmusic/app.js index 27f3f6e4d..68e88c2d8 100644 --- a/apps/messagesmusic/app.js +++ b/apps/messagesmusic/app.js @@ -1,14 +1,2 @@ -let showMusic = () => { - Bangle.CLOCK = 1; // To pass condition in messages library - require('messages').pushMessage({"t":"add","artist":" ","album":" ","track":" ","dur":0,"c":-1,"n":-1,"id":"music","title":"Music","state":"play","new":true}); -}; - -var settings = require('Storage').readJSON('messages.settings.json', true) || {}; //read settings if they exist else set to empty dict -if (!settings.openMusic) { - settings.openMusic = true; // This app/hack works as intended only if this setting is true - require('Storage').writeJSON('messages.settings.json', settings); - E.showMessage("First run:\n\nMessages setting\n\n 'Auto-Open Music'\n\n set to 'Yes'"); - setTimeout(()=>{showMusic();}, 5000); -} else { - showMusic(); -} +// don't define artist/etc here so we don't wipe them out of memory if they were stored from before +setTimeout(()=>require('messages').openGUI({"t":"add","id":"music","state":"show","new":true})); diff --git a/apps/messagesmusic/metadata.json b/apps/messagesmusic/metadata.json index c29ffbc34..eef528f55 100644 --- a/apps/messagesmusic/metadata.json +++ b/apps/messagesmusic/metadata.json @@ -1,7 +1,8 @@ { "id": "messagesmusic", "name":"Messages Music", - "version":"0.02", + "shortName": "Music", + "version":"0.05", "description": "Uses Messages library to push a music message which in turn displays Messages app music controls", "icon":"app.png", "type": "app", @@ -13,6 +14,5 @@ {"name":"messagesmusic.app.js","url":"app.js"}, {"name":"messagesmusic.img","url":"app-icon.js","evaluate":true} ], - "dependencies": {"messages":"app"} - + "dependencies":{"messages":"module"} } diff --git a/apps/mitherm/ChangeLog b/apps/mitherm/ChangeLog new file mode 100644 index 000000000..630459c15 --- /dev/null +++ b/apps/mitherm/ChangeLog @@ -0,0 +1 @@ +0.01: Create mitherm app with support for pvvx firmware only diff --git a/apps/mitherm/README.md b/apps/mitherm/README.md new file mode 100644 index 000000000..cdf3daa61 --- /dev/null +++ b/apps/mitherm/README.md @@ -0,0 +1,22 @@ +Reads BLE advertisement data from Xiaomi temperature/humidity sensors running the +`pvvx` custom firmware (https://github.com/pvvx/ATC_MiThermometer). + +## Features + +* Display temperature +* Display humidity +* Display battery state of sensor +* Auto-refresh every 5 minutes +* Manual refresh on demand +* Add aliases for MAC addresses to easily recognise devices + +## Planned features + +* Supprt for other advertising formats: + * atc1441 format + * BTHome + * Xiaomi Mijia format +* Configurable auto-refresh interval +* Configurable scan length (currently 30s) +* Alerts when temperature outside defined limits (with a widget or bootcode to + work when app is inactive) diff --git a/apps/mitherm/app-icon.js b/apps/mitherm/app-icon.js new file mode 100644 index 000000000..2e8737704 --- /dev/null +++ b/apps/mitherm/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwhC/AH4Ac5gWVhnM4AWVAAIYTCwQABCywYRIoYADJJwWHDB4RD5sz7hJPFIlP//0MRxFE6f/AAM9JJgWE4gWCAANMDBZcEn4XE+ZiKFwhcBCYPdDYRiEGAoXDLgf97vfMQwXILggXFMQYXHLgoXB6czMQoXHLgQXJMQQXG4YWEI44ABngXGh4XHF4v/+DAGC6DXGC5BHGC509F4IXTdwIABV4gXOIwIABJAoX/C6p3Xa4a/UABAXfgczABswC/4XmAH4A/ABY")) diff --git a/apps/mitherm/app.js b/apps/mitherm/app.js new file mode 100644 index 000000000..b7abdb2fc --- /dev/null +++ b/apps/mitherm/app.js @@ -0,0 +1,172 @@ +var filterTemperature = [{ + serviceData: { + "181a": {} + } +}]; +var results = {}; +var macs = []; + +var aliases = require("Storage").readJSON("mitherm.json", true); +if (!aliases) aliases = {}; + +var lastSeen = {}; +var current = 0; +var scanning = false; +var timeoutDraw; +var timeoutScan; + + +const scan = function() { + if (!scanning) { // Don't start scanning if already doing so. + scanning = true; + if (timeoutScan) clearTimeout(timeoutScan); + timeoutScan = setTimeout(scan, 300000); // Scan again in 5 minutes. + drawScanState(scanning); + NRF.findDevices(function(devices) { + onDevices(devices); + }, { + filters: filterTemperature, + timeout: 30000 // Scan for 30s + }); + } +}; + + +const onDevices = function(devices) { + let now = Date.now(); + for (let i = 0; i < devices.length; i++) { + let device = devices[i]; + + let processedData = extractData(device.data); + console.log({ + rssi: device.rssi, + data: processedData + }); + if (!macs.includes(processedData.MAC)) { + macs.push(processedData.MAC); + } + results[processedData.MAC] = processedData; + lastSeen[processedData.MAC] = now; + } + console.log("Scan complete."); + scanning = false; + writeOutput(); +}; + + +const extractData = function(thedata) { + let data = DataView(thedata); + let MAC = []; + for (let i = 9; i > 3; i--) { + MAC.push(data.getUint8(i, true).toString(16).padStart(2, "0")); + } + out = { + size: data.getUint8(0, true), + uid: data.getUint8(1, true), + UUID: data.getUint16(2, true), + MAC: MAC.join(":"), + temperature: data.getInt16(10, true) * 0.01, + humidity: data.getUint16(12, true) * 0.01, + battery_mv: data.getUint16(14, true), + battery_level: data.getUint8(16, true), + }; + return out; +}; + + +const writeOutput = function() { + let now = Date.now(); + if (timeoutDraw) clearTimeout(timeoutDraw); + timeoutDraw = setTimeout(writeOutput, 60000); // Refresh in 1 minute. + g.clear(true); + Bangle.drawWidgets(); + g.reset(); + drawScanState(scanning); + + if (macs.length == 0) return; + + processedData = results[macs[current]]; + g.setFont12x20(2); + g.drawString(`${processedData.temperature.toFixed(2)}°C`, 10, 30); + g.drawString(`${processedData.humidity.toFixed(2)} %`, 10, 70); + + g.setFont6x15(); + g.drawString(`${((now - lastSeen[macs[current]]) / 60000).toFixed(0)} min ago`, 10, 130); + g.drawString(`${processedData.battery_level} % battery`, 80, 130); + g.drawString(` ${processedData.MAC in aliases ? aliases[processedData.MAC] : processedData.MAC}: ${current + 1} / ${macs.length}`, 10, 150); +}; + + +const scrollDevices = function(directionLR) { + // Swipe left or right to move between devices. + current -= directionLR; // inverted feels a more familiar gesture. + if (current + 1 > macs.length) + current = 0; + if (current < 0) + current = macs.length - 1; + writeOutput(); +}; + +const drawScanState = function(state) { + if (state) + g.fillRect(160, 160, 170, 170); + else + g.clearRect(160, 160, 170, 170); +}; + +const setAlias = function(mac, alias) { + if (alias === "") { + delete aliases[mac]; + } + else { + aliases[mac] = alias; + require("Storage").writeJSON("mitherm.json", aliases); + } +}; + +const changeAlias = function(mac) { + g.clear(); + require("textinput").input((mac in aliases) ? aliases[mac] : "").then(function(text) { + setAlias(mac, text); + setUI(); + writeOutput(); + }); +}; + + +const setUI = function() { + Bangle.setUI({ + mode: "custom", + swipe: scrollDevices, + btn: function() { + E.showMenu(actionsMenu); + } + }); +}; + + +const actionsMenu = { + "": { + "title": "-- Actions --", + "back": function() { + E.showMenu(); + }, + "remove": function() { + setUI(); + writeOutput(); + }, + }, + "Scan now": function() { + scan(); + E.showMenu(); + }, + "Edit alias": function() { + changeAlias(macs[current]); + }, +}; + +setUI(); +Bangle.loadWidgets(); +g.setClipRect(Bangle.appRect); +scan(); +writeOutput(); diff --git a/apps/mitherm/app.png b/apps/mitherm/app.png new file mode 100644 index 000000000..81d6bb24f Binary files /dev/null and b/apps/mitherm/app.png differ diff --git a/apps/mitherm/metadata.json b/apps/mitherm/metadata.json new file mode 100644 index 000000000..a8da6fd26 --- /dev/null +++ b/apps/mitherm/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "mitherm", + "name": "Xiaomi Mijia Temperature and Humidity display", + "shortName": "MiTherm", + "version": "0.01", + "description": "Reads and displays data from Xiaomi temperature/humidity sensors running custom firmware", + "icon": "app.png", + "tags": "xiaomi,mi,ble,bluetooth,thermometer,humidity", + "readme": "README.md", + "supports": ["BANGLEJS", "BANGLEJS2"], + "storage": [ + {"name":"mitherm.app.js","url":"app.js"}, + {"name":"mitherm.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/mylocation/ChangeLog b/apps/mylocation/ChangeLog index b46b3b178..afe1810e9 100644 --- a/apps/mylocation/ChangeLog +++ b/apps/mylocation/ChangeLog @@ -6,3 +6,4 @@ 0.06: renamed source files to match standard 0.07: Move mylocation app into 'Settings -> Apps' 0.08: Allow setting location from webinterface in the AppLoader +0.09: Fix web interface so app can be installed (replaced custom with interface html) diff --git a/apps/mylocation/README.md b/apps/mylocation/README.md index b12ed5dcf..11a644262 100644 --- a/apps/mylocation/README.md +++ b/apps/mylocation/README.md @@ -1,8 +1,13 @@ # My Location - *Sets and stores GPS lat and lon of your preferred city* +*Sets and stores GPS lat and lon of your preferred city* -To access, go to `Settings -> Apps -> My Location` +To access, you have two options: + +**In the App Loader** once My Location is installed, click on the 'Save' icon +next to it - and you can choose your location on a map. + +**On Bangle.js** go to `Settings -> Apps -> My Location` * Select one of the preset Cities, setup through the GPS or use the webinterface from the AppLoader * Other Apps can read this information to do calculations based on location diff --git a/apps/mylocation/custom.html b/apps/mylocation/interface.html similarity index 79% rename from apps/mylocation/custom.html rename to apps/mylocation/interface.html index 5c0130199..79a122bf7 100644 --- a/apps/mylocation/custom.html +++ b/apps/mylocation/interface.html @@ -33,10 +33,11 @@
+ Click the map to select a location
- + @@ -68,15 +69,19 @@ let latlon; var marker; - - map.on('click', function(e){ - console.log(e); + + function setPosition(ll) { + latlon = ll; if (map.hasLayer(marker)) { map.removeLayer(marker); } - latlon = e.latlng; - marker = new L.marker(e.latlng).addTo(map); + marker = new L.marker(latlon).addTo(map); + document.getElementById("select-hint").style.display="none"; document.getElementById("select").style.display=""; + } + + map.on('click', function(e){ + setPosition(e.latlng); }); document.getElementById("select").addEventListener("click", function() { @@ -87,9 +92,23 @@ Util.showModal("Saving..."); Util.writeStorage("mylocation.json", JSON.stringify(settings), ()=>{ Util.hideModal(); + Util.close(); // close this window }); }); + function onInit() { + // read existing location + Util.readStorage("mylocation.json", function(data) { + if (data===undefined) return; // no file + try { + var j = JSON.parse(data); + setPosition(j); + } catch (e) { + console.error(e); + } + }); + } + diff --git a/apps/mylocation/metadata.json b/apps/mylocation/metadata.json index bef5b983c..1c2974030 100644 --- a/apps/mylocation/metadata.json +++ b/apps/mylocation/metadata.json @@ -4,12 +4,12 @@ "icon": "app.png", "type": "settings", "screenshots": [{"url":"screenshot_1.png"}], - "version":"0.08", + "version":"0.09", "description": "Sets and stores the latitude and longitude of your preferred City. It can be set from GPS or webinterface. `mylocation.json` can be used by other apps that need your main location. See README for details.", "readme": "README.md", "tags": "tool,utility", "supports": ["BANGLEJS", "BANGLEJS2"], - "custom": "custom.html","custom": "custom.html", + "interface": "interface.html", "storage": [ {"name":"mylocation.settings.js","url":"settings.js"} ], diff --git a/apps/noteify/ChangeLog b/apps/noteify/ChangeLog index d7bc46dcd..a37a66731 100644 --- a/apps/noteify/ChangeLog +++ b/apps/noteify/ChangeLog @@ -1,2 +1,3 @@ 0.01: Initial version 0.02: Use default Bangle formatter for booleans +0.03: Drop duplicate alarm widget diff --git a/apps/noteify/interface.html b/apps/noteify/interface.html index 027c98860..4d7974ad9 100644 --- a/apps/noteify/interface.html +++ b/apps/noteify/interface.html @@ -18,6 +18,11 @@ var notesElement = document.getElementById("notes"); var notes = {}; +function disableFormInput() { + document.querySelectorAll(".form-input").forEach(el => el.disabled = true); + document.querySelectorAll(".btn").forEach(el => el.disabled = true); +} + function getData() { // show loading window Util.showModal("Loading..."); @@ -53,8 +58,10 @@ function getData() { buttonSave.classList.add('btn-default'); buttonSave.onclick = function() { notes[i].note = textarea.value; - Util.writeStorage("noteify.json", JSON.stringify(notes)); - location.reload(); + disableFormInput(); + Util.writeStorage("noteify.json", JSON.stringify(notes), () => { + location.reload(); // reload so we see current data + }); } divColumn2.appendChild(buttonSave); @@ -64,8 +71,10 @@ function getData() { buttonDelete.onclick = function() { notes[i].note = textarea.value; notes.splice(i, 1); - Util.writeStorage("noteify.json", JSON.stringify(notes)); - location.reload(); // reload so we see current data + disableFormInput(); + Util.writeStorage("noteify.json", JSON.stringify(notes), () => { + location.reload(); // reload so we see current data + }); } divColumn2.appendChild(buttonDelete); divColumn.appendChild(divColumn2); @@ -77,8 +86,10 @@ function getData() { document.getElementById("btnAdd").addEventListener("click", function() { const note = document.getElementById("note-new").value; notes.push({"note": note}); - Util.writeStorage("noteify.json", JSON.stringify(notes)); - location.reload(); // reload so we see current data + disableFormInput(); + Util.writeStorage("noteify.json", JSON.stringify(notes), () => { + location.reload(); // reload so we see current data + }); }); }); } diff --git a/apps/noteify/metadata.json b/apps/noteify/metadata.json index fbd5a88f1..850628c46 100644 --- a/apps/noteify/metadata.json +++ b/apps/noteify/metadata.json @@ -1,7 +1,7 @@ { "id": "noteify", "name": "Noteify", - "version": "0.02", + "version": "0.03", "description": "Write notes using an onscreen keyboard and use them as custom messages for alarms or timers.", "icon": "app.png", "tags": "tool,alarm", @@ -9,8 +9,7 @@ "readme": "README.md", "storage": [ {"name":"noteify.app.js","url":"app.js"}, - {"name":"noteify.img","url":"app-icon.js","evaluate":true}, - {"name":"noteify.wid.js","url":"widget.js"} + {"name":"noteify.img","url":"app-icon.js","evaluate":true} ], "data": [{"name":"noteify.json"}], "dependencies": {"scheduler":"type","textinput":"type"}, diff --git a/apps/noteify/widget.js b/apps/noteify/widget.js deleted file mode 100644 index 052ac9ebd..000000000 --- a/apps/noteify/widget.js +++ /dev/null @@ -1,8 +0,0 @@ -WIDGETS["alarm"]={area:"tl",width:0,draw:function() { - if (this.width) g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y); - },reload:function() { - // don't include library here as we're trying to use as little RAM as possible - WIDGETS["alarm"].width = (require('Storage').readJSON('sched.json',1)||[]).some(alarm=>alarm.on&&(alarm.hidden!==false)) ? 24 : 0; - } -}; -WIDGETS["alarm"].reload(); diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index 2a6c15a09..7f788c139 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -12,3 +12,7 @@ Fix alignment of satellite info text 0.12: switch to using normal OpenStreetMap tiles (opentopomap was too slow) 0.13: Use a single image file with 'frames' of data (drastically reduces file count, possibility of >1 map on device) +0.14: Added ability to upload multiple sets of map tiles + Support for zooming in on map + Satellite count moved to widget bar to leave more room for the map +0.15: Make track drawing an option (default off) diff --git a/apps/openstmap/README.md b/apps/openstmap/README.md new file mode 100644 index 000000000..f19b13bd1 --- /dev/null +++ b/apps/openstmap/README.md @@ -0,0 +1,53 @@ +# OpenStreetMap + +This app allows you to upload and use OpenSteetMap map tiles onto your +Bangle. There's an uploader, the app, and also a library that +allows you to use the maps in your Bangle.js applications. + +## Uploader + +Once you've installed OpenStreepMap on your Bangle, find it +in the App Loader and click the Disk icon next to it. + +A window will pop up showing what maps you have loaded. + +To add a map: + +* Click `Add Map` +* Scroll and zoom to the area of interest or use the Search button in the top left +* Now choose the size you want to upload (Small/Medium/etc) +* On Bangle.js 1 you can choose if you want a 3 bits per pixel map (this is lower +quality but uploads faster and takes less space). On Bangle.js 2 you only have a 3bpp +display so can only use 3bpp. +* Click `Get Map`, and a preview will be displayed. If you need to adjust the area you +can change settings, move the map around, and click `Get Map` again. +* When you're ready, click `Upload` + +## Bangle.js App + +The Bangle.js app allows you to view a map - it also turns the GPS on and marks +the path that you've been travelling (if enabled). + +* Drag on the screen to move the map +* Press the button to bring up a menu, where you can zoom, go to GPS location +, put the map back in its default location, or choose whether to draw the currently +recording GPS track (from the `Recorder` app). + +**Note:** If enabled, drawing the currently recorded GPS track can take a second +or two (which happens after you've finished scrolling the screen with your finger). + + +## Library + +See the documentation in the library itself for full usage info: +https://github.com/espruino/BangleApps/blob/master/apps/openstmap/openstmap.js + +Or check the app itself: https://github.com/espruino/BangleApps/blob/master/apps/openstmap/app.js + +But in the most simple form: + +``` +var m = require("openstmap"); +// m.lat/lon are now the center of the loaded map +m.draw(); // draw centered on the middle of the loaded map +``` diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js index 62597ca20..89e2d2ddb 100644 --- a/apps/openstmap/app.js +++ b/apps/openstmap/app.js @@ -1,20 +1,31 @@ var m = require("openstmap"); var HASWIDGETS = true; -var y1,y2; +var R; var fix = {}; +var mapVisible = false; +var hasScrolled = false; +var settings = require("Storage").readJSON("openstmap.json",1)||{}; +// Redraw the whole page function redraw() { - g.setClipRect(0,y1,g.getWidth()-1,y2); + g.setClipRect(R.x,R.y,R.x2,R.y2); m.draw(); drawMarker(); - if (WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) { - g.flip(); // force immediate draw on double-buffered screens - track will update later - g.setColor(0.75,0.2,0); - WIDGETS["gpsrec"].plotTrack(m); + // if track drawing is enabled... + if (settings.drawTrack) { + if (HASWIDGETS && WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) { + g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later + WIDGETS["gpsrec"].plotTrack(m); + } + if (HASWIDGETS && WIDGETS["recorder"] && WIDGETS["recorder"].plotTrack) { + g.setColor("#f00").flip(); // force immediate draw on double-buffered screens - track will update later + WIDGETS["recorder"].plotTrack(m); + } } g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); } +// Draw the marker for where we are function drawMarker() { if (!fix.fix) return; var p = m.latLonToXY(fix.lat, fix.lon); @@ -22,50 +33,70 @@ function drawMarker() { g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2); } -var fix; Bangle.on('GPS',function(f) { fix=f; - g.reset().clearRect(0,y1,g.getWidth()-1,y1+8).setFont("6x8").setFontAlign(0,0); - var txt = fix.satellites+" satellites"; - if (!fix.fix) - txt += " - NO FIX"; - g.drawString(txt,g.getWidth()/2,y1 + 4); - drawMarker(); + if (HASWIDGETS) WIDGETS["sats"].draw(WIDGETS["sats"]); + if (mapVisible) drawMarker(); }); Bangle.setGPSPower(1, "app"); if (HASWIDGETS) { Bangle.loadWidgets(); + WIDGETS["sats"] = { area:"tl", width:48, draw:w=>{ + var txt = (0|fix.satellites)+" Sats"; + if (!fix.fix) txt += "\nNO FIX"; + g.reset().setFont("6x8").setFontAlign(0,0) + .drawString(txt,w.x+24,w.y+12); + } + }; Bangle.drawWidgets(); - y1 = 24; - var hasBottomRow = Object.keys(WIDGETS).some(w=>WIDGETS[w].area[0]=="b"); - y2 = g.getHeight() - (hasBottomRow ? 24 : 1); -} else { - y1=0; - y2=g.getHeight()-1; } +R = Bangle.appRect; -redraw(); - -function recenter() { - if (!fix.fix) return; - m.lat = fix.lat; - m.lon = fix.lon; +function showMap() { + mapVisible = true; + g.reset().clearRect(R); redraw(); + Bangle.setUI({mode:"custom",drag:e=>{ + if (e.b) { + g.setClipRect(R.x,R.y,R.x2,R.y2); + g.scroll(e.dx,e.dy); + m.scroll(e.dx,e.dy); + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); + hasScrolled = true; + } else if (hasScrolled) { + hasScrolled = false; + redraw(); + } + }, btn: btn=>{ + mapVisible = false; + var menu = {"":{title:"Map"}, + "< Back": ()=> showMap(), + /*LANG*/"Zoom In": () =>{ + m.scale /= 2; + showMap(); + }, + /*LANG*/"Zoom Out": () =>{ + m.scale *= 2; + showMap(); + }, + /*LANG*/"Draw Track": { + value : !!settings.drawTrack, + onchange : v => { settings.drawTrack=v; require("Storage").writeJSON("openstmap.json",settings); } + }, + /*LANG*/"Center Map": () =>{ + m.lat = m.map.lat; + m.lon = m.map.lon; + m.scale = m.map.scale; + showMap(); + }}; + if (fix.fix) menu[/*LANG*/"Center GPS"]=() =>{ + m.lat = fix.lat; + m.lon = fix.lon; + showMap(); + }; + E.showMenu(menu); + }}); } -setWatch(recenter, global.BTN2?BTN2:BTN1, {repeat:true}); - -var hasScrolled = false; -Bangle.on('drag',e=>{ - if (e.b) { - g.setClipRect(0,y1,g.getWidth()-1,y2); - g.scroll(e.dx,e.dy); - m.scroll(e.dx,e.dy); - g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); - hasScrolled = true; - } else if (hasScrolled) { - hasScrolled = false; - redraw(); - } -}); +showMap(); diff --git a/apps/openstmap/custom.html b/apps/openstmap/interface.html similarity index 57% rename from apps/openstmap/custom.html rename to apps/openstmap/interface.html index c1a161458..0bf2268a4 100644 --- a/apps/openstmap/custom.html +++ b/apps/openstmap/interface.html @@ -9,7 +9,8 @@ padding: 0; margin: 0; } - html, body, #map { + html, body, #map, #mapsLoaded, #mapContainer { + position: relative; height: 100%; width: 100%; } @@ -27,20 +28,42 @@ width: 256px; height: 256px; } + .tile-title { + font-weight:bold; + font-size: 125%; + } + .tile-map { + width: 128px; + height: 128px; + } -
+
-
-

3 bit
-
- - +
+
+
+
+

3 bit
+
+ +
+
+ + +
- + @@ -60,8 +83,6 @@ TODO: */ var TILESIZE = 96; // Size of our tiles var OSMTILESIZE = 256; // Size of openstreetmap tiles - var MAPSIZE = TILESIZE*5; ///< 480 - Size of map we download - var OSMTILECOUNT = 3; // how many tiles do we download in each direction (Math.floor(MAPSIZE / OSMTILESIZE)+1) /* Can see possible tiles on http://leaflet-extras.github.io/leaflet-providers/preview/ However some don't allow cross-origin use */ //var TILELAYER = 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png'; // simple, high contrast, TOO SLOW @@ -69,8 +90,8 @@ TODO: var TILELAYER = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; var PREVIEWTILELAYER = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'; - // Create map and try and set the location to where the browser thinks we are - var map = L.map('map').locate({setView: true, maxZoom: 16, enableHighAccuracy:true}); + var loadedMaps = []; + // Tiles used for Bangle.js itself var bangleTileLayer = L.tileLayer(TILELAYER, { maxZoom: 18, @@ -83,6 +104,10 @@ TODO: }); // Could optionally overlay trails: https://wiki.openstreetmap.org/wiki/Tiles + // Create map and try and set the location to where the browser thinks we are + var map = L.map('map').locate({setView: true, maxZoom: 16, enableHighAccuracy:true}); + previewTileLayer.addTo(map); + // Search box: const searchProvider = new window.GeoSearch.OpenStreetMapProvider(); const searchControl = new GeoSearch.GeoSearchControl({ @@ -96,6 +121,7 @@ TODO: }); map.addControl(searchControl); + // ---------------------------------------- Run at startup function onInit(device) { if (device && device.info && device.info.g) { // On 3 bit devices, don't even offer the option. 3 bit is the only way @@ -104,12 +130,122 @@ TODO: document.getElementById("3bitdiv").style = "display:none"; } } + + showLoadedMaps(); } - var mapFiles = []; - previewTileLayer.addTo(map); + function showLoadedMaps() { + document.getElementById("mapsLoadedContainer").style.display = ""; + document.getElementById("mapContainer").style.display = "none"; - function tilesLoaded(ctx, width, height) { + Util.showModal("Loading maps..."); + let mapsLoadedContainer = document.getElementById("mapsLoadedContainer"); + mapsLoadedContainer.innerHTML = ""; + loadedMaps = []; + + Puck.write(`\x10Bluetooth.println(require("Storage").list(/openstmap\\.\\d+\\.json/))\n`,function(files) { + console.log("MAPS:",files); + let promise = Promise.resolve(); + files.trim().split(",").forEach(filename => { + if (filename=="") return; + promise = promise.then(() => new Promise(resolve => { + Util.readStorage(filename, fileContents => { + console.log(filename + " => " + fileContents); + let mapNumber = filename.match(/\d+/)[0]; // figure out what map number we are + let mapInfo; + try { + mapInfo = JSON.parse(fileContents); + } catch (e) { + console.error(e); + } + loadedMaps[mapNumber] = mapInfo; + if (mapInfo!==undefined) { + let latlon = L.latLng(mapInfo.lat, mapInfo.lon); + mapsLoadedContainer.innerHTML += ` +
+
+
+
+
+
+

Map ${mapNumber}

+

${mapInfo.w*mapInfo.h} Tiles (${((mapInfo.imgx*mapInfo.imgy)>>11).toFixed(0)}k)

+
+
+ +
+
+ `; + setTimeout(function() { + let map = L.map(`tile-map-${mapNumber}`); + L.tileLayer(PREVIEWTILELAYER, { + maxZoom: 18 + }).addTo(map); + let marker = new L.marker(latlon).addTo(map); + map.fitBounds(latlon.toBounds(2000/*meters*/), {animation: false}); + }, 100); + } + resolve(); + }); + })); + }); + promise = promise.then(() => new Promise(resolve => { + if (!loadedMaps.length) { + mapsLoadedContainer.innerHTML += ` +
+
+
+
+
+
+

No Maps Loaded

+
+
+
+
+ `; + } + mapsLoadedContainer.innerHTML += ` +
+
+
+
+
+
+
+
+ +
+
+ `; + Util.hideModal(); + })); + }); + } + + function onMapDelete(mapNumber) { + console.log("delete", mapNumber); + Util.showModal(`Erasing map ${mapNumber}...`); + Util.eraseStorage(`openstmap.${mapNumber}.json`, function() { + Util.eraseStorage(`openstmap.${mapNumber}.img`, function() { + Util.hideModal(); + showLoadedMaps(); + }); + }); + } + + function showMap() { + document.getElementById("mapsLoadedContainer").style.display = "none"; + document.getElementById("mapContainer").style.display = ""; + document.getElementById("maptiles").style.display="none"; + document.getElementById("uploadbuttons").style.display="none"; + } + + // ----------------------------------------------------- + var mapFiles = []; + + // convert canvas into an actual tiled image file + function tilesLoaded(ctx, width, height, mapImageFile) { var options = { compression:false, output:"raw", mode:"web" @@ -118,6 +254,7 @@ TODO: options = { compression:false, output:"raw", mode:"3bit", + diffusion:"bayer2" }; /* If in 3 bit mode, go through all the data beforehand and turn the saturation up to maximum, so when thresholded it @@ -166,19 +303,26 @@ TODO: } } return [{ - name:"openstmap.0.img", + name:mapImageFile, content:tiledImage }]; } document.getElementById("getmap").addEventListener("click", function() { + + var MAPTILES = parseInt(document.getElementById("mapSize").value); + var MAPSIZE = TILESIZE*MAPTILES; /// Size of map we download to Bangle in pixels + var OSMTILECOUNT = (Math.ceil((MAPSIZE+TILESIZE) / OSMTILESIZE)+1); // how many tiles do we download from OSM in each direction + var zoom = map.getZoom(); var centerlatlon = map.getBounds().getCenter(); - var center = map.project(centerlatlon, zoom).divideBy(OSMTILESIZE); - // Reason for 16px adjustment below not 100% known, but it seems to - // align everything perfectly: https://github.com/espruino/BangleApps/issues/984 - var ox = Math.round((center.x - Math.floor(center.x)) * OSMTILESIZE) + 16; - var oy = Math.round((center.y - Math.floor(center.y)) * OSMTILESIZE) + 16; + var center = map.project(centerlatlon, zoom).divideBy(OSMTILESIZE); // the center of our map + // ox/oy = offset in pixels + var ox = Math.round((center.x - Math.floor(center.x)) * OSMTILESIZE); + var oy = Math.round((center.y - Math.floor(center.y)) * OSMTILESIZE); + // adjust offset because we want to center our map + ox -= MAPTILES * TILESIZE / 2; + oy -= MAPTILES * TILESIZE / 2; center = center.floor(); // make sure we're in the middle of a tile // JS version of Bangle.js's projection function bproject(lat, lon) { @@ -215,10 +359,12 @@ TODO: var ctx = canvas.getContext('2d'); canvas.width = MAPSIZE; canvas.height = MAPSIZE; - for (var i = 0; i < OSMTILECOUNT; i++) { - for (var j = 0; j < OSMTILECOUNT; j++) { + var tileMin = Math.round(-OSMTILECOUNT/2); + var tileMax = Math.round(OSMTILECOUNT/2); + for (var i = tileMin; i <= tileMax; i++) { + for (var j = tileMin; j <= tileMax; j++) { (function(i,j){ - var coords = new L.Point(center.x+i-1, center.y+j-1); + var coords = new L.Point(center.x+i, center.y+j); coords.z = zoom; var img = new Image(); img.crossOrigin = "Anonymous"; @@ -230,6 +376,8 @@ TODO: ctx.fillRect(testPt.x-1, testPt.y-5, 3,10); ctx.fillRect(testPt.x-5, testPt.y-1, 10,3); }*/ + /*ctx.fillStyle="black"; + ctx.fillRect(i*OSMTILESIZE - ox, j*OSMTILESIZE - oy, 6,6);*/ resolve(); }; })); @@ -242,8 +390,11 @@ TODO: Promise.all(tileGetters).then(() => { document.getElementById("uploadbuttons").style.display=""; - mapFiles = tilesLoaded(ctx, canvas.width, canvas.height); - mapFiles.unshift({name:"openstmap.0.json",content:JSON.stringify({ + var mapNumber = 0; + while (loadedMaps[mapNumber]) mapNumber++; + let mapImageFile = `openstmap.${mapNumber}.img`; + mapFiles = tilesLoaded(ctx, canvas.width, canvas.height, mapImageFile); + mapFiles.unshift({name:`openstmap.${mapNumber}.json`,content:JSON.stringify({ imgx : canvas.width, imgy : canvas.height, tilesize : TILESIZE, @@ -252,21 +403,33 @@ TODO: lon : centerlatlon.lng, w : Math.round(canvas.width / TILESIZE), // width in tiles h : Math.round(canvas.height / TILESIZE), // height in tiles - fn : "openstmap.0.img" + fn : mapImageFile })}); + var mapSizeInK = Math.round(mapFiles.reduce((r,m)=>m.content.length+r,0)/1000); + document.getElementById("mapstats").innerText = "Size : "+ (mapSizeInK+"kb"); console.log(mapFiles); }); }); document.getElementById("upload").addEventListener("click", function() { - sendCustomizedApp({ - storage:mapFiles + Util.showModal("Uploading..."); + let promise = Promise.resolve(); + mapFiles.forEach(file => { + promise = promise.then(function() { + return new Promise(resolve => { + Util.writeStorage(file.name, file.content, resolve); + }); + }); + }); + promise.then(function() { + Util.hideModal(); + console.log("Upload Complete"); + showLoadedMaps(); }); }); document.getElementById("cancel").addEventListener("click", function() { - document.getElementById("maptiles").style.display="none"; - document.getElementById("uploadbuttons").style.display="none"; + showMap(); }); diff --git a/apps/openstmap/metadata.json b/apps/openstmap/metadata.json index 32093f70e..819dc4122 100644 --- a/apps/openstmap/metadata.json +++ b/apps/openstmap/metadata.json @@ -2,17 +2,21 @@ "id": "openstmap", "name": "OpenStreetMap", "shortName": "OpenStMap", - "version": "0.13", + "version": "0.15", "description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps", + "readme": "README.md", "icon": "app.png", "tags": "outdoors,gps,osm", "supports": ["BANGLEJS","BANGLEJS2"], "screenshots": [{"url":"screenshot.png"}], - "custom": "custom.html", - "customConnect": true, + "interface": "interface.html", "storage": [ {"name":"openstmap","url":"openstmap.js"}, {"name":"openstmap.app.js","url":"app.js"}, {"name":"openstmap.img","url":"app-icon.js","evaluate":true} + ], "data": [ + {"name":"openstmap.json"}, + {"wildcard":"openstmap.*.json"}, + {"wildcard":"openstmap.*.img"} ] } diff --git a/apps/openstmap/openstmap.js b/apps/openstmap/openstmap.js index 2bd7d2e2e..692344357 100644 --- a/apps/openstmap/openstmap.js +++ b/apps/openstmap/openstmap.js @@ -20,32 +20,59 @@ function center() { m.draw(); } +// you can even change the scale - eg 'm/scale *= 2' + */ -var map = require("Storage").readJSON("openstmap.0.json"); -map.center = Bangle.project({lat:map.lat,lon:map.lon}); -exports.map = map; -exports.lat = map.lat; // actual position of middle of screen -exports.lon = map.lon; // actual position of middle of screen var m = exports; +m.maps = require("Storage").list(/openstmap\.\d+\.json/).map(f=>{ + let map = require("Storage").readJSON(f); + map.center = Bangle.project({lat:map.lat,lon:map.lon}); + return map; +}); +// we base our start position on the middle of the first map +m.map = m.maps[0]; +m.scale = m.map.scale; // current scale (based on first map) +m.lat = m.map.lat; // position of middle of screen +m.lon = m.map.lon; // position of middle of screen exports.draw = function() { - var img = require("Storage").read(map.fn); var cx = g.getWidth()/2; var cy = g.getHeight()/2; var p = Bangle.project({lat:m.lat,lon:m.lon}); - var ix = (p.x-map.center.x)/map.scale + (map.imgx/2) - cx; - var iy = (map.center.y-p.y)/map.scale + (map.imgy/2) - cy; - //console.log(ix,iy); - var tx = 0|(ix/map.tilesize); - var ty = 0|(iy/map.tilesize); - var ox = (tx*map.tilesize)-ix; - var oy = (ty*map.tilesize)-iy; - for (var x=ox,ttx=tx;x=0 && ttx=0 && tty { + var d = map.scale/m.scale; + var ix = (p.x-map.center.x)/m.scale + (map.imgx*d/2) - cx; + var iy = (map.center.y-p.y)/m.scale + (map.imgy*d/2) - cy; + var o = {}; + var s = map.tilesize; + if (d!=1) { // if the two are different, add scaling + s *= d; + o.scale = d; } + //console.log(ix,iy); + var tx = 0|(ix/s); + var ty = 0|(iy/s); + var ox = (tx*s)-ix; + var oy = (ty*s)-iy; + var img = require("Storage").read(map.fn); + // fix out of range so we don't have to iterate over them + if (tx<0) { + ox+=s*-tx; + tx=0; + } + if (ty<0) { + oy+=s*-ty; + ty=0; + } + var mx = g.getWidth(); + var my = g.getHeight(); + for (var x=ox,ttx=tx; x 12) @@ -215,12 +215,12 @@ function drawClock() { g.reset(); g.setColor(g.theme.bg); g.fillRect(Bangle.appRect); - + // draw a grid like graph paper if (settings.grid && process.env.HWVERSION !=1) { g.setColor("#0f0"); for (var gx=20; gx <= w; gx += 20) - g.drawLine(gx, 30, gx, h - 24); + g.drawLine(gx, 30, gx, h - 24); for (var gy=30; gy <= h - 24; gy += 20) g.drawLine(0, gy, w, gy); } @@ -238,7 +238,7 @@ function drawClock() { g.drawString( (w_wind.split(' ').slice(0, 2).join(' ')), (w/2) + 6, 24 + ((y - 24)/2)); // display first 2 words of the wind string eg '4 mph' } - + if (settings.font == "Architect") g.setFontArchitect(); else if (settings.font == "GochiHand") @@ -253,7 +253,7 @@ function drawClock() { g.setFontSpecialElite(); else g.setFontLato(); - + g.setFontAlign(1,-1); // right aligned g.drawString(hh, x - 6, y); g.setFontAlign(-1,-1); // left aligned @@ -310,7 +310,7 @@ function BUTTON(name,x,y,w,h,c,f,tx) { // if pressed the callback BUTTON.prototype.check = function(x,y) { //console.log(this.name + ":check() x=" + x + " y=" + y +"\n"); - + if (x>= this.x && x<= (this.x + this.w) && y>= this.y && y<= (this.y + this.h)) { log_debug(this.name + ":callback\n"); this.callback(); @@ -366,7 +366,7 @@ function checkIdle() { warned = false; return; } - + let hour = (new Date()).getHours(); let active = (hour >= 9 && hour < 21); //let active = true; @@ -397,7 +397,7 @@ function buzzer(n) { if (n-- < 1) return; Bangle.buzz(250); - + if (buzzTimeout) clearTimeout(buzzTimeout); buzzTimeout = setTimeout(function() { buzzTimeout = undefined; diff --git a/apps/podadrem/ChangeLog b/apps/podadrem/ChangeLog index c26e40c0e..3c68f15ac 100644 --- a/apps/podadrem/ChangeLog +++ b/apps/podadrem/ChangeLog @@ -4,3 +4,6 @@ Addict. 0.04: New layout. 0.05: Add widget field, tweak layout. +0.06: Add compatibility with Fastload Utils. +0.07: Remove just the specific listeners to not interfere with Quick Launch +when fastloading. diff --git a/apps/podadrem/app.js b/apps/podadrem/app.js index b04d80b17..9c9ed8b04 100644 --- a/apps/podadrem/app.js +++ b/apps/podadrem/app.js @@ -1,77 +1,20 @@ +{ /* -Bluetooth.println(JSON.stringify({t:"intent", action:"", flags:["flag1", "flag2",...], categories:["category1","category2",...], mimetype:"", data:"", package:"", class:"", target:"", extra:{someKey:"someValueOrString"}})); + Bluetooth.println(JSON.stringify({t:"intent", action:"", flags:["flag1", "flag2",...], categories:["category1","category2",...], mimetype:"", data:"", package:"", class:"", target:"", extra:{someKey:"someValueOrString"}})); -Podcast Addict is developed by Xavier Guillemane and can be downloaded on Google Play Store: https://play.google.com/store/apps/details?id=com.bambuna.podcastaddict&hl=en_US&gl=US - -Podcast Addict can be controlled through the sending of remote commands called 'Intents'. -Some 3rd parties apps specialized in task automation will then allow you to control Podcast Addict. For example, you will be able to wake up to the sound of your playlist or to start automatically playing when some NFC tag has been detected. -In Tasker, you just need to copy/paste one of the following intent in the task Action field ("Misc" action type then select "Send Itent") . -If you prefer Automate It, you can use the Podcast Addict plugin that will save you some configuration time (https://play.google.com/store/apps/details?id=com.smarterapps.podcastaddictplugin ) -Before using an intent make sure to set the following: -Package: com.bambuna.podcastaddict -Class (UPDATE intent only): com.bambuna.podcastaddict.receiver.PodcastAddictBroadcastReceiver -Class (every other intent): com.bambuna.podcastaddict.receiver.PodcastAddictPlayerReceiver -Here are the supported commands (Intents) : -com.bambuna.podcastaddict.service.player.toggle – Toggle the playlist -com.bambuna.podcastaddict.service.player.stop – Stop the player and release its resources -com.bambuna.podcastaddict.service.player.play – Start playing the playlist -com.bambuna.podcastaddict.service.player.pause – Pause the playlist -com.bambuna.podcastaddict.service.player.nexttrack – Start playing next track -com.bambuna.podcastaddict.service.player.previoustrack – Start playing previous track -com.bambuna.podcastaddict.service.player.jumpforward – Jump 30s forward -com.bambuna.podcastaddict.service.player.jumpbackward – Jump 15s backward -com.bambuna.podcastaddict.service.player.1xspeed - Disable the variable playback speed -com.bambuna.podcastaddict.service.player.1.5xspeed – Force the playback speed at 1.5x -com.bambuna.podcastaddict.service.player.2xspeed – Force the playback speed at 2.0x -com.bambuna.podcastaddict.service.player.stoptimer – Disable the timer -com.bambuna.podcastaddict.service.player.15mntimer – Set the timer at 15 minutes -com.bambuna.podcastaddict.service.player.30mntimer – Set the timer at 30 minutes -com.bambuna.podcastaddict.service.player.60mntimer – Set the timer at 1 hour -com.bambuna.podcastaddict.service.update – Trigger podcasts update -com.bambuna.podcastaddict.openmainscreen – Open the app on the Main screen -com.bambuna.podcastaddict.openplaylist – Open the app on the Playlist screen -com.bambuna.podcastaddict.openplayer – Open the app on the Player screen -com.bambuna.podcastaddict.opennewepisodes – Open the app on the New episodes screen -com.bambuna.podcastaddict.opendownloadedepisodes – Open the app on the Downloaded episodes screen -com.bambuna.podcastaddict.service.player.playfirstepisode – Start playing the first episode in the playlist -com.bambuna.podcastaddict.service.player.customspeed – Select playback speed -In order to use this intent you need to pass a float argument called "arg1". Valid values are within [0.1, 5.0] -com.bambuna.podcastaddict.service.player.customtimer – Start a custom timer -In order to use this intent you need to pass an int argument called "arg1" containing the number of minutes. Valid values are within [1, 1440] -com.bambuna.podcastaddict.service.player.deletecurrentskipnexttrack – Delete the current episode and skip to the next one. It behaves the same way as long pressing on the player >| button, but doesn't display any confirmation popup. -com.bambuna.podcastaddict.service.player.deletecurrentskipprevioustrack – Delete the current episode and skip to the previous one. It behaves the same way as long pressing on the player |< button, but doesn't display any confirmation popup. -com.bambuna.podcastaddict.service.player.boostVolume – Toggle the Volume Boost audio effect -You can pass a, optional boolean argument called "arg1" in order to create a ON or OFF button for the volume boost. Without this parameter the app will just toggle the current value -com.bambuna.podcastaddict.service.player.quickBookmark – Creates a bookmark at the current playback position so you can easily retrieve it later. -com.bambuna.podcastaddict.service.download.pause – Pause downloads -com.bambuna.podcastaddict.service.download.resume – Resume downloads -com.bambuna.podcastaddict.service. download.toggle – Toggle downloads -com.bambuna.podcastaddict.service.player.favorite – Mark the current episode playing as favorite. -com.bambuna.podcastaddict.openplaylist – Open the app on the Playlist screen -You can pass an optional string argument called "arg1" in order to select the playlist to open. Without this parameter the app will open the current playlist -Here's how it works: -##AUDIO## will open the Audio playlist screen -##VIDEO## will open the Video playlist screen -##RADIO## will open the Radio screen -Any other argument will be used as a CATEGORY name. The app will then open this category under the playlist CUSTOM tab -You can pass an optional boolean argument called "arg2" in order to select if the app UI should be opened. Without this parameter the playlist will be displayed -You can pass an optional boolean argument called "arg3" in order to select if the app should start playing the selected playlist. Without this parameter the playback won't start -Since v2020.3 -com.bambuna.podcastaddict.service.full_backup – Trigger a full backup of the app data (relies on the app automatic backup settings for the folder and the # of backup to keep) -This task takes a lot of resources and might take up to a minute to complete, so please avoid using the app at the same time -Since v2020.15 -com.bambuna.podcastaddict.service.player.toggletimer – This will toggle the Sleep Timer using the last duration and parameter used in the app. -Since v2020.16 -com.bambuna.podcastaddict.service.player.togglespeed – This will toggle the Playback speed for the episode currently playing (alternate between selected speed and 1.0x). + Podcast Addict is developed by Xavier Guillemane and can be downloaded on Google Play Store: https://play.google.com/store/apps/details?id=com.bambuna.podcastaddict&hl=en_US&gl=US + + How to use intents to control Podcast Addict: https://podcastaddict.com/faq/130 */ -var R; -var backToMenu = false; -var dark = g.theme.dark; // bool +let R; +let widgetUtils = require("widget_utils"); +let backToMenu = false; +let dark = g.theme.dark; // bool // The main layout of the app -function gfx() { - //Bangle.drawWidgets(); +let gfx = function() { + widgetUtils.hide(); R = Bangle.appRect; marigin = 8; // g.drawString(str, x, y, solid) @@ -106,19 +49,19 @@ function gfx() { g.setFontAlign(1, 1, 0); g.drawString("Speed", R.x + R.w - 2*marigin, R.y + R.h - 2*marigin); -} +}; // Touch handler for main layout -function touchHandler(_, xy) { +let touchHandler = function(_, xy) { x = xy.x; y = xy.y; len = (R.wb-1 instead of a>b. + // doing ab-1 instead of a>=b. if ((R.x-1 { - if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup"); - } + {mode : "updown", + remove : ()=>{ + Bangle.removeListener("touch", touchHandler); + Bangle.removeListener("swipe", swipeHandler); + clearWatch(buttonHandler); + widgetUtils.show(); + } + }, + ud => { + if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup"); + } ); Bangle.on("touch", touchHandler); Bangle.on("swipe", swipeHandler); -} + let buttonHandler = setWatch(()=>{load();}, BTN, {edge:'falling'}); +}; /* The functions for interacting with Android and the Podcast Addict app */ -pkg = "com.bambuna.podcastaddict"; -standardCls = pkg + ".receiver.PodcastAddictPlayerReceiver"; -updateCls = pkg + ".receiver.PodcastAddictBroadcastReceiver"; -speed = 1.0; +let pkg = "com.bambuna.podcastaddict"; +let standardCls = pkg + ".receiver.PodcastAddictPlayerReceiver"; +let updateCls = pkg + ".receiver.PodcastAddictBroadcastReceiver"; +let speed = 1.0; -simpleSearch = ""; +let simpleSearch = ""; -function simpleSearchTerm() { // input a simple search term without tags, overrides search with tags (artist and track) +let simpleSearchTerm = function() { // input a simple search term without tags, overrides search with tags (artist and track) require("textinput").input({ text: simpleSearch }).then(result => { @@ -189,9 +140,9 @@ function simpleSearchTerm() { // input a simple search term without tags, overri }).then(() => { E.showMenu(searchMenu); }); -} +}; -function searchPlayWOTags() { //make a search and play using entered terms +let searchPlayWOTags = function() { //make a search and play using entered terms searchString = simpleSearch; Bluetooth.println(JSON.stringify({ t: "intent", @@ -203,9 +154,9 @@ function searchPlayWOTags() { //make a search and play using entered terms }, flags: ["FLAG_ACTIVITY_NEW_TASK"] })); -} +}; -function gadgetbridgeWake() { +let gadgetbridgeWake = function() { Bluetooth.println(JSON.stringify({ t: "intent", target: "activity", @@ -213,15 +164,15 @@ function gadgetbridgeWake() { package: "gadgetbridge", class: "nodomain.freeyourgadget.gadgetbridge.activities.WakeActivity" })); -} +}; // For stringing together the action for Podcast Addict to perform -function actFn(actName, activOrServ) { +let actFn = function(actName, activOrServ) { return "com.bambuna.podcastaddict." + (activOrServ == "service" ? "service." : "") + actName; -} +}; // Send the intent message to Gadgetbridge -function btMsg(activOrServ, cls, actName, xtra) { +let btMsg = function(activOrServ, cls, actName, xtra) { Bluetooth.println(JSON.stringify({ t: "intent", @@ -231,22 +182,20 @@ function btMsg(activOrServ, cls, actName, xtra) { target: "broadcastreceiver", extra: xtra })); -} +}; // Get back to the main layout -function backToGfx() { +let backToGfx = function() { E.showMenu(); g.clear(); g.reset(); - Bangle.removeAllListeners("touch"); - Bangle.removeAllListeners("swipe"); setUI(); gfx(); backToMenu = false; -} +}; // Podcast Addict Menu -var paMenu = { +let paMenu = { "": { title: " ", back: backToGfx @@ -271,7 +220,7 @@ var paMenu = { }; -var controlMenu = { +let controlMenu = { "": { title: " ", back: () => {if (backToMenu) E.showMenu(paMenu); @@ -310,7 +259,7 @@ var controlMenu = { }, }; -var speedMenu = { +let speedMenu = { "": { title: " ", back: () => {if (backToMenu) E.showMenu(paMenu); @@ -333,7 +282,7 @@ var speedMenu = { //"Slower" : ()=>{speed-=0.1; speed=((speed<0.1)?0.1:speed); btMsg("service",standardCls,"player.customspeed",{arg1:speed});}, }; -var searchMenu = { +let searchMenu = { "": { title: " ", @@ -356,7 +305,7 @@ var searchMenu = { "Simpler search and play" : searchPlayWOTags, }; -var navigationMenu = { +let navigationMenu = { "": { title: " ", back: () => {if (backToMenu) E.showMenu(paMenu); @@ -372,4 +321,6 @@ var navigationMenu = { Bangle.loadWidgets(); setUI(); +widgetUtils.hide(); gfx(); +} diff --git a/apps/podadrem/metadata.json b/apps/podadrem/metadata.json index 929269762..c58b9241d 100644 --- a/apps/podadrem/metadata.json +++ b/apps/podadrem/metadata.json @@ -2,7 +2,7 @@ "id": "podadrem", "name": "Podcast Addict Remote", "shortName": "PA Remote", - "version": "0.05", + "version": "0.07", "description": "Control Podcast Addict on your android device.", "readme": "README.md", "type": "app", diff --git a/apps/presentor/metadata.json b/apps/presentor/metadata.json index e5b5e289f..2d0a22102 100644 --- a/apps/presentor/metadata.json +++ b/apps/presentor/metadata.json @@ -12,7 +12,8 @@ "allow_emulator": true, "storage": [ {"name":"presentor.app.js","url":"app.js"}, - {"name":"presentor.img","url":"app-icon.js","evaluate":true}, + {"name":"presentor.img","url":"app-icon.js","evaluate":true} + ], "data": [ {"name":"presentor.json","url":"settings.json"} ] } diff --git a/apps/primetimelato/ChangeLog b/apps/primetimelato/ChangeLog index 46690e360..7781e93a1 100644 --- a/apps/primetimelato/ChangeLog +++ b/apps/primetimelato/ChangeLog @@ -1,2 +1,4 @@ 0.01: first release 0.02: added option to buzz on prime, with settings +0.03: added option to debug settings and test fw 2.15.93 load speed ups +0.04: changed icon diff --git a/apps/primetimelato/README.md b/apps/primetimelato/README.md index 924a6fae6..0ffd5a3fa 100644 --- a/apps/primetimelato/README.md +++ b/apps/primetimelato/README.md @@ -1,4 +1,4 @@ -# Prime Time Lato (clock) +# Prime Lato (clock) A watchface that displays time and its prime factors in the Lato font. For example when the time is 21:05, the prime factors are 5,421. diff --git a/apps/primetimelato/app.js b/apps/primetimelato/app.js index 817da7cda..b4b9d5bb9 100644 --- a/apps/primetimelato/app.js +++ b/apps/primetimelato/app.js @@ -2,7 +2,8 @@ const h = g.getHeight(); const w = g.getWidth(); const SETTINGS_FILE = "primetimelato.json"; let settings; - +let setStr = 'U'; + Graphics.prototype.setFontLato = function(scale) { // Actual height 41 (43 - 3) this.setFontCustom( @@ -28,6 +29,22 @@ Graphics.prototype.setFontLatoSmall = function(scale) { function loadSettings() { settings = require("Storage").readJSON(SETTINGS_FILE,1)||{}; settings.buzz_on_prime = (settings.buzz_on_prime === undefined ? false : settings.buzz_on_prime); + settings.debug = (settings.debug === undefined ? false : settings.debug); + + switch(settings.buzz_on_prime) { + case true: + setStr = 'T'; + break; + + case false: + setStr = 'F'; + break; + + case undefined: + default: + setStr = 'U'; + break; + } } // creates a list of prime factors of n and outputs them as a string, if n is prime outputs "Prime Time!" @@ -69,9 +86,16 @@ function draw() { g.setColor(0,0,0); g.fillRect(Bangle.appRect); + g.setColor(100,100,100); + + if (settings.debug) { + g.setFontLatoSmall(); + g.setFontAlign(0, 0); + g.drawString(setStr, w/2, h/4); + } + g.setFontLato(); g.setFontAlign(0, 0); - g.setColor(100,100,100); g.drawString(timeStr, w/2, h/2); g.setFontLatoSmall(); diff --git a/apps/primetimelato/app.png b/apps/primetimelato/app.png index 2a84c62a0..e5762b97c 100644 Binary files a/apps/primetimelato/app.png and b/apps/primetimelato/app.png differ diff --git a/apps/primetimelato/icon.js b/apps/primetimelato/icon.js index 06f93e2ef..7c8d5380b 100644 --- a/apps/primetimelato/icon.js +++ b/apps/primetimelato/icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwwIdah/wAof//4ECgYFB4AFBg4FB8AFBj/wh/4AoM/wEB/gFBvwCEBAU/AQP4gfAj8AgPwAoMPwED8AFBg/AAYIBDA4ngg4TB4EBApkPKgJSBJQIFTMgIFCJIIFDKoIFEvgFBGoMAnw7DP4IFEh+BAoItBg+DNIQwBMIaeCKoKxCPoIzCEgKVHUIqtFXIrFFaIrdFdIwAV")) +require("heatshrink").decompress(atob("mEw4X/AAIHB8cYttrJf4AR1gKJgdYBZMCBZdcBZMNsALKuALJhNABZMFwALJmvAAwkOqvAmtAkwSF83+uEV4EMOIpZBznWII5NB7mXGo5BB7Z0HkpBB6x0HFYXVNA4rC6pcFAANXDQhSFqgaEBZGYaBLfIaAUBBZUJNQ4jCm+cHZPcBZFXgdwzELBg1W/PAy/rBY3VPAOVTY863kAnaPHAH4A/ADAA==")) diff --git a/apps/primetimelato/metadata.json b/apps/primetimelato/metadata.json index 6b292c380..400220b10 100644 --- a/apps/primetimelato/metadata.json +++ b/apps/primetimelato/metadata.json @@ -1,6 +1,6 @@ { "id": "primetimelato", - "name": "Prime Time Lato Clock", - "version": "0.02", + "name": "Prime Lato", + "version": "0.04", "type": "clock", "description": "A clock that tells you the primes of the time in the Lato font", "icon": "app.png", diff --git a/apps/primetimelato/settings.js b/apps/primetimelato/settings.js index 5550055eb..069c976c8 100644 --- a/apps/primetimelato/settings.js +++ b/apps/primetimelato/settings.js @@ -3,7 +3,8 @@ // initialize with default settings... let s = { - 'buzz_on_prime': true + 'buzz_on_prime': true, + 'debug': false } // ...and overwrite them with any saved values @@ -29,6 +30,16 @@ s.buzz_on_prime = v; save(); }, + }, + + 'Debug': { + value: !!s.debug, + onchange: v => { + s.debug = v; + save(); + }, } + + }) }) diff --git a/apps/qcenter/ChangeLog b/apps/qcenter/ChangeLog new file mode 100644 index 000000000..900b9017c --- /dev/null +++ b/apps/qcenter/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! +0.02: Fix fast loading on swipe to clock diff --git a/apps/qcenter/README.md b/apps/qcenter/README.md new file mode 100644 index 000000000..4931b9c7f --- /dev/null +++ b/apps/qcenter/README.md @@ -0,0 +1,20 @@ +# Quick Center + +An app with a status bar showing various information and up to six shortcuts for your favorite apps! +Designed for use with any kind of quick launcher, such as Quick Launch or Pattern Launcher. + +![](screenshot.png) + +## Usage + +Pin your apps with settings, then launch them with your favorite quick launcher to access them quickly. +If you don't have any apps pinned, the settings and about apps will be shown as an example. + +## Features + +Battery and GPS status display (for now) +Up to six shortcuts to your favorite apps + +## Upcoming features +- Quick switches for toggleable features such as Bluetooth or HID mode +- Customizable status information \ No newline at end of file diff --git a/apps/qcenter/app-icon.js b/apps/qcenter/app-icon.js new file mode 100644 index 000000000..bfc94d10a --- /dev/null +++ b/apps/qcenter/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4UB6cA/4ACBYNVAElQHAsFBYZFHCxIYEoALHgILNOxILChWqAAmgBYNUBZMVBYIAIBc0C1WAlWoAgQL/O96D/Qf4LZqoLJqoLMoAKHgILNqALHgoLBGBAKCDA4WDAEQA=")) \ No newline at end of file diff --git a/apps/qcenter/app.js b/apps/qcenter/app.js new file mode 100644 index 000000000..be28db3b6 --- /dev/null +++ b/apps/qcenter/app.js @@ -0,0 +1,129 @@ +{ +require("Font8x12").add(Graphics); + +// load pinned apps from config +let settings = require("Storage").readJSON("qcenter.json", 1) || {}; +let pinnedApps = settings.pinnedApps || []; +let exitGesture = settings.exitGesture || "swipeup"; + +// if empty load a default set of apps as an example +if (pinnedApps.length == 0) { + pinnedApps = [ + { src: "setting.app.js", icon: "setting.img" }, + { src: "about.app.js", icon: "about.img" }, + ]; +} + +// button drawing from Layout.js, edited to have completely custom button size with icon +let drawButton = function(l) { + let x = l.x + (0 | l.pad), + y = l.y + (0 | l.pad), + w = l.w - (l.pad << 1), + h = l.h - (l.pad << 1); + let poly = [ + x, + y + 4, + x + 4, + y, + x + w - 5, + y, + x + w - 1, + y + 4, + x + w - 1, + y + h - 5, + x + w - 5, + y + h - 1, + x + 4, + y + h - 1, + 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); + 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) } + ); +} + +// function to split array into group of 3, for button placement +let groupBy3 = function(data) { + let result = []; + for (let i = 0; i < data.length; i += 3) result.push(data.slice(i, i + 3)); + return result; +} + +// generate object with buttons for apps by group of 3 +let appButtons = groupBy3(pinnedApps).map((appGroup, i) => { + return appGroup.map((app, j) => { + return { + type: "custom", + render: drawButton, + width: 50, + height: 50, + pad: 5, + src: require("Storage").read(app.icon), + scale: 0.75, + cb: (l) => load(app.src), + }; + }); +}); + +// create basic layout content with status info and sensor status on top +let layoutContent = [ + { + type: "h", + pad: 5, + fillx: 1, + c: [ + { type: "txt", font: "8x12", pad: 3, scale: 2, label: E.getBattery() + "%" }, + { type: "txt", font: "8x12", pad: 3, scale: 2, label: "GPS: " + (Bangle.isGPSOn() ? "ON" : "OFF") }, + ], + }, +]; + +// create rows for buttons and add them to layoutContent +appButtons.forEach((appGroup) => { + layoutContent.push({ + type: "h", + pad: 2, + c: appGroup, + }); +}); + +// create layout with content + +Bangle.loadWidgets(); + +let Layout = require("Layout"); +let layout = new Layout({ + type: "v", + c: layoutContent +}, { + remove: ()=>{ + Bangle.removeListener("swipe", onSwipe); + delete Graphics.prototype.setFont8x12; + } +}); +g.clear(); +layout.render(); +Bangle.drawWidgets(); + +// swipe event listener for exit gesture +let onSwipe = function (lr, ud) { + if(exitGesture == "swipeup" && ud == -1) Bangle.showClock(); + if(exitGesture == "swipedown" && ud == 1) Bangle.showClock(); + if(exitGesture == "swipeleft" && lr == -1) Bangle.showClock(); + if(exitGesture == "swiperight" && lr == 1) Bangle.showClock(); +} + +Bangle.on("swipe", onSwipe); +} diff --git a/apps/qcenter/app.png b/apps/qcenter/app.png new file mode 100644 index 000000000..27ec75f1c Binary files /dev/null and b/apps/qcenter/app.png differ diff --git a/apps/qcenter/metadata.json b/apps/qcenter/metadata.json new file mode 100644 index 000000000..a325de10f --- /dev/null +++ b/apps/qcenter/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "qcenter", + "name": "Quick Center", + "shortName": "QCenter", + "version": "0.02", + "description": "An app for quickly launching your favourite apps, inspired by the control centres of other watches.", + "icon": "app.png", + "tags": "", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "screenshots": [{ "url": "screenshot.png" }], + "storage": [ + { "name": "qcenter.app.js", "url": "app.js" }, + { "name": "qcenter.settings.js", "url": "settings.js" }, + { "name": "qcenter.img", "url": "app-icon.js", "evaluate": true } + ], + "data": [{"name":"qcenter.json"}] +} diff --git a/apps/qcenter/screenshot.png b/apps/qcenter/screenshot.png new file mode 100644 index 000000000..8c0a335aa Binary files /dev/null and b/apps/qcenter/screenshot.png differ diff --git a/apps/qcenter/settings.js b/apps/qcenter/settings.js new file mode 100644 index 000000000..2c97f8a5f --- /dev/null +++ b/apps/qcenter/settings.js @@ -0,0 +1,133 @@ +// make sure to enclose the function in parentheses +(function (back) { + let settings = require("Storage").readJSON("qcenter.json", 1) || {}; + 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, + icon: a.icon, + } + ); + }) + .filter( + (app) => + app && + (app.type == "app" || + app.type == "launch" || + app.type == "clock" || + !app.type) + ); + apps.sort((a, b) => { + var n = (0 | a.sortorder) - (0 | b.sortorder); + if (n) return n; // do sortorder first + if (a.name < b.name) return -1; + if (a.name > b.name) return 1; + return 0; + }); + + function save(key, value) { + settings[key] = value; + require("Storage").write("qcenter.json", settings); + } + + var pinnedApps = settings.pinnedApps || []; + var exitGesture = settings.exitGesture || "swipeup"; + + function showMainMenu() { + var mainmenu = { + "": { title: "Quick Center", back: back}, + }; + + // Set exit gesture + mainmenu["Exit Gesture: " + exitGesture] = function () { + E.showMenu(exitGestureMenu); + }; + + //List all pinned apps, redirecting to menu with options to unpin and reorder + pinnedApps.forEach((app, i) => { + mainmenu[app.name] = function () { + E.showMenu({ + "": { title: app.name, back: showMainMenu }, + "Unpin": () => { + pinnedApps.splice(i, 1); + save("pinnedApps", pinnedApps); + showMainMenu(); + }, + "Move Up": () => { + if (i > 0) { + pinnedApps.splice(i - 1, 0, pinnedApps.splice(i, 1)[0]); + save("pinnedApps", pinnedApps); + showMainMenu(); + } + }, + "Move Down": () => { + if (i < pinnedApps.length - 1) { + pinnedApps.splice(i + 1, 0, pinnedApps.splice(i, 1)[0]); + save("pinnedApps", pinnedApps); + showMainMenu(); + } + }, + }); + }; + }); + + // Show pin app menu, or show alert if max amount of apps are pinned + mainmenu["Pin App"] = function () { + if (pinnedApps.length < 6) { + E.showMenu(pinAppMenu); + } else { + E.showAlert("Max apps pinned").then(showMainMenu); + } + }; + + return E.showMenu(mainmenu); + } + + // menu for adding apps to the quick launch menu, listing all apps + var pinAppMenu = { + "": { title: "Add App", back: showMainMenu } + }; + apps.forEach((a) => { + pinAppMenu[a.name] = function () { + // strip unncecessary properties + delete a.type; + delete a.sortorder; + pinnedApps.push(a); + save("pinnedApps", pinnedApps); + showMainMenu(); + }; + }); + + // menu for setting exit gesture + var exitGestureMenu = { + "": { title: "Exit Gesture", back: showMainMenu } + }; + exitGestureMenu["Swipe Up"] = function () { + exitGesture = "swipeup"; + save("exitGesture", "swipeup"); + showMainMenu(); + }; + exitGestureMenu["Swipe Down"] = function () { + exitGesture = "swipedown"; + save("exitGesture", "swipedown"); + showMainMenu(); + }; + exitGestureMenu["Swipe Left"] = function () { + exitGesture = "swipeleft"; + save("exitGesture", "swipeleft"); + showMainMenu(); + }; + exitGestureMenu["Swipe Right"] = function () { + exitGesture = "swiperight"; + save("exitGesture", "swiperight"); + showMainMenu(); + }; + + showMainMenu(); +}); diff --git a/apps/rebble/ChangeLog b/apps/rebble/ChangeLog index c009a4ec1..78ba0c5da 100644 --- a/apps/rebble/ChangeLog +++ b/apps/rebble/ChangeLog @@ -9,7 +9,8 @@ 0.09: fix battery icon size 0.10: Tell clock widgets to hide. 0.11: fix issue https://github.com/espruino/BangleApps/issues/2128 (#2128) ( settings undefined ) -0.12: implemented widget_utils +0.12: implemented widget_utils 0.13: convert var/function into let -0.14: cleanup code and fix fastload issue -0.15: fix draw before widget hide \ No newline at end of file +0.14: cleanup code and fix fastload issue +0.15: fix draw before widget hide +0.16: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps diff --git a/apps/rebble/metadata.json b/apps/rebble/metadata.json index 7042fcb95..c380204a4 100644 --- a/apps/rebble/metadata.json +++ b/apps/rebble/metadata.json @@ -2,7 +2,7 @@ "id": "rebble", "name": "Rebble Clock", "shortName": "Rebble", - "version": "0.15", + "version": "0.16", "description": "A Pebble style clock, with configurable background, three sidebars including steps, day, date, sunrise, sunset, long live the rebellion", "readme": "README.md", "icon": "rebble.png", @@ -14,7 +14,6 @@ "storage": [ {"name":"rebble.app.js","url":"rebble.app.js"}, {"name":"rebble.settings.js","url":"rebble.settings.js"}, - {"name":"rebble.img","url":"rebble.icon.js","evaluate":true}, - {"name":"suncalc","url":"suncalc.js"} + {"name":"rebble.img","url":"rebble.icon.js","evaluate":true} ] } diff --git a/apps/rebble/rebble.app.js b/apps/rebble/rebble.app.js index fd0c8f7f9..445c30125 100644 --- a/apps/rebble/rebble.app.js +++ b/apps/rebble/rebble.app.js @@ -11,7 +11,7 @@ Graphics.prototype.setFontKdamThmor = function(scale) { { - let SunCalc = require("suncalc"); + let SunCalc = require("suncalc"); // from modules folder const SETTINGS_FILE = "rebble.json"; const LOCATION_FILE = "mylocation.json"; const GLOBAL_SETTINGS = "setting.json"; @@ -49,7 +49,7 @@ Graphics.prototype.setFontKdamThmor = function(scale) { } if(settings.sideTap!=0) - sideBar=parseInt(settings.sideTap)-1; //tab to + sideBar=parseInt(settings.sideTap)-1; //tab to is12Hour = (require("Storage").readJSON(GLOBAL_SETTINGS, 1) || {})["12hour"] || false; } @@ -110,15 +110,15 @@ Graphics.prototype.setFontKdamThmor = function(scale) { let date = new Date(); let hh = date.getHours(); let mm = date.getMinutes(); - + hh = formatHours(hh); mm = zeroPad(mm,2); - + //const t = 6; if (drawCount % 60 == 0) updateSunRiseSunSet(location.lat, location.lon); - + g.reset(); g.setColor(g.theme.bg); g.fillRect(0, 0, w2, h); @@ -143,7 +143,7 @@ Graphics.prototype.setFontKdamThmor = function(scale) { drawSideBar3(); break; } - + drawCount++; queueDraw(); } @@ -154,14 +154,14 @@ Graphics.prototype.setFontKdamThmor = function(scale) { let dd=date.getDate(); let mm=require("date_utils").month(date.getMonth()+1,1).toUpperCase(); - + drawBattery(w2 + (w-w2-wb)/2, h/10, wb, 17); setTextColor(); g.setFont('Vector', 20); g.setFontAlign(0, -1); g.drawString(E.getBattery() + '%', w3, (h/10) + 17 + 7); - + drawDateAndCalendar(w3, h/2, dy, dd, mm); } @@ -250,7 +250,7 @@ Graphics.prototype.setFontKdamThmor = function(scale) { } } - + // format steps so they fit in the place let formatSteps=function() { let s = Bangle.getHealthStatus("day").steps; @@ -292,8 +292,8 @@ Graphics.prototype.setFontKdamThmor = function(scale) { - let chargingListener= function(charging) { - + let chargingListener= function(charging) { + //redraw the sidebar ( with the battery ) switch(sideBar) { case 0: @@ -304,7 +304,7 @@ Graphics.prototype.setFontKdamThmor = function(scale) { break; } } - + let deleteAll=function() { // Called to unload all of the clock app @@ -320,7 +320,7 @@ Graphics.prototype.setFontKdamThmor = function(scale) { log_debug("starting.."); loadSettings(); loadLocation(); - + if(settings.autoCycle || settings.sideTap==0) { Bangle.setUI({ @@ -332,7 +332,7 @@ Graphics.prototype.setFontKdamThmor = function(scale) { if (btn>0) nextSidebar(); draw(); }); - + } else{ Bangle.setUI({ @@ -354,4 +354,4 @@ Graphics.prototype.setFontKdamThmor = function(scale) { main(); -} \ No newline at end of file +} diff --git a/apps/rebble/suncalc.js b/apps/rebble/suncalc.js deleted file mode 100644 index d86f039c5..000000000 --- a/apps/rebble/suncalc.js +++ /dev/null @@ -1,143 +0,0 @@ -/* - (c) 2011-2015, Vladimir Agafonkin - SunCalc is a JavaScript library for calculating sun/moon position and light phases. - https://github.com/mourner/suncalc - - edit for banglejs -*/ - -(function () { 'use strict'; - -// shortcuts for easier to read formulas - -var PI = Math.PI, - sin = Math.sin, - cos = Math.cos, - tan = Math.tan, - asin = Math.asin, - atan = Math.atan2, - acos = Math.acos, - rad = PI / 180; - -// sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas - - -// date/time constants and conversions - -var dayMs = 1000 * 60 * 60 * 24, - J1970 = 2440588, - J2000 = 2451545; - -function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; } -function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); } -function toDays(date) { return toJulian(date) - J2000; } - - -// general calculations for position - -var e = rad * 23.4397; // obliquity of the Earth - -function declination(l, b) { return asin(sin(b) * cos(e) + cos(b) * sin(e) * sin(l)); } - - -// general sun calculations - -function solarMeanAnomaly(d) { return rad * (357.5291 + 0.98560028 * d); } - -function eclipticLongitude(M) { - - var C = rad * (1.9148 * sin(M) + 0.02 * sin(2 * M) + 0.0003 * sin(3 * M)), // equation of center - P = rad * 102.9372; // perihelion of the Earth - - return M + C + P + PI; -} - -var SunCalc = {}; - - -// sun times configuration (angle, morning name, evening name) - -var times = SunCalc.times = [ - [-0.833, 'sunrise', 'sunset' ], - [ -0.3, 'sunriseEnd', 'sunsetStart' ], - [ -6, 'dawn', 'dusk' ], - [ -12, 'nauticalDawn', 'nauticalDusk'], - [ -18, 'nightEnd', 'night' ], - [ 6, 'goldenHourEnd', 'goldenHour' ] -]; - - - -// calculations for sun times - -var J0 = 0.0009; - -function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); } - -function approxTransit(Ht, lw, n) { return J0 + (Ht + lw) / (2 * PI) + n; } -function solarTransitJ(ds, M, L) { return J2000 + ds + 0.0053 * sin(M) - 0.0069 * sin(2 * L); } - -function hourAngle(h, phi, d) { return acos((sin(h) - sin(phi) * sin(d)) / (cos(phi) * cos(d))); } -function observerAngle(height) { return -2.076 * Math.sqrt(height) / 60; } - -// returns set time for the given sun altitude -function getSetJ(h, lw, phi, dec, n, M, L) { - - var w = hourAngle(h, phi, dec), - a = approxTransit(w, lw, n); - return solarTransitJ(a, M, L); -} - - -// calculates sun times for a given date, latitude/longitude, and, optionally, -// the observer height (in meters) relative to the horizon - -SunCalc.getTimes = function (date, lat, lng, height) { - - height = height || 0; - - var lw = rad * -lng, - phi = rad * lat, - - dh = observerAngle(height), - - d = toDays(date), - n = julianCycle(d, lw), - ds = approxTransit(0, lw, n), - - M = solarMeanAnomaly(ds), - L = eclipticLongitude(M), - dec = declination(L, 0), - - Jnoon = solarTransitJ(ds, M, L), - - i, len, time, h0, Jset, Jrise; - - - var result = { - solarNoon: fromJulian(Jnoon), - nadir: fromJulian(Jnoon - 0.5) - }; - - for (i = 0, len = times.length; i < len; i += 1) { - time = times[i]; - h0 = (time[0] + dh) * rad; - - Jset = getSetJ(h0, lw, phi, dec, n, M, L); - Jrise = Jnoon - (Jset - Jnoon); - - result[time[1]] = fromJulian(Jrise); - result[time[2]] = fromJulian(Jset); - } - - return result; -}; - - -// export as Node module / AMD module / browser variable -if (typeof exports === 'object' && typeof module !== 'undefined') module.exports = SunCalc; -else if (typeof define === 'function' && define.amd) define(SunCalc); -else window.SunCalc = SunCalc; - - -}()); \ No newline at end of file diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index 1941a435b..a4d0b7e88 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -21,3 +21,6 @@ 0.15: Show distance more accurately in conjunction with new locale app (fix #1523) 0.16: Ability to append to existing track (fix #1712) 0.17: Use default Bangle formatter for booleans +0.18: Improve widget load speed, allow currently recording track to be plotted in openstmap +0.19: Fix track plotting code +0.20: Automatic translation of some more strings. diff --git a/apps/recorder/app.js b/apps/recorder/app.js index 9006d2236..8dcc4c3ed 100644 --- a/apps/recorder/app.js +++ b/apps/recorder/app.js @@ -61,7 +61,7 @@ function showMainMenu() { setTimeout(function() { E.showMenu(); WIDGETS["recorder"].setRecording(v).then(function() { - print("Complete"); + print(/*LANG*/"Complete"); loadSettings(); print(settings.recording); showMainMenu(); @@ -96,7 +96,7 @@ function showMainMenu() { }; var recorders = WIDGETS["recorder"].getRecorders(); Object.keys(recorders).forEach(id=>{ - mainmenu["Log "+recorders[id]().name] = menuRecord(id); + mainmenu[/*LANG*/"Log "+recorders[id]().name] = menuRecord(id); }); delete recorders; return E.showMenu(mainmenu); @@ -404,7 +404,7 @@ function viewTrack(filename, info) { title: title, miny: min, maxy: max, - xlabel : x=>Math.round(x*dur/(60*infn.length))+" min" // minutes + xlabel : x=>Math.round(x*dur/(60*infn.length))+/*LANG*/" min" // minutes }); g.setFont("6x8",2); g.setFontAlign(0,0,3); diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json index a7eb09cd5..91ceaf86e 100644 --- a/apps/recorder/metadata.json +++ b/apps/recorder/metadata.json @@ -2,7 +2,7 @@ "id": "recorder", "name": "Recorder", "shortName": "Recorder", - "version": "0.17", + "version": "0.20", "description": "Record GPS position, heart rate and more in the background, then download to your PC.", "icon": "app.png", "tags": "tool,outdoors,gps,widget", diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js index be714c19d..87d4fb012 100644 --- a/apps/recorder/widget.js +++ b/apps/recorder/widget.js @@ -1,10 +1,10 @@ -(() => { - var storageFile; // file for GPS track - var entriesWritten = 0; - var activeRecorders = []; - var writeInterval; +{ + let storageFile; // file for GPS track + let entriesWritten = 0; + let activeRecorders = []; + let writeInterval; - function loadSettings() { + let loadSettings = function() { var settings = require("Storage").readJSON("recorder.json",1)||{}; settings.period = settings.period||10; if (!settings.file || !settings.file.startsWith("recorder.log")) @@ -12,12 +12,12 @@ return settings; } - function updateSettings(settings) { + let updateSettings = function(settings) { require("Storage").writeJSON("recorder.json", settings); if (WIDGETS["recorder"]) WIDGETS["recorder"].reload(); } - function getRecorders() { + let getRecorders = function() { var recorders = { gps:function() { var lat = 0; @@ -159,7 +159,7 @@ return recorders; } - function writeLog() { + let writeLog = function() { entriesWritten++; WIDGETS["recorder"].draw(); try { @@ -178,7 +178,7 @@ } // Called by the GPS app to reload settings and decide what to do - function reload() { + let reload = function() { var settings = loadSettings(); if (writeInterval) clearInterval(writeInterval); writeInterval = undefined; @@ -224,7 +224,7 @@ // add the widget WIDGETS["recorder"]={area:"tl",width:0,draw:function() { if (!writeInterval) return; - g.reset(); g.drawImage(atob("DRSBAAGAHgDwAwAAA8B/D/hvx38zzh4w8A+AbgMwGYDMDGBjAA=="),this.x+1,this.y+2); + g.reset().drawImage(atob("DRSBAAGAHgDwAwAAA8B/D/hvx38zzh4w8A+AbgMwGYDMDGBjAA=="),this.x+1,this.y+2); activeRecorders.forEach((recorder,i)=>{ recorder.draw(this.x+15+(i>>1)*12, this.y+(i&1)*12); }); @@ -265,23 +265,39 @@ updateSettings(settings); WIDGETS["recorder"].reload(); return Promise.resolve(settings.recording); - }/*,plotTrack:function(m) { // m=instance of openstmap module - // if we're here, settings was already loaded - var f = require("Storage").open(settings.file,"r"); - var l = f.readLine(f); - if (l===undefined) return; - var c = l.split(","); - var mp = m.latLonToXY(+c[1], +c[2]); - g.moveTo(mp.x,mp.y); - l = f.readLine(f); - while(l!==undefined) { - c = l.split(","); - mp = m.latLonToXY(+c[1], +c[2]); - g.lineTo(mp.x,mp.y); - g.fillCircle(mp.x,mp.y,2); // make the track more visible + },plotTrack:function(m) { // m=instance of openstmap module + // Plots the current track in the currently set color + if (!activeRecorders.length) return; // not recording + var settings = loadSettings(); + // keep function to draw track in RAM + function plot(g) { "ram"; + var f = require("Storage").open(settings.file,"r"); + var l = f.readLine(); + if (l===undefined) return; // empty file? + var mp, c = l.split(","); + var la=c.indexOf("Latitude"),lo=c.indexOf("Longitude"); + if (la<0 || lo<0) return; // no GPS! + l = f.readLine();c=[]; + while (l && !c[la]) { + c = l.split(","); + l = f.readLine(f); + } + if (l===undefined) return; // empty file? + mp = m.latLonToXY(+c[la], +c[lo]); + g.moveTo(mp.x,mp.y); l = f.readLine(f); + var n = 200; // only plot first 200 points to keep things fast(ish) + while(l && n--) { + c = l.split(","); + if (c[la]) { + mp = m.latLonToXY(+c[la], +c[lo]); + g.lineTo(mp.x,mp.y); + } + l = f.readLine(f); + } } - }*/}; + plot(g); + }}; // load settings, set correct widget width reload(); -})() +} diff --git a/apps/sched/ChangeLog b/apps/sched/ChangeLog index f23cf93cb..b24ba9266 100644 --- a/apps/sched/ChangeLog +++ b/apps/sched/ChangeLog @@ -1,6 +1,6 @@ 0.01: New App! 0.02: Fix scheduling of other alarms if there is a pending alarm from the past (fix #1667) -0.03: Fix `getTimeToAlarm` for a timer already used at same day, don't set `last` for timers. +0.03: Fix `getTimeToAlarm` for a timer already used at same day, don't set `last` for timers. 0.04: Fix `getTimeToAlarm` to check for next dow if alarm.t lower currentTime. 0.05: Export new functions (`newDefaultAlarm/Timer`), add Settings page 0.06: Refactor some methods to library @@ -14,3 +14,8 @@ Improve timer message using formatDuration Fix wrong fallback for buzz pattern 0.13: Ask to delete a timer after stopping it +0.14: Added clkinfo for alarms and timers +0.15: Automatic translation of some string in clkinfo +0.16: Improve support for date timezone +0.17: Fix midnight in local timezone (alarms wouldn't always fire as expected in timezone != 0) +0.18: Update clock_info to avoid a redraw diff --git a/apps/sched/boot.js b/apps/sched/boot.js index 98bb0ff7d..d736dd0e7 100644 --- a/apps/sched/boot.js +++ b/apps/sched/boot.js @@ -13,7 +13,7 @@ && (a.last != d) // not already fired today && (a.t + 60000 > currentTime) // is not in the past by >1 minute && (a.dow >> time.getDay() & 1) // is allowed on this day of the week - && (!a.date || a.date == time.toISOString().substr(0, 10)) // is allowed on this date + && (!a.date || a.date == time.toLocalISOString().substr(0, 10)) // is allowed on this date ); if (active.length) { active = active.sort((a,b)=>a.t-b.t); // sort by time @@ -26,7 +26,7 @@ If active[0].js is defined, just run that code as-is and not alarm.js */ Bangle.SCHED = setTimeout(active[0].js||'load("sched.js")',t); } else { // check for new alarms at midnight (so day of week works) - Bangle.SCHED = setTimeout('eval(require("Storage").read("sched.boot.js"))', 86400000 - (Date.now()%86400000)); + Bangle.SCHED = setTimeout('eval(require("Storage").read("sched.boot.js"))', 86400000 - currentTime); } })(); /* DEBUGGING diff --git a/apps/sched/clkinfo.js b/apps/sched/clkinfo.js new file mode 100644 index 000000000..439784039 --- /dev/null +++ b/apps/sched/clkinfo.js @@ -0,0 +1,73 @@ +(function() { + const alarm = require('sched'); + const iconAlarmOn = atob("GBiBAAAAAAAAAAYAYA4AcBx+ODn/nAP/wAf/4A/n8A/n8B/n+B/n+B/n+B/n+B/h+B/4+A/+8A//8Af/4AP/wAH/gAB+AAAAAAAAAA=="); + const iconAlarmOff = atob("GBiBAAAAAAAAAAYAYA4AcBx+ODn/nAP/wAf/4A/n8A/n8B/n+B/n+B/nAB/mAB/geB/5/g/5tg/zAwfzhwPzhwHzAwB5tgAB/gAAeA=="); + const iconTimerOn = atob("GBiBAAAAAAAAAAAAAAf/4Af/4AGBgAGBgAGBgAD/AAD/AAB+AAA8AAA8AAB+AADnAADDAAGBgAGBgAGBgAf/4Af/4AAAAAAAAAAAAA=="); + const iconTimerOff = atob("GBiBAAAAAAAAAAAAAAf/4Af/4AGBgAGBgAGBgAD/AAD/AAB+AAA8AAA8AAB+AADkeADB/gGBtgGDAwGDhwfzhwfzAwABtgAB/gAAeA=="); + + //from 0 to max, the higher the closer to fire (as in a progress bar) + function getAlarmValue(a){ + let min = Math.round(alarm.getTimeToAlarm(a)/(60*1000)); + if(!min) return 0; //not active or more than a day + return getAlarmMax(a)-min; + } + + function getAlarmMax(a) { + if(a.timer) + return Math.round(a.timer/(60*1000)); + //minutes cannot be more than a full day + return 1440; + } + + function getAlarmIcon(a) { + if(a.on) { + if(a.timer) return iconTimerOn; + return iconAlarmOn; + } else { + if(a.timer) return iconTimerOff; + return iconAlarmOff; + } + } + + function getAlarmText(a){ + if(a.timer) { + if(!a.on) return /*LANG*/"off"; + let time = Math.round(alarm.getTimeToAlarm(a)/(60*1000)); + if(time > 60) + time = Math.round(time / 60) + "h"; + else + time += "m"; + return time; + } + return require("time_utils").formatTime(a.t); + } + + //workaround for sorting undefined values + function getAlarmOrder(a) { + let val = alarm.getTimeToAlarm(a); + if(typeof val == "undefined") return 86400*1000; + return val; + } + + var img = iconAlarmOn; + //get only alarms not created by other apps + var alarmItems = { + name: /*LANG*/"Alarms", + img: img, + dynamic: true, + items: alarm.getAlarms().filter(a=>!a.appid) + //.sort((a,b)=>alarm.getTimeToAlarm(a)-alarm.getTimeToAlarm(b)) + .sort((a,b)=>getAlarmOrder(a)-getAlarmOrder(b)) + .map((a, i)=>({ + name: null, + hasRange: true, + get: () => ({ text: getAlarmText(a), img: getAlarmIcon(a), + v: getAlarmValue(a), min:0, max:getAlarmMax(a)}), + show: function() {}, + hide: function () {}, + run: function() { } + })), + }; + + return alarmItems; +}) diff --git a/apps/sched/lib.js b/apps/sched/lib.js index 74018dcde..c8961b9e3 100644 --- a/apps/sched/lib.js +++ b/apps/sched/lib.js @@ -21,7 +21,7 @@ exports.getActiveAlarms = function (alarms, time) { && (a.last != time.getDate()) // not already fired today && (a.t < currentTime) && (a.dow >> time.getDay() & 1) // is allowed on this day of the week - && (!a.date || a.date == time.toISOString().substr(0, 10)) // is allowed on this date + && (!a.date || a.date == time.toLocalISOString().substr(0, 10)) // is allowed on this date ) .sort((a, b) => a.t - b.t); } @@ -46,7 +46,7 @@ exports.getTimeToAlarm = function(alarm, time) { if (!alarm) return undefined; if (!time) time = new Date(); var currentTime = (time.getHours()*3600000)+(time.getMinutes()*60000)+(time.getSeconds()*1000); - var active = alarm.on && (alarm.dow>>((time.getDay()+(alarm.t>((time.getDay()+(alarm.t ["Off", "Alarms", "Silent"][v%3], + format: v => [/*LANG*/"Off", /*LANG*/"Alarms", /*LANG*/"Silent"][v%3], onchange: v => { settings.quiet = v%3; updateSettings(); @@ -162,9 +162,9 @@ function showAlertsMenu() { function showBLEMenu() { var hidV = [false, "kbmedia", "kb", "com", "joy"]; - var hidN = ["Off", "Kbrd & Media", "Kbrd", "Kbrd & Mouse" ,"Joystick"]; + var hidN = [/*LANG*/"Off", /*LANG*/"Kbrd & Media", /*LANG*/"Kbrd", /*LANG*/"Kbrd & Mouse", /*LANG*/"Joystick"]; E.showMenu({ - '': { 'title': 'Bluetooth' }, + '': { 'title': /*LANG*/'Bluetooth' }, '< Back': ()=>showMainMenu(), /*LANG*/'Make Connectable': ()=>makeConnectable(), /*LANG*/'BLE': { @@ -193,11 +193,11 @@ function showBLEMenu() { } }, /*LANG*/'Passkey BETA': { - value: settings.passkey?settings.passkey:"none", + value: settings.passkey?settings.passkey:/*LANG*/"none", onchange: () => setTimeout(showPasskeyMenu) // graphical_menu redraws after the call }, /*LANG*/'Whitelist': { - value: settings.whitelist?(settings.whitelist.length+" devs"):"off", + value: settings.whitelist?(settings.whitelist.length+/*LANG*/" devs"):/*LANG*/"off", onchange: () => setTimeout(showWhitelistMenu) // graphical_menu redraws after the call } }); @@ -606,7 +606,7 @@ function showUtilMenu() { menu[/*LANG*/'Reset Settings'] = () => { E.showPrompt(/*LANG*/'Reset to Defaults?',{title:/*LANG*/"Settings"}).then((v) => { if (v) { - E.showMessage('Resetting'); + E.showMessage(/*LANG*/'Resetting'); resetSettings(); setTimeout(showMainMenu, 50); } else showUtilMenu(); @@ -824,6 +824,7 @@ function showAppSettings(app) { function showTouchscreenCalibration() { Bangle.setUI(); + require('widget_utils').hide(); // disable touchscreen calibration (passed coords right through) Bangle.setOptions({touchX1: 0, touchY1: 0, touchX2: g.getWidth(), touchY2: g.getHeight() }); @@ -847,7 +848,7 @@ function showTouchscreenCalibration() { g.drawLine(spot[0],spot[1]-32,spot[0],spot[1]+32); g.drawCircle(spot[0],spot[1], 16); var tapsLeft = (1-currentTry)*4+(4-currentCorner); - g.setFont("6x8:2").setFontAlign(0,0).drawString(tapsLeft+" taps\nto go", g.getWidth()/2, g.getHeight()/2); + g.setFont("6x8:2").setFontAlign(0,0).drawString(tapsLeft+/*LANG*/" taps\nto go", g.getWidth()/2, g.getHeight()/2); } function calcCalibration() { @@ -870,7 +871,7 @@ function showTouchscreenCalibration() { var s = storage.readJSON("setting.json",1)||{}; s.touch = calib; storage.writeJSON("setting.json",s); - g.setFont("6x8:2").setFontAlign(0,0).drawString("Calibrated!", g.getWidth()/2, g.getHeight()/2); + g.setFont("6x8:2").setFontAlign(0,0).drawString(/*LANG*/"Calibrated!", g.getWidth()/2, g.getHeight()/2); // now load the main menu again setTimeout(showLCDMenu, 500); } diff --git a/apps/simplestpp/README.md b/apps/simplestpp/README.md new file mode 100644 index 000000000..4b459bda1 --- /dev/null +++ b/apps/simplestpp/README.md @@ -0,0 +1,54 @@ +# Simplest++ Clock + +The simplest working clock, with fast load and clock_info + +![](screenshot1.png) +![](screenshot2.png) +![](screenshot3.png) + + +Simplest++ has 1 clock info menu that is displayed as a single line at the bottom of the screen. + +This provides a working demo of how to use the clock_info modules. + + +## Usage + +* When the screen is unlocked, tap at the bottom of the csreen on the information text. + It should change color showing it is selected. + +* Swipe up or down to cycle through the info screens that can be displayed + when you have finished tap again towards the centre of the screen to unselect. + +* Swipe left or right to change the type of info screens displayed (by default + there is only one type of data so this will have no effect) + +* Settings are saved automatically and reloaded along with the clock. + +## About Clock Info's + +* The clock info modules enable all clocks to add the display of information to the clock face. + +* The default clock_info module provides a display of battery %, Steps, Heart Rate and Altitude. + +* Installing the [Sunrise ClockInfo](https://banglejs.com/apps/?id=clkinfosunrise) adds Sunrise and Sunset times into the list of info's. + + +## References + +* [What is Fast Load and how does it work](http://www.espruino.com/Bangle.js+Fast+Load) + +* [Clock Info Tutorial](http://www.espruino.com/Bangle.js+Clock+Info) + +* [How to load modules through the IDE](https://github.com/espruino/BangleApps/blob/master/modules/README.md) + + +## With Thanks + +* Gordon for support +* David Peer for his work on BW Clock + + +Written by: [Hugh Barney](https://github.com/hughbarney) For support +and discussion please post in the [Bangle JS +Forum](http://forum.espruino.com/microcosms/1424/) diff --git a/apps/simplestpp/app.js b/apps/simplestpp/app.js new file mode 100644 index 000000000..c07fdbcbb --- /dev/null +++ b/apps/simplestpp/app.js @@ -0,0 +1,108 @@ +/** + * + * Simplestpp Clock + * + * The entire clock code is contained within the block below this + * supports 'fast load' + * + * To add support for clock_info_supprt we add the code marked at [1] and [2] + * + */ + +{ + // must be inside our own scope here so that when we are unloaded everything disappears + // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global + + let draw = function() { + var date = new Date(); + var timeStr = require("locale").time(date,1); + var h = g.getHeight(); + var w = g.getWidth(); + + g.reset(); + g.setColor(g.theme.bg); + g.fillRect(Bangle.appRect); + + g.setFont('Vector', w/3); + g.setFontAlign(0, 0); + g.setColor(g.theme.fg); + g.drawString(timeStr, w/2, h/2); + clockInfoMenu.redraw(); // clock_info_support + + // schedule a draw for the next minute + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); + }; + + /** + * clock_info_support + * this is the callback function that get invoked by clockInfoMenu.redraw(); + * + * We will display the image and text on the same line and centre the combined + * length of the image+text + * + * + */ + let clockInfoDraw = (itm, info, options) => { + + g.reset().setFont('Vector',24).setBgColor(options.bg).setColor(options.fg); + + //use info.text.toString(), steps does not have length defined + var text_w = g.stringWidth(info.text.toString()); + // gap between image and text + var gap = 10; + // width of the image and text combined + var w = gap + (info.img ? 24 :0) + text_w; + // different fg color if we tapped on the menu + if (options.focus) g.setColor(options.hl); + + // clear the whole info line + g.clearRect(0, options.y -1, g.getWidth(), options.y+24); + + // draw the image if we have one + if (info.img) { + // image start + var x = (g.getWidth() / 2) - (w/2); + g.drawImage(info.img, x, options.y); + // draw the text to the side of the image (left/centre alignment) + g.setFontAlign(-1,0).drawString(info.text, x + 23 + gap, options.y+12); + } else { + // text only option, not tested yet + g.setFontAlign(0,0).drawString(info.text, g.getWidth() / 2, options.y+12); + } + + }; + + // clock_info_support + // retrieve all the clock_info modules that are installed + let clockInfoItems = require("clock_info").load(); + + // clock_info_support + // setup the way we wish to interact with the menu + // the hl property defines the color the of the info when the menu is selected after tapping on it + let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:64, y:132, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#0ff"} ); + + // timeout used to update every minute + var drawTimeout; + g.clear(); + + // Show launcher when middle button pressed, add updown button handlers + Bangle.setUI({ + mode : "clock", + remove : function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + // remove info menu + clockInfoMenu.remove(); + delete clockInfoMenu; + } + }); + + // Load widgets + Bangle.loadWidgets(); + draw(); + setTimeout(Bangle.drawWidgets,0); +} // end of clock diff --git a/apps/simplestpp/app.png b/apps/simplestpp/app.png new file mode 100644 index 000000000..814306471 Binary files /dev/null and b/apps/simplestpp/app.png differ diff --git a/apps/simplestpp/icon.js b/apps/simplestpp/icon.js new file mode 100644 index 000000000..e4e40c82c --- /dev/null +++ b/apps/simplestpp/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AAIHBqOM997IGkq1AKIltVqt4BQ0DBQIAB4ALFktVv/9qtYBYvVA4Ulq4KEgNVwAEBgVVoALDgtcAoc1qAFDitgAocJqguEGoowDgVWBYuVGoUBFwgwCHgUNGgUNuBCCAYY6CBYcJCYUlDYYLCgJxCmozCBYcCG4UVG4QLDgBgCBZeXBY/WBYcC1WtvWqGoILEVAIACJoILQgf/+tf/7jBBYg7JL66DLTZazLZZbjLfZcA6oLFq4EDio8CHQReCGgQwEmpCCHgVVFIUCVAQAD6plCkouEA4VVv/9qoPCAAcDZYa/BAAstBQN4BQwABlWoBRAAr")) diff --git a/apps/simplestpp/metadata.json b/apps/simplestpp/metadata.json new file mode 100644 index 000000000..d808b132b --- /dev/null +++ b/apps/simplestpp/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "simplestpp", + "name": "Simplest++ Clock", + "shortName": "Simplest++", + "version": "0.01", + "description": "The simplest working clock, with fast load and clock_info, acts as a tutorial piece", + "readme": "README.md", + "icon": "app.png", + "screenshots": [{"url":"screenshot3.png"}], + "type": "clock", + "tags": "clock", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"simplestpp.app.js","url":"app.js"}, + {"name":"simplestpp.img","url":"icon.js","evaluate":true} + ] +} diff --git a/apps/simplestpp/screenshot1.png b/apps/simplestpp/screenshot1.png new file mode 100644 index 000000000..94cb35cd6 Binary files /dev/null and b/apps/simplestpp/screenshot1.png differ diff --git a/apps/simplestpp/screenshot2.png b/apps/simplestpp/screenshot2.png new file mode 100644 index 000000000..d24d55b02 Binary files /dev/null and b/apps/simplestpp/screenshot2.png differ diff --git a/apps/simplestpp/screenshot3.png b/apps/simplestpp/screenshot3.png new file mode 100644 index 000000000..af7ae7249 Binary files /dev/null and b/apps/simplestpp/screenshot3.png differ diff --git a/apps/sleeplog/ChangeLog b/apps/sleeplog/ChangeLog index a0698d471..aa700f931 100644 --- a/apps/sleeplog/ChangeLog +++ b/apps/sleeplog/ChangeLog @@ -7,3 +7,4 @@ 0.10: Complete rework off this app! 0.10beta: Add interface.html to view saved log data, add "View log" function for debugging log, send data for gadgetbridge, change caching for global getStats 0.11: Prevent module not found error +0.12: Improve README, option to add functions triggered by status changes or time periods, remove old log (<0.10) conversion \ No newline at end of file diff --git a/apps/sleeplog/README.md b/apps/sleeplog/README.md index 45aeb316b..71a7ddde2 100644 --- a/apps/sleeplog/README.md +++ b/apps/sleeplog/README.md @@ -1,11 +1,19 @@ # Sleep Log -This app logs and displays the following states: +This app logs and displays the following states: - sleepling status: _unknown, not worn, awake, light sleep, deep sleep_ - consecutive sleep status: _unknown, not consecutive, consecutive_ It is using the built in movement calculation to decide your sleeping state. While charging it is assumed that you are not wearing the watch and if the status changes to _deep sleep_ the internal heartrate sensor is used to detect if you are wearing the watch. +#### Explanations +* __Detection of Sleep__ + The movement value of bangle's build in health event that is triggered every 10 minutes is checked against the thresholds for light and deep sleep. If the measured movement is lower or equal to the __Deep Sleep__-threshold a deep sleep phase is detected for the last 10 minutes. If the threshold is exceeded but not the __Light Sleep__-threshold than the last timeperiod is detected as light sleep phase. On exceeding even this threshold it is assumed that you were awake. +* __True Sleep__ + The true sleep value is a simple addition of all registered sleeping periods. +* __Consecutive Sleep__ + In addition the consecutive sleep value tries to predict the complete time you were asleep, even the very light sleeping periods when an awake period is detected based on the registered movements. All periods after a sleeping period will be summarized until the first following non sleeping period that is longer then the maximal awake duration (__Max Awake__). If this sum is lower than the minimal consecutive sleep duration (__Min Consecutive__) it is not considered, otherwise it will be added to the consecutive sleep value. + Logfiles are not removed on un-/reinstall to prevent data loss. | Filename (* _example_) | Content | Removeable in | @@ -16,59 +24,73 @@ Logfiles are not removed on un-/reinstall to prevent data loss. --- -### App Usage +### Main App Usage --- -#### On the main app screen: - - __swipe left & right__ +#### Controls: + - __swipe left & right__ to change the displayed day - - __touch the "title"__ (e.g. `Night to Fri 20/05/2022`) + - __touch the "title"__ (e.g. `Night to Fri 20/05/2022`) to enter day selection prompt - - __touch the info area__ - to change the displayed information + - __touch the info area__ + to change the displayed information (by default: consecutive & true sleeping) - - __touch the wrench__ (upper right corner) + - __touch the wrench__ (upper right corner) to enter the settings - - __use back button widget__ (upper left corner) + - __use back button widget__ (upper left corner) exit the app -#### Inside the settings: - - __Thresholds__ submenu +#### View: +| Status | Color | Height | +|-------------|:------:|----------:| +| unknown | black | 0% | +| not worn | red | 40% | +| awake | green | 60% | +| light sleep | cyan | 80% | +| deep sleep | blue | 100% | +| consecutive | violet | as status | + + +--- +### Settings Usage +--- + + - __Thresholds__ submenu Changes take effect from now on, not retrospective! - - __Max Awake__ | maximal awake duration + - __Max Awake__ | maximal awake duration _10min_ / _20min_ / ... / __60min__ / ... / _120min_ - - __Min Consecutive__ | minimal consecutive sleep duration + - __Min Consecutive__ | minimal consecutive sleep duration _10min_ / _20min_ / ... / __30min__ / ... / _120min_ - - __Deep Sleep__ | deep sleep threshold + - __Deep Sleep__ | deep sleep threshold _30_ / _31_ / ... / __100__ / ... / _200_ - - __Light Sleep__ | light sleep threshold - _100_ / _110_ / ... / __200__ / ... / _400_ + - __Light Sleep__ | light sleep threshold + _100_ / _110_ / ... / __200__ / ... / _400_ - __Reset to Default__ | reset to bold values above - - __BreakToD__ | time of day to break view + - __BreakToD__ | time of day to break view _0:00_ / _1:00_ / ... / __12:00__ / ... / _23:00_ - - __App Timeout__ | app specific lock timeout + - __App Timeout__ | app specific lock timeout __0s__ / _10s_ / ... / _120s_ - - __Enabled__ | completely en-/disables the background service + - __Enabled__ | completely en-/disables the background service __on__ / _off_ - - __Debugging__ submenu - - __View log__ | display logfile data - Select the logfile by its starting time. - Thresholds are shown as line with its value. - - __swipe left & right__ + - __Debugging__ submenu + - __View log__ | display logfile data + Select the logfile by its starting time. + Thresholds are shown as line with its value. + - __swipe left & right__ to change displayed duration - - __swipe up & down__ + - __swipe up & down__ to change displayed value range - - __touch the graph__ + - __touch the graph__ to change between light & dark colors - - __use back button widget__ (upper left corner) + - __use back button widget__ (upper left corner) to go back to the logfile selection - - __Enabled__ | en-/disables debugging + - __Enabled__ | en-/disables debugging _on_ / __off__ - - __write File__ | toggles if a logfile is written + - __write File__ | toggles if a logfile is written _on_ / __off__ - - __Duration__ | duration for writing into logfile - _1h_ / _2h_ / ... / __12h__ / _96_ - - The following data is logged to a csv-file: + - __Duration__ | duration for writing into logfile + _1h_ / _2h_ / ... / __12h__ / _96_ + - The following data is logged to a csv-file: _timestamp_ (in days since 1900-01-01 00:00 UTC used by office software) _, movement, status, consecutive, asleepSince, awakeSince, bpm, bpmConfidence_ @@ -78,48 +100,50 @@ Logfiles are not removed on un-/reinstall to prevent data loss. Available through the App Loader when your watch is connected. -- __view data__ +- __view data__ Display the data to each timestamp in a table. -- __save csv-file__ - Download a csv-file with the data to each timestamp. - The time format is chooseable beneath the file list. -- __delete file__ +- __save csv-file__ + Download a csv-file with the data to each timestamp. + The time format is chooseable beneath the file list. +- __delete file__ Deletes the logfile from the watch. __Please backup your data first!__ --- -### Timestamps and files +### Timestamps and Files --- -1. externally visible/usable timestamps (in `global.sleeplog`) are formatted as Bangle timestamps: +1. externally visible/usable timestamps (in `global.sleeplog`) are formatted as Bangle timestamps: seconds since 1970-01-01 00:00 UTC -2. internally used and logged (to `sleeplog.log (StorageFile)`) is within the highest available resolution: +2. internally used and logged (to `sleeplog.log (StorageFile)`) is within the highest available resolution: 10 minutes since 1970-01-01 00:00 UTC (`Bangle / (10 * 60 * 1000)`) 3. debug .csv file ID (`sleeplog_123456.csv`) has a hourly resolution: hours since 1970-01-01 00:00 UTC (`Bangle / (60 * 60 * 1000)`) -4. logged timestamps inside the debug .csv file are formatted for office calculation software: +4. logged timestamps inside the debug .csv file are formatted for office calculation software: days since 1900-01-01 00:00 UTC (`Bangle / (24 * 60 * 60 * 1000) + 25569`) -5. every 14 days the `sleeplog.log (StorageFile)` is reduced and old entries are moved into separat files for each fortnight (`sleeplog_1234.log`) but still accessible though the app: +5. every 14 days the `sleeplog.log (StorageFile)` is reduced and old entries are moved into separat files for each fortnight (`sleeplog_1234.log`) but still accessible though the app: fortnights since 1970-01-04 12:00 UTC (converted with `require("sleeplog").msToFn(Bangle)` and `require("sleeplog").fnToMs(fortnight)`) -- __Logfiles from before 0.10:__ +- __Logfiles from before 0.10:__ timestamps and sleeping status of old logfiles are automatically converted on your first consecutive sleep or manually by `require("sleeplog").convertOldLog()` -- __View logged data:__ - if you'd like to view your logged data in the IDE, you can access it with `require("sleeplog").printLog(since, until)` or `require("sleeplog").readLog(since, until)` to view the raw data +- __View logged data:__ + if you'd like to view your logged data in the IDE, you can access it with `require("sleeplog").printLog(since, until)` or `require("sleeplog").readLog(since, until)` to view the raw data since & until in Bangle timestamp, e.g. `require("sleeplog").printLog(Date()-24*60*60*1000, Date())` for the last 24h --- -### Access statistics (developer information) +### Developer Information --- -- Last Asleep Time [Date]: + +#### Access statistics +- Last Asleep Time [Date]: `Date(sleeplog.awakeSince)` -- Last Awake Duration [ms]: +- Last Awake Duration [ms]: `Date() - sleeplog.awakeSince` -- Last Statistics [object]: +- Last Statistics [object]: ``` // get stats of the last night (period as displayed inside the app) - // as this might be the mostly used function the data is cached inside the global object + // as this might be the mostly used function the data is cached inside the global object sleeplog.getStats(); // get stats of the last 24h @@ -130,25 +154,50 @@ Available through the App Loader when your watch is connected. ={ calculatedAt: 1653123553810, deepSleep: 250, lightSleep: 150, awakeSleep: 10, consecSleep: 320, awakeTime: 1030, notWornTime: 0, unknownTime: 0, logDuration: 1440, firstDate: 1653036600000, lastDate: 1653111600000 } - + // to get the start of a period defined by "Break TOD" of any date var startOfBreak = require("sleeplog").getLastBreak(); // same as var startOfBreak = require("sleeplog").getLastBreak(Date.now()); // output as date =Date: Sat May 21 2022 12:00:00 GMT+0200 - + // get stats of this period as displayed inside the app require("sleeplog").getStats(require("sleeplog").getLastBreak(), 24*60*60*1000); // or any other day require("sleeplog").getStats(require("sleeplog").getLastBreak(Date(2022,4,10)), 24*60*60*1000); ``` -- Total Statistics [object]: +- Total Statistics [object]: ``` // use with caution, may take a long time ! require("sleeplog").getStats(0, 0, require("sleeplog").readLog()); ``` +#### Add functions triggered by status changes or inside a specified time period +With the following code it is possible to add functions that will be called every 10 minutes after new movement data when meeting the specified parameters on each : +``` +// first ensure that the sleeplog trigger object is available (sleeplog is enabled) +if (typeof (global.sleeplog || {}).trigger === "object") { + // then add your parameters with the function to call as object into the trigger object + sleeplog.trigger["my app name"] = { + onChange: false, // false as default, if true call fn only on a status change + from: 0, // 0 as default, in ms, first time fn will be called + to: 24*60*60*1000, // 24h as default, in ms, last time fn will be called + // reference time to from & to is rounded to full minutes + fn: function(data) { print(data); } // function to be executed + }; +} +``` +The passed data object has the following properties: +- timestamp: of the status change as date object, + (should be around 10min. before "now", the actual call of the function) +- status: value of the new status (0-4), + (0 = unknown, 1 = not worn, 2 = awake, 3 = light sleep, 4 = deep sleep) +- consecutive: value of the new status (0-2), + (0 = unknown, 1 = no consecutive sleep, 2 = consecutive sleep) +- prevStatus: if changed the value of the previous status (0-4) else undefined, +- prevConsecutive: if changed the value of the previous status (0-2) else undefined + --- ### Worth Mentioning @@ -156,14 +205,14 @@ Available through the App Loader when your watch is connected. #### To do list - Check translations. - Add more functionallities to interface.html. -- Enable recieving data on the Gadgetbridge side + testing. +- Enable receiving data on the Gadgetbridge side + testing. __Help appreciated!__ #### Requests, Bugs and Feedback Please leave requests and bug reports by raising an issue at [github.com/storm64/BangleApps](https://github.com/storm64/BangleApps) (or send me a [mail](mailto:banglejs@storm64.de)). #### Creator -Storm64 ([Mail](mailto:banglejs@storm64.de), [github](https://github.com/storm64)) +Storm64 ([mail](mailto:banglejs@storm64.de), [github](https://github.com/storm64)) #### Contributors myxor ([github](https://github.com/myxor)) diff --git a/apps/sleeplog/boot.js b/apps/sleeplog/boot.js index c1f8a2d2d..78fd23450 100644 --- a/apps/sleeplog/boot.js +++ b/apps/sleeplog/boot.js @@ -124,7 +124,7 @@ if (sleeplog.conf.enabled) { if (!sleeplog.info.saveUpToDate || force) { // save status, consecutive status and info timestamps to restore on reload var save = [sleeplog.info.lastCheck, sleeplog.info.awakeSince, sleeplog.info.asleepSince]; - // add debuging status if active + // add debuging status if active if (sleeplog.debug) save.push(sleeplog.debug.writeUntil, sleeplog.debug.fileid); // stringify entries @@ -253,11 +253,38 @@ if (sleeplog.conf.enabled) { } } + // check if the status has changed + var changed = data.status !== this.status || data.consecutive !== this.consecutive; + + // read and check trigger entries + var triggers = Object.keys(this.trigger) || []; + if (triggers.length) { + // calculate time from timestamp in ms on full minutes + var time = new Date(); + time = (time.getHours() * 60 + time.getMinutes()) * 60 * 1000; + // go through all triggers + triggers.forEach(key => { + // read entry to key + var entry = this.trigger[key]; + // check if the event matches the entries requirements + if (typeof entry.fn === "function" && (changed || !entry.onChange) && + (entry.from || 0) <= time && (entry.to || 24 * 60 * 60 * 1000) >= time) + // and call afterwards with status data + setTimeout(entry.fn, 100, { + timestamp: new Date(data.timestamp), + status: data.status, + consecutive: data.consecutive, + prevStatus: data.status === this.status ? undefined : this.status, + prevConsecutive: data.consecutive === this.consecutive ? undefined : this.consecutive + }); + }); + } + // cache change into a known consecutive state var changeIntoConsec = data.consecutive; - // check if the status has changed - if (data.status !== this.status || data.consecutive !== this.consecutive) { + // actions on a status change + if (changed) { // append status this.appendStatus(data.timestamp, data.status, data.consecutive); @@ -268,7 +295,7 @@ if (sleeplog.conf.enabled) { // reset saveUpToDate status delete this.info.saveUpToDate; } - + // send status to gadgetbridge var gb_kinds = "unknown,not_worn,activity,light_sleep,deep_sleep"; Bluetooth.println(JSON.stringify({ @@ -319,7 +346,10 @@ if (sleeplog.conf.enabled) { } // return stats cache return this.statsCache; - } + }, + + // define trigger object + trigger: {} }, sleeplog); // initial starting diff --git a/apps/sleeplog/lib.js b/apps/sleeplog/lib.js index 1919e7483..83c45de66 100644 --- a/apps/sleeplog/lib.js +++ b/apps/sleeplog/lib.js @@ -149,14 +149,6 @@ exports = { // define move log function, move StorageFile content into files seperated by fortnights moveLog: function(force) { - /** convert old logfile (< v0.10) if present **/ - if (require("Storage").list("sleeplog.log", { - sf: false - }).length) { - convertOldLog(); - } - /** may be removed in later versions **/ - // first day of this fortnight period var thisFirstDay = this.fnToMs(this.msToFn(Date.now())); @@ -384,82 +376,5 @@ exports = { "unknown,not worn,awake,light sleep,deep sleep".split(",")[entry[1]].padEnd(12) + "for" + (duration + "min").padStart(8)); }); - }, - - /** convert old (< v0.10) to new logfile data **/ - convertOldLog: function() { - // read old logfile - var oldLog = require("Storage").read("sleeplog.log") || ""; - // decode data if needed - if (!oldLog.startsWith("[")) oldLog = atob(oldLog); - // delete old logfile and return if it is empty or corrupted - if (!oldLog.startsWith("[[") || !oldLog.endsWith("]]")) { - require("Storage").erase("sleeplog.log"); - return; - } - - // transform into StorageFile and clear oldLog to have more free ram accessable - require("Storage").open("sleeplog_old.log", "w").write(JSON.parse(oldLog).reverse().join("\n")); - oldLog = undefined; - - // calculate fortnight from now - var fnOfNow = this.msToFn(Date.now()); - - // open StorageFile with old log data - var file = require("Storage").open("sleeplog_old.log", "r"); - // define active fortnight and file cache - var activeFn = true; - var fileCache = []; - // loop through StorageFile entries - while (activeFn) { - // define fortnight for this entry - var thisFn = false; - // cache new line - var line = file.readLine(); - // check if line is filled - if (line) { - // parse line - line = line.substr(0, 15).split(",").map(e => parseInt(e)); - // calculate fortnight for this entry - thisFn = this.msToFn(line[0]); - // convert timestamp into 10min steps - line[0] = line[0] / 6E5 | 0; - // set consecutive to unknown - line.push(0); - } - // check if active fortnight and file cache is set, fortnight has changed and - // active fortnight is not fortnight from now - if (activeFn && fileCache.length && activeFn !== thisFn && activeFn !== fnOfNow) { - // write file cache into new file according to fortnight - require("Storage").writeJSON("sleeplog_" + activeFn + ".log", fileCache); - // clear file cache - fileCache = []; - } - // add line to file cache if it is filled - if (line) fileCache.push(line); - // set active fortnight - activeFn = thisFn; - } - // check if entries are leftover - if (fileCache.length) { - // format fileCache entries into a string - fileCache = fileCache.map(e => e.join(",")).join("\n"); - // read complete new log StorageFile as string - file = require("Storage").open("sleeplog.log", "r"); - var newLogString = file.read(file.getLength()); - // add entries at the beginning of the new log string - newLogString = fileCache + "\n" + newLogString; - // rewrite new log StorageFile - require("Storage").open("sleeplog.log", "w").write(newLogString); - } - - // free ram - file = undefined; - fileCache = undefined; - - // clean up old files - require("Storage").erase("sleeplog.log"); - require("Storage").open("sleeplog_old.log", "w").erase(); } - /** may be removed in later versions **/ }; diff --git a/apps/sleeplog/metadata.json b/apps/sleeplog/metadata.json index f6ce661e8..353476446 100644 --- a/apps/sleeplog/metadata.json +++ b/apps/sleeplog/metadata.json @@ -2,7 +2,7 @@ "id":"sleeplog", "name":"Sleep Log", "shortName": "SleepLog", - "version": "0.11", + "version": "0.12", "description": "Log and view your sleeping habits. This app is using the built in movement calculation.", "icon": "app.png", "type": "app", diff --git a/apps/sleeplogalarm/ChangeLog b/apps/sleeplogalarm/ChangeLog new file mode 100644 index 000000000..80f8bd7e4 --- /dev/null +++ b/apps/sleeplogalarm/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Add "from Consec."-setting +0.03: Correct how to ignore last triggered alarm +0.04: Make "disable alarm" possible on next day; correct alarm filtering; improve settings \ No newline at end of file diff --git a/apps/sleeplogalarm/README.md b/apps/sleeplogalarm/README.md new file mode 100644 index 000000000..005377fb1 --- /dev/null +++ b/apps/sleeplogalarm/README.md @@ -0,0 +1,56 @@ +# Sleep Log Alarm + +This widget searches for active alarms and raises an own alarm event up to the defined time earlier, if in light sleep or awake phase. Optional the earlier alarm will only be triggered if comming from or in consecutive sleep. The settings of the earlier alarm can be adjusted and it is possible to filter the targeting alarms by time and message. By default the time of the targeting alarm is displayed inside the widget which can be adjusted, too. + +_This widget does not detect sleep on its own and can not create alarms. It requires the [sleeplog](/apps/?id=sleeplog) app and any alarm app that uses [sched](/apps/?id=sched) to be installed._ + +--- +### Settings +--- + + - __earlier__ | duration to trigger alarm earlier + _10min_ / _20min_ / __30min__ / ... / _120min_ + - __from Consec.__ | only trigger if comming from consecutive sleep + _on_ / __off__ + - __vib pattern__ | vibration pattern for the earlier alarm + __..__ / ... + - __msg__ | customized message for the earlier alarm + __...__ / ... + - __msg as prefix__ | use the customized message as prefix to the original message or replace it comlpetely if disabled + __on__ / _off_ + - __disable alarm__ | if enabled the original alarm will be disabled + _on_ / __off__ + - __auto snooze__ | auto snooze option for the earlier alarm + __on__ / _off_ + - __Filter Alarm__ submenu + - __time from__ | exclude alarms before this time + _0:00_ / _0:15_ / ... / __3:00__ / ... / _24:00_ + - __time to__ | exclude alarms after this time + _0:00_ / _0:15_ / ... / __12:00__ / ... / _24:00_ + - __msg includes__ | include only alarms including this string in msg + __""__ / ... + - __Widget__ submenu + - __hide__ | completely hide the widget + _on_ / __off__ + - __show time__ | show the time of the targeting alarm + __on__ / _off_ + - __color__ | color of the widget + _red_ / __yellow__ / ... / _white_ + - __Enabled__ | completely en-/disables the background service + __on__ / _off_ + +--- +### Worth Mentioning +--- + +#### Requests, Bugs and Feedback +Please leave requests and bug reports by raising an issue at [github.com/storm64/BangleApps](https://github.com/storm64/BangleApps) (or send me a [mail](mailto:banglejs@storm64.de)). + +#### Creator +Storm64 ([Mail](mailto:banglejs@storm64.de), [github](https://github.com/storm64)) + +#### Attributions +The app icon is downloaded from [https://icons8.com](https://icons8.com). + +#### License +[MIT License](LICENSE) diff --git a/apps/sleeplogalarm/app.png b/apps/sleeplogalarm/app.png new file mode 100644 index 000000000..1a8e53865 Binary files /dev/null and b/apps/sleeplogalarm/app.png differ diff --git a/apps/sleeplogalarm/lib.js b/apps/sleeplogalarm/lib.js new file mode 100644 index 000000000..343e811af --- /dev/null +++ b/apps/sleeplogalarm/lib.js @@ -0,0 +1,141 @@ +// load library +var sched = require("sched"); + +// find next active alarm in range +function getNextAlarm(allAlarms, fo, withId) { + if (withId) allAlarms = allAlarms.map((a, idx) => { + a.idx = idx; + return a; + }); + // return next active alarms in range, filter for + // active && not timer && not own alarm && + // after from && before to && includes msg + var ret = allAlarms.filter( + a => a.on && !a.timer && a.id !== "sleeplog" && + a.t >= fo.from && a.t < fo.to && (!fo.msg || a.msg.includes(fo.msg)) + ).map(a => { // add time to alarm + a.tTo = sched.getTimeToAlarm(a); + return a; + }).filter(a => a.tTo // filter non active alarms + // sort to get next alarm first + ).sort((a, b) => a.tTo - b.tTo); + // prevent triggering for an already triggered alarm again if available + if (fo.lastDate) { + var toLast = fo.lastDate - new Date().valueOf() + 1000; + if (toLast > 0) ret = ret.filter(a => a.tTo > toLast); + } + // return first entry + return ret[0] || {}; +} + +exports = { + // function to read settings with defaults + getSettings: function() { + return Object.assign({ + enabled: true, + earlier: 30, + fromConsec: false, + vibrate: "..", + msg: "...\n", + msgAsPrefix: true, + disableOnAlarm: false, // !!! not available if alarm is at the next day + as: true, + filter: { + from: 3 * 36E5, + to: 12 * 36E5, + msg: "" + }, + wid: { + hide: false, + time: true, + color: g.theme.dark ? 65504 : 31 // yellow or blue + } + }, require("Storage").readJSON("sleeplogalarm.settings.json", true) || {}); + }, + + // widget reload function + widReload: function() { + // abort if trigger object is not available + if (typeof (global.sleeplog || {}).trigger !== "object") return; + + // read settings to calculate alarm range + var settings = exports.getSettings(); + + // set the alarm time + this.time = getNextAlarm(sched.getAlarms(), settings.filter).t; + + // abort if no alarm time could be found inside range + if (!this.time) return; + + // set widget width if not hidden + if (!this.hidden) this.width = 8; + + // insert sleeplogalarm conditions and function + sleeplog.trigger.sleeplogalarm = { + from: this.time - settings.earlier * 6E4, + to: this.time - 1, + fn: function (data) { + // execute trigger function if on light sleep or awake + // and if set if comming from consecutive + if ((data.status === 3 || data.status === 2) && !settings.fromConsec || + data.consecutive === 3 || data.prevConsecutive === 3) + require("sleeplogalarm").trigger(); + } + }; + }, + + // trigger function + trigger: function() { + // read settings + var settings = exports.getSettings(); + + // read all alarms + var allAlarms = sched.getAlarms(); + + // find first active alarm + var alarm = getNextAlarm(sched.getAlarms(), settings.filter, settings.disableOnAlarm); + + // return if no alarm is found + if (!alarm) return; + + // get now + var now = new Date(); + + // get date of the alarm + var aDate = new Date(now + alarm.tTo); + + // disable earlier triggered alarm if set + if (settings.disableOnAlarm) { + // set alarms last to the day it would trigger + allAlarms[alarm.idx].last = aDate.getDate(); + // remove added indexes + allAlarms = allAlarms.map(a => { + delete a.idx; + return a; + }); + } + + // add new alarm for now with data from found alarm + allAlarms.push({ + id: "sleeplog", + appid: "sleeplog", + on: true, + t: ((now.getHours() * 60 + now.getMinutes()) * 60 + now.getSeconds()) * 1000, + dow: 127, + msg: settings.msg + (settings.msgAsPrefix ? alarm.msg || "" : ""), + vibrate: settings.vibrate || alarm.vibrate, + as: settings.as, + del: true + }); + + // save date of the alarm to prevent triggering for the same alarm again + settings.filter.lastDate = aDate.valueOf(); + require("Storage").writeJSON("sleeplogalarm.settings.json", settings); + + // write changes + sched.setAlarms(allAlarms); + + // trigger sched.js + load("sched.js"); + } +}; \ No newline at end of file diff --git a/apps/sleeplogalarm/metadata.json b/apps/sleeplogalarm/metadata.json new file mode 100644 index 000000000..fd85507e6 --- /dev/null +++ b/apps/sleeplogalarm/metadata.json @@ -0,0 +1,21 @@ +{ + "id":"sleeplogalarm", + "name":"Sleep Log Alarm", + "shortName": "SleepLogAlarm", + "version": "0.04", + "description": "Enhance your morning and let your alarms wake you up when you are in light sleep.", + "icon": "app.png", + "type": "widget", + "tags": "tool,widget", + "supports": ["BANGLEJS2"], + "dependencies": { + "scheduler": "type", + "sleeplog": "app" + }, + "readme": "README.md", + "storage": [ + {"name": "sleeplogalarm", "url": "lib.js"}, + {"name": "sleeplogalarm.settings.js", "url": "settings.js"}, + {"name": "sleeplogalarm.wid.js", "url": "widget.js"} + ] +} diff --git a/apps/sleeplogalarm/settings.js b/apps/sleeplogalarm/settings.js new file mode 100644 index 000000000..1f3a13272 --- /dev/null +++ b/apps/sleeplogalarm/settings.js @@ -0,0 +1,192 @@ +(function(back) { + // read settings + var settings = require("sleeplogalarm").getSettings(); + + // write change to storage + function writeSetting() { + require("Storage").writeJSON("sleeplogalarm.settings.json", settings); + } + + // read input from keyboard + function readInput(v, cb) { + // setTimeout required to load after menu refresh + setTimeout((v, cb) => { + if (require("Storage").read("textinput")) { + g.clear(); + require("textinput").input({text: v}).then(v => cb(v)); + } else { + E.showAlert(/*LANG*/"No keyboard app installed").then(() => cb()); + } + }, 0, v, cb); + } + + // show widget menu + function showFilterMenu() { + // set menu + var filterMenu = { + "": { + title: "Filter Alarm" + }, + /*LANG*/"< Back": () => showMain(8), + /*LANG*/"time from": { + value: settings.filter.from / 6E4, + step: 10, + min: 0, + max: 1440, + wrap: true, + noList: true, + format: v => (0|v/60) + ":" + ("" + (v%60)).padStart(2, "0"), + onchange: v => { + settings.filter.from = v * 6E4; + writeSetting(); + } + }, + /*LANG*/"time to": { + value: settings.filter.to / 6E4, + step: 10, + min: 0, + max: 1440, + wrap: true, + noList: true, + format: v => (0|v/60) + ":" + ("" + (v%60)).padStart(2, "0"), + onchange: v => { + settings.filter.to = v * 6E4; + writeSetting(); + } + }, + /*LANG*/"msg includes": { + value: settings.filter.msg, + format: v => !v ? "" : v.length > 6 ? v.substring(0, 6)+"..." : v, + onchange: v => readInput(v, v => { + settings.filter.msg = v; + writeSetting(); + showFilterMenu(3); + }) + } + }; + var menu = E.showMenu(filterMenu); + } + + // show widget menu + function showWidMenu() { + // define color values and names + var colName = ["red", "yellow", "green", "cyan", "blue", "magenta", "black", "white"]; + var colVal = [63488, 65504, 2016, 2047, 31, 63519, 0, 65535]; + + // set menu + var widgetMenu = { + "": { + title: "Widget Settings" + }, + /*LANG*/"< Back": () => showMain(9), + /*LANG*/"hide": { + value: settings.wid.hide, + onchange: v => { + settings.wid.hide = v; + writeSetting(); + } + }, + /*LANG*/"show time": { + value: settings.wid.time, + onchange: v => { + settings.wid.time = v; + writeSetting(); + } + }, + /*LANG*/"color": { + value: colVal.indexOf(settings.wid.color), + min: 0, + max: colVal.length -1, + wrap: true, + format: v => colName[v], + onchange: v => { + settings.wid.color = colVal[v]; + writeSetting(); + } + } + }; + var menu = E.showMenu(widgetMenu); + } + + // show main menu + function showMain(selected) { + // set menu + var mainMenu = { + "": { + title: "Sleep Log Alarm", + selected: selected + }, + /*LANG*/"< Back": () => back(), + /*LANG*/"erlier": { + value: settings.earlier, + step: 10, + min: 10, + max: 120, + wrap: true, + noList: true, + format: v => v + /*LANG*/"min", + onchange: v => { + settings.earlier = v; + writeSetting(); + } + }, + /*LANG*/"from Consec.": { + value: settings.fromConsec, + onchange: v => { + settings.fromConsec = v; + writeSetting(); + } + }, + /*LANG*/"vib pattern": require("buzz_menu").pattern( + settings.vibrate, + v => { + settings.vibrate = v; + writeSetting(); + } + ), + /*LANG*/"msg": { + value: settings.msg, + format: v => !v ? "" : v.length > 6 ? v.substring(0, 6)+"..." : v, + onchange: v => readInput(v, v => { + settings.msg = v; + writeSetting(); + showMenu(4); + }) + }, + /*LANG*/"msg as prefix": { + value: settings.msgAsPrefix, + onchange: v => { + settings.msgAsPrefix = v; + writeSetting(); + } + }, + /*LANG*/"disable alarm": { + value: settings.disableOnAlarm, + onchange: v => { + settings.disableOnAlarm = v; + writeSetting(); + } + }, + /*LANG*/"auto snooze": { + value: settings.as, + onchange: v => { + settings.as = v; + writeSetting(); + } + }, + /*LANG*/"Filter Alarm": () => showFilterMenu(), + /*LANG*/"Widget": () => showWidMenu(), + /*LANG*/"Enabled": { + value: settings.enabled, + onchange: v => { + settings.enabled = v; + writeSetting(); + } + } + }; + var menu = E.showMenu(mainMenu); + } + + // draw main menu + showMain(); +}) diff --git a/apps/sleeplogalarm/widget.js b/apps/sleeplogalarm/widget.js new file mode 100644 index 000000000..e3171751f --- /dev/null +++ b/apps/sleeplogalarm/widget.js @@ -0,0 +1,32 @@ +// check if enabled in settings +if ((require("Storage").readJSON("sleeplogalarm.settings.json", true) || {enabled: true}).enabled) { + // read settings + settings = require("sleeplogalarm").getSettings(); // is undefined if used with var + + // insert neccessary settings into widget + WIDGETS.sleeplogalarm = { + area: "tl", + width: 0, + time: 0, + earlier: settings.earlier, + draw: function () { + // draw zzz + g.reset().setColor(settings.wid.color).drawImage(atob("BwoBD8SSSP4EEEDg"), this.x + 1, this.y); + // call function to draw the time of alarm if a alarm is found + if (this.time) this.drawTime(this.time + 1); + }, + drawTime: () => {}, + reload: require("sleeplogalarm").widReload + }; + + // add function to draw the time of alarm if enabled + if (settings.wid.time) WIDGETS.sleeplogalarm.drawTime = function(time) { + // directly include Font4x5Numeric + g.setFontCustom(atob("CAZMA/H4PgvXoK1+DhPg7W4P1uCEPg/X4O1+AA=="), 46, atob("AgQEAgQEBAQEBAQE"), 5).setFontAlign(1, 1); + g.drawString(0|(time / 36E5), this.x + this.width + 1, this.y + 17); + g.drawString(0|((time / 36E5)%1 * 60), this.x + this.width + 1, this.y + 23); + }; + + // load widget + WIDGETS.sleeplogalarm.reload(); +} \ No newline at end of file diff --git a/apps/sleepphasealarm/ChangeLog b/apps/sleepphasealarm/ChangeLog index 9f2b07d49..795c62fa2 100644 --- a/apps/sleepphasealarm/ChangeLog +++ b/apps/sleepphasealarm/ChangeLog @@ -11,3 +11,5 @@ Add setting to defer start of algorithm Add setting to disable scheduler alarm 0.10: Fix: Do not wake when falling asleep +0.11: Minor tweaks +0.12: Support javascript command to execute as defined in scheduler 'js' configuration diff --git a/apps/sleepphasealarm/app.js b/apps/sleepphasealarm/app.js index b3aacc80d..ba8bff9b2 100644 --- a/apps/sleepphasealarm/app.js +++ b/apps/sleepphasealarm/app.js @@ -21,8 +21,8 @@ let logs = []; // // Function needs to be called for every measurement but returns a value at maximum once a second (see winwidth) // start of sleep marker is delayed by sleepthresh due to continous data reading -const winwidth=13; -const nomothresh=0.03; // 0.006 was working on Bangle1, but Bangle2 has higher noise. +const winwidth=13; // Actually 12.5 Hz, rounded +const nomothresh=0.023; // Original implementation: 6, resolution 11 bit, scale +-4G = 6/(2^(11-1))*4 = 0.023438 in G const sleepthresh=600; var ess_values = []; var slsnds = 0; @@ -69,6 +69,9 @@ active.forEach(alarm => { } }); +const LABEL_ETA = /*LANG*/"ETA"; +const LABEL_WAKEUP_TIME = /*LANG*/"Alarm at"; + var layout = new Layout({ type:"v", c: [ {type:"txt", font:"10%", label:"Sleep Phase Alarm", bgCol:g.theme.bgH, fillx: true, height:Bangle.appRect.h/6}, @@ -84,7 +87,7 @@ function drawApp() { var alarmMinute = nextAlarmDate.getMinutes(); if (alarmHour < 10) alarmHour = "0" + alarmHour; if (alarmMinute < 10) alarmMinute = "0" + alarmMinute; - layout.alarm_date.label = "Alarm at " + alarmHour + ":" + alarmMinute; + layout.alarm_date.label = `${LABEL_WAKEUP_TIME}: ${alarmHour}:${alarmMinute}`; layout.render(); function drawTime() { @@ -94,7 +97,7 @@ function drawApp() { const diff = nextAlarmDate - now; const diffHour = Math.floor((diff % 86400000) / 3600000).toString(); const diffMinutes = Math.floor(((diff % 86400000) % 3600000) / 60000).toString(); - layout.eta.label = "ETA: -"+ diffHour + ":" + diffMinutes.padStart(2, '0'); + layout.eta.label = `${LABEL_ETA}: ${diffHour}:${diffMinutes.padStart(2, '0')}`; layout.render(); } @@ -139,7 +142,7 @@ if (nextAlarmDate !== undefined) { // minimum alert 30 minutes early minAlarm.setTime(nextAlarmDate.getTime() - (30*60*1000)); run = () => { - layout.state.label = "Start"; + layout.state.label = /*LANG*/"Start"; layout.render(); Bangle.setOptions({powerSave: false}); // do not dynamically change accelerometer poll interval Bangle.setPollInterval(80); // 12.5Hz @@ -150,7 +153,7 @@ if (nextAlarmDate !== undefined) { if (swest !== undefined) { if (Bangle.isLCDOn()) { - layout.state.label = swest ? "Sleep" : "Awake"; + layout.state.label = swest ? /*LANG*/"Sleep" : /*LANG*/"Awake"; layout.render(); } // log @@ -170,12 +173,16 @@ if (nextAlarmDate !== undefined) { setTimeout(load, 1000); } else if (measure && now >= minAlarm && swest === false) { addLog(now, "alarm"); - buzz(); measure = false; - if (config.settings.disableAlarm) { - // disable alarm for scheduler - nextAlarmConfig.last = now.getDate(); - require("Storage").writeJSON("sched.json", alarms); + if (nextAlarmConfig.js) { + eval(nextAlarmConfig.js); // run nextAlarmConfig.js if set + } else { + buzz(); + if (config.settings.disableAlarm) { + // disable alarm for scheduler + nextAlarmConfig.last = now.getDate(); + require('Storage').writeJSON('sched.json', alarms); + } } } }); diff --git a/apps/sleepphasealarm/metadata.json b/apps/sleepphasealarm/metadata.json index 35eea7466..ced99062f 100644 --- a/apps/sleepphasealarm/metadata.json +++ b/apps/sleepphasealarm/metadata.json @@ -2,7 +2,7 @@ "id": "sleepphasealarm", "name": "SleepPhaseAlarm", "shortName": "SleepPhaseAlarm", - "version": "0.10", + "version": "0.12", "description": "Uses the accelerometer to estimate sleep and wake states with the principle of Estimation of Stationary Sleep-segments (ESS, see https://ubicomp.eti.uni-siegen.de/home/datasets/ichi14/index.html.en). This app will read the next alarm from the alarm application and will wake you up to 30 minutes early at the best guessed time when you are almost already awake.", "icon": "app.png", "tags": "alarm", diff --git a/apps/slopeclock/ChangeLog b/apps/slopeclock/ChangeLog index af766d95d..da82f6355 100644 --- a/apps/slopeclock/ChangeLog +++ b/apps/slopeclock/ChangeLog @@ -1,2 +1,4 @@ 0.01: New App! 0.02: Reset font to save some memory during remove +0.03: Added support for locale based time +0.04: Stability improvements diff --git a/apps/slopeclock/app.js b/apps/slopeclock/app.js index 178084fb0..2164e7ede 100644 --- a/apps/slopeclock/app.js +++ b/apps/slopeclock/app.js @@ -1,7 +1,12 @@ +{ // must be inside our own scope here so that when we are unloaded everything disappears + // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global + +const fontBitmap = E.toString(require('heatshrink').decompress(atob('AH8AgP/BpcD//gBpn4Bpn+Bpn/wANMHBRTB//wBphGLBoJGLv4OBBpU/KhkfBoPABpMPMRkHMRh+CMRRwC/hwmMQQNKMQTTNBpRGCRhSpCBpY4BFJY4BBpcAjgMLAHUwBpl4BhcBd5Z/Bd5abCBpa3BTZd/YpcBcIPgBpMHBoPwIhf//BEL/5wKIgP/OBJECAAJELAAJwIIgQABOBBECOBRECOBJEEOBBEEOBBEEOBBEEOBBEEOA5EFBo5EFFI5EFKY5EGN4woGTIpEpj5EMDYzeGG4xEFgEDWZhhFbo59FfI7QFIgynGIgxwGBg5wEIhBwE+ANIOAZEIOAhEIOAgMJOAREJOAZEJOAZEJOAZEKOAQMKOAJELOAJELAAJELAH0EBhaQBSJa6BZJbkCDhMDBof4XJIADBpvAKRIqKBov+Bo0fBogqHBozpGBoyAGBoxjGBo44FBo44FMIpxHBo5xFBo7HFU4pGHBpBGEBpB/EdohGIgINHIwgNJIwgWEn4EC8ANGQ4SNHv4VEQgRUEEgQxCHwRUEYgRNDEQQNKFQRUDAwQNDQoRUDTQQUDHASpDCgR3EHAJiDCgR3ELYJiEBow/BMQgiBbQ4iFSYg/CLYZwBGAg/COAwNGOAwiDJoRwUKggNBOAwGEBoJwEcIT2GaYw4DAoINEMQQ/CHwRbEMQQHCLQTaHI4QvCNIoHCAArMEJoQAFO4gkDBpJUCAAraHBpRUDAAihEIxANFIw4NFIw7EEIxANFRo4NGcQQNKHAwNGHAwNGHAwNHHAoNHf4YNJVQqLFFQ7DEFRDtEKpHgBpCADwANIDgRSHKwvABpQA/AFp7BZwkfXIyXFVoLVFv//bArxFBoLBDga6GfgK0DHwIiEH4TrEcgw/BJogwBa4g/BJogwBEQgNGOAxNBAAwUEJoQAFOAoNHOAoNHOApbBAAxwEBpBwENIIAGOAgNIOAh3BOBYNIOAi2BOBYNIOAgNJOAbEBOBbEIOAjEIOAoNIOAioIOAiaIOAiMIOH5wLAAw/BOAgAGH4JwEAAw/CBpQ/COAYAHWAJwDAA6wBOAYAHWAJwEAAywBODIA/ABsDUBYNBOwpwGZgIcEcIwNBDggNBcIraFBoQjEbQK+DBoThEBoIqDBoThEdAJNDBoThEBpBNEewJbDBoRwEewINGOAiFBNIYNCOAgNJO5INDOAaaBAwYNDOAgGEBoZwEBpBwEVAgNDOAiMBCgQNDOAiMBCgRnCOAqMEBohwDPwgNEOAZ+EBohwDPwQGBFwJwJAwINEOAxUBLAP/+5wHIwIDC/ZwHHAInC/JwHAAn4OBAAD/g/BOAwNEHYJwGBog/BOAgiBAAf+H4JwELwQNDH4JwEMQQNDH4JwEMQv+H4QNDKgoYBOApUGJoRwDKgxNCOAZUGJoRwEIwoGCOAhGFWARwEIwoUCOAhGEBIJwGRogXCOAriEBoRwGHAZBCOAxxDBoRwGFQZrCOAxADEgRwGCwZOCOA4A/AEMBXggAISQ0AjCZFZYgjBTQt/AwqgBBoraFfozgBbQgNBGIgNGEQIGEewJVECgIGEHwJGEAxr9BKggGBewImBfoRUEAwQ7CBIJUFgINCFoIJBO4oNCwAtBBIJ3JFoIJBFoJNEEQQfBBIJNDRgwJCJoaMGBIQ/DPwgNBFoJiHRgYtBMQ4+DFoJiHHwYfBMQbFDPwoJBXww+CFoZwGHwQtDOAz2CFoZwGUIQJCTwRwGGAIJBTwRwGEQICBKAIRDOAngAQJCBJoJwGAAfhD4ZwEAAxwGBpZiBAA4NDMQIAHPwZiCAAx+DMQQNKKhKMDKhKMDKhINEKgf7BoaaDIwn5BpCpD/A8DVAhGD/g8DBooJC/g8DBoqNC/A8DWwg4DIAINIe4k/BpA0BPAI4CBowmBWAI4CBo4uFKYoAFM4KLEAAxZBWogA/ADSMBRZaaCBpTlCwANMXYIAIaQXgBpioKBoTEKaILgLBoRwKn4NBOBQNDOBINDOBN/BoRwJBoZwJBgRwKBoZwJBoZwIgILCOBINDJAJwHfQX8OQJwHBoaqBOA4NC/DUBOA8HBoQDBOA4NC+AfBOA76C8BXBOA4NDQIQNJLwJwILoINCOBANCC4JwIfQQNBOBAbCMwZwGIoQAGJAZ9CAAxIDU4QAGJAbfCAAxIEBpBIEQ4IAGXIhwCAAq5EOAQAGOH5w/OH5wvBoYAELIInEAA4ZKLIiYDAA5ZBTAYAHLIKYDAA5ZBTAgAGZQKYEAAzKBTAhwjAH4A8U4LRCh7xGS4LRCcYwGBAATDBAwLjEBojDBeILVEAwIADwA7Baoj4BAAfAcYLVECgIADGgIRCfAgAD/EAn5UFBohUIv4OEKg4iBKghNBKghwEGgJNCOBJCBD4RwIIQI/BMQZwHH4JUDOArFDOgJwHBIJiGOAQtBBoJiGSYQNBC4JiGSYTPDH4RiDGAP4Z4jFFGAImBBoY/BYoYmDEoZwIRAhwIwDrDBoJwG4AXDJoJwHRAbMCOAzICZgZwGRAXADYRwGK4X4EQLhGOAYADPwZwFcopwHcopwHBpBwEAAaMEOAoACRgjhFBo7hFAAYNDOAZiFBoZwDKgqoDOAZUFBohwCW4QNHfQYNEWwZwDCIQNHGgINBIwgNEOAIDDBo8DLAoNGAAg4DBpJxDMIgAEXAYNJFQYMJXgTtEAA8HIhIA/ACp9BN5SZD8B7JBoX+YZjSJb4f//ANMYpF/BogqHBovwBowMEKpANF/+ABpiAGBoxjGBoyrGBoxxGBo5xFBo5xFPopGHBo5/FBo5GFYYpGHBpCNEj5UMBpCNEh4ICw//g5UGA4X8AYOAHwQNG/EDBoIGCcQYJBH4IDB4EBKgoGCBoQJBQoJUDBoYDBBIJbBVIgNGHAJiEEQIUBAQQtBMQhbBBoQXBGISMFBQN/C4RiFRgIKBD4IxDYoY+BBoIfBC4IRBOAZ+CBoQJBAYJwGwAtBBIIDBOA3AFoIJBOBHgNgY/DOAiMCHYLFCOAp+CFoZwGPwQRBAwINEGAb6CAAR+DGgYtBAAZ+DGgYmCBo5iCIQQACRgZiGAASMEKgYNJKgYtBAASaEYoZiEBohUIVAhUIBoomB/BUEBopUIBoipIBogmBDYJGEBogmBO4JmCBo8/V4QNJh7nCHAYNFgxYEMIxKGBpYqCU4oAFOoLtEAA8PBhYA/AB9///AQ5jFCABEfQ47MCYAbvBXQgiEUYKxFg4iEgbNGh4UEbgRNFCgoNBH4hpBOBYUBAwhwFHwJ3FOApaBNIpwFCYJpFOAovBNIpwFBgJbFOAgECKgwUDIgQABTYhwDJQIACKghwDKQRGGOAYfBAAZwHBghUEOASXCAAaiF/xSEKgprCIgibGAwO/BopUEKApwJAAyMEGoyoGSwhvHWQqLHOARgKbgpSHfAqYGOBJSEOBAMFOAyXEOBBEGOAyXEOBBEGOAyXEOA5EHOAqXFOA5EHOAqXGOAxEIOAgMIOAZEJOAaXHMQpEJAH4AOn6QJbIaDKQgYcKUATXJVxwNCZQ8fCwIND4C4H4ANDHAzUCBoY4GBAP+MIQEBBo//4IDCOIoXD+ANDewozDBoZGFBIZXBIw4NDAAZGFBo6NFEoYAERogNIKgk/Bo5UEBpBUEj5UMh5UMBpKpDg4KFAwRUDbgP4JARCBKgrEB/AsC/BNCAYINEfYQJBCQJiEBIQpDCQJiEv4JBHAT2DRggTBQIReBWAJiDBQJlDYIIgBYoY+BwBGCLwIVBOAYYBCYJUFOAYYBCYIzBHgIVBOAoTBKgYVBOA6NCwAVBOA6zEOAwlDSIhwF4ANCEAJKBOAvwcgYNCOAv/TQQYBGILhFAAn4DYJwDHwQAGBogUBAAx+ERIQAFPwiJCAAwNDL4YNJPYQAGRgZUJRgZUJBoiKC/wNETQZGEMwiaDIwhmEBohGDMwgNFEwS7EVAiNDLAgNFDARYDBowqBWAJGDBo0DH4JYDaQgAFDZKRGBpRxCBpQqCPooAFKoLDEAA8cBhYA/ACM/8AMKcQYAJaASXKWYTdDgwNI/+AawSyHAAJHCn64FBobeCHgwND/xLCeAoNDHAIFBCIINI8BnCKZA0BQYRGEBohxBv5YDBow0Bn5UFGIRGFSIYNG4AiBKgg/CKhQNFPYJUGBohUIBohUICgIADSYSpECgJiEKgwNCKAXAKg0fCgRCCLYWAYggNBCIJiHGAYDBBoJiFGAINBEwJwBMQowCOgQtFPwh0DH4TFEJgYYBOA4XBJgIYBaYRwEHwJMBBQLTDOAYlBJgIKBPwZwFHwIKB+ANCOA5KBD4INBOAwwBTQhwGGAN/BpBiBEQM/HYINBPwhiBS4X8GAR+EMQI4BBoJvCPwiFC/kPAIINGCof//oEDRgYxCAAwNDKgQAGTQZUCBpZUCAAqoDKgYNKKggADWwapDBpZGHBopGHBopGHBoqNHBoqNHBow4GBow4GBow4GBow4GTIgACfIYNJFQrREFRD7EKo/+Bg7HE/ANJDgQ2IeYZRHAH4AmgaYDn50HRgKLCv/8BpD6CZQINIC4QNBVgy2CBoYgCIojEDBoI4GBoRQBn7yHgLuDBoJGGBoQlBj7zIBAIlBh4uDAAhBBEoJYCKgwzCwBKCHgIAEGYY8EAAgzEHgaMHGYI8DPw5wEwBwTEoJwLUgatEMQ4uDPwzhNC4RPBEAKMGC4QNBEAINHC4INBEAIpGKAQgDBo8AnASDRYoAnA='))); + Graphics.prototype.setFontPaytoneOne = function(scale) { // Actual height 81 (91 - 11) this.setFontCustom( - E.toString(require('heatshrink').decompress(atob('AH8AgP/BpcD//gBpn4Bpn+Bpn/wANMHBRTB//wBphGLBoJGLv4OBBpU/KhkfBoPABpMPMRkHMRh+CMRRwC/hwmMQQNKMQTTNBpRGCRhSpCBpY4BFJY4BBpcAjgMLAHUwBpl4BhcBd5Z/Bd5abCBpa3BTZd/YpcBcIPgBpMHBoPwIhf//BEL/5wKIgP/OBJECAAJELAAJwIIgQABOBBECOBRECOBJEEOBBEEOBBEEOBBEEOBBEEOA5EFBo5EFFI5EFKY5EGN4woGTIpEpj5EMDYzeGG4xEFgEDWZhhFbo59FfI7QFIgynGIgxwGBg5wEIhBwE+ANIOAZEIOAhEIOAgMJOAREJOAZEJOAZEJOAZEKOAQMKOAJELOAJELAAJELAH0EBhaQBSJa6BZJbkCDhMDBof4XJIADBpvAKRIqKBov+Bo0fBogqHBozpGBoyAGBoxjGBo44FBo44FMIpxHBo5xFBo7HFU4pGHBpBGEBpB/EdohGIgINHIwgNJIwgWEn4EC8ANGQ4SNHv4VEQgRUEEgQxCHwRUEYgRNDEQQNKFQRUDAwQNDQoRUDTQQUDHASpDCgR3EHAJiDCgR3ELYJiEBow/BMQgiBbQ4iFSYg/CLYZwBGAg/COAwNGOAwiDJoRwUKggNBOAwGEBoJwEcIT2GaYw4DAoINEMQQ/CHwRbEMQQHCLQTaHI4QvCNIoHCAArMEJoQAFO4gkDBpJUCAAraHBpRUDAAihEIxANFIw4NFIw7EEIxANFRo4NGcQQNKHAwNGHAwNGHAwNHHAoNHf4YNJVQqLFFQ7DEFRDtEKpHgBpCADwANIDgRSHKwvABpQA/AFp7BZwkfXIyXFVoLVFv//bArxFBoLBDga6GfgK0DHwIiEH4TrEcgw/BJogwBa4g/BJogwBEQgNGOAxNBAAwUEJoQAFOAoNHOAoNHOApbBAAxwEBpBwENIIAGOAgNIOAh3BOBYNIOAi2BOBYNIOAgNJOAbEBOBbEIOAjEIOAoNIOAioIOAiaIOAiMIOH5wLAAw/BOAgAGH4JwEAAw/CBpQ/COAYAHWAJwDAA6wBOAYAHWAJwEAAywBODIA/ABsDUBYNBOwpwGZgIcEcIwNBDggNBcIraFBoQjEbQK+DBoThEBoIqDBoThEdAJNDBoThEBpBNEewJbDBoRwEewINGOAiFBNIYNCOAgNJO5INDOAaaBAwYNDOAgGEBoZwEBpBwEVAgNDOAiMBCgQNDOAiMBCgRnCOAqMEBohwDPwgNEOAZ+EBohwDPwQGBFwJwJAwINEOAxUBLAP/+5wHIwIDC/ZwHHAInC/JwHAAn4OBAAD/g/BOAwNEHYJwGBog/BOAgiBAAf+H4JwELwQNDH4JwEMQQNDH4JwEMQv+H4QNDKgoYBOApUGJoRwDKgxNCOAZUGJoRwEIwoGCOAhGFWARwEIwoUCOAhGEBIJwGRogXCOAriEBoRwGHAZBCOAxxDBoRwGFQZrCOAxADEgRwGCwZOCOA4A/AEMBXggAISQ0AjCZFZYgjBTQt/AwqgBBoraFfozgBbQgNBGIgNGEQIGEewJVECgIGEHwJGEAxr9BKggGBewImBfoRUEAwQ7CBIJUFgINCFoIJBO4oNCwAtBBIJ3JFoIJBFoJNEEQQfBBIJNDRgwJCJoaMGBIQ/DPwgNBFoJiHRgYtBMQ4+DFoJiHHwYfBMQbFDPwoJBXww+CFoZwGHwQtDOAz2CFoZwGUIQJCTwRwGGAIJBTwRwGEQICBKAIRDOAngAQJCBJoJwGAAfhD4ZwEAAxwGBpZiBAA4NDMQIAHPwZiCAAx+DMQQNKKhKMDKhKMDKhINEKgf7BoaaDIwn5BpCpD/A8DVAhGD/g8DBooJC/g8DBoqNC/A8DWwg4DIAINIe4k/BpA0BPAI4CBowmBWAI4CBo4uFKYoAFM4KLEAAxZBWogA/ADSMBRZaaCBpTlCwANMXYIAIaQXgBpioKBoTEKaILgLBoRwKn4NBOBQNDOBINDOBN/BoRwJBoZwJBgRwKBoZwJBoZwIgILCOBINDJAJwHfQX8OQJwHBoaqBOA4NC/DUBOA8HBoQDBOA4NC+AfBOA76C8BXBOA4NDQIQNJLwJwILoINCOBANCC4JwIfQQNBOBAbCMwZwGIoQAGJAZ9CAAxIDU4QAGJAbfCAAxIEBpBIEQ4IAGXIhwCAAq5EOAQAGOH5w/OH5wvBoYAELIInEAA4ZKLIiYDAA5ZBTAYAHLIKYDAA5ZBTAgAGZQKYEAAzKBTAhwjAH4A8U4LRCh7xGS4LRCcYwGBAATDBAwLjEBojDBeILVEAwIADwA7Baoj4BAAfAcYLVECgIADGgIRCfAgAD/EAn5UFBohUIv4OEKg4iBKghNBKghwEGgJNCOBJCBD4RwIIQI/BMQZwHH4JUDOArFDOgJwHBIJiGOAQtBBoJiGSYQNBC4JiGSYTPDH4RiDGAP4Z4jFFGAImBBoY/BYoYmDEoZwIRAhwIwDrDBoJwG4AXDJoJwHRAbMCOAzICZgZwGRAXADYRwGK4X4EQLhGOAYADPwZwFcopwHcopwHBpBwEAAaMEOAoACRgjhFBo7hFAAYNDOAZiFBoZwDKgqoDOAZUFBohwCW4QNHfQYNEWwZwDCIQNHGgINBIwgNEOAIDDBo8DLAoNGAAg4DBpJxDMIgAEXAYNJFQYMJXgTtEAA8HIhIA/ACp9BN5SZD8B7JBoX+YZjSJb4f//ANMYpF/BogqHBovwBowMEKpANF/+ABpiAGBoxjGBoyrGBoxxGBo5xFBo5xFPopGHBo5/FBo5GFYYpGHBpCNEj5UMBpCNEh4ICw//g5UGA4X8AYOAHwQNG/EDBoIGCcQYJBH4IDB4EBKgoGCBoQJBQoJUDBoYDBBIJbBVIgNGHAJiEEQIUBAQQtBMQhbBBoQXBGISMFBQN/C4RiFRgIKBD4IxDYoY+BBoIfBC4IRBOAZ+CBoQJBAYJwGwAtBBIIDBOA3AFoIJBOBHgNgY/DOAiMCHYLFCOAp+CFoZwGPwQRBAwINEGAb6CAAR+DGgYtBAAZ+DGgYmCBo5iCIQQACRgZiGAASMEKgYNJKgYtBAASaEYoZiEBohUIVAhUIBoomB/BUEBopUIBoipIBogmBDYJGEBogmBO4JmCBo8/V4QNJh7nCHAYNFgxYEMIxKGBpYqCU4oAFOoLtEAA8PBhYA/AB9///AQ5jFCABEfQ47MCYAbvBXQgiEUYKxFg4iEgbNGh4UEbgRNFCgoNBH4hpBOBYUBAwhwFHwJ3FOApaBNIpwFCYJpFOAovBNIpwFBgJbFOAgECKgwUDIgQABTYhwDJQIACKghwDKQRGGOAYfBAAZwHBghUEOASXCAAaiF/xSEKgprCIgibGAwO/BopUEKApwJAAyMEGoyoGSwhvHWQqLHOARgKbgpSHfAqYGOBJSEOBAMFOAyXEOBBEGOAyXEOBBEGOAyXEOA5EHOAqXFOA5EHOAqXGOAxEIOAgMIOAZEJOAaXHMQpEJAH4AOn6QJbIaDKQgYcKUATXJVxwNCZQ8fCwIND4C4H4ANDHAzUCBoY4GBAP+MIQEBBo//4IDCOIoXD+ANDewozDBoZGFBIZXBIw4NDAAZGFBo6NFEoYAERogNIKgk/Bo5UEBpBUEj5UMh5UMBpKpDg4KFAwRUDbgP4JARCBKgrEB/AsC/BNCAYINEfYQJBCQJiEBIQpDCQJiEv4JBHAT2DRggTBQIReBWAJiDBQJlDYIIgBYoY+BwBGCLwIVBOAYYBCYJUFOAYYBCYIzBHgIVBOAoTBKgYVBOA6NCwAVBOA6zEOAwlDSIhwF4ANCEAJKBOAvwcgYNCOAv/TQQYBGILhFAAn4DYJwDHwQAGBogUBAAx+ERIQAFPwiJCAAwNDL4YNJPYQAGRgZUJRgZUJBoiKC/wNETQZGEMwiaDIwhmEBohGDMwgNFEwS7EVAiNDLAgNFDARYDBowqBWAJGDBo0DH4JYDaQgAFDZKRGBpRxCBpQqCPooAFKoLDEAA8cBhYA/ACM/8AMKcQYAJaASXKWYTdDgwNI/+AawSyHAAJHCn64FBobeCHgwND/xLCeAoNDHAIFBCIINI8BnCKZA0BQYRGEBohxBv5YDBow0Bn5UFGIRGFSIYNG4AiBKgg/CKhQNFPYJUGBohUIBohUICgIADSYSpECgJiEKgwNCKAXAKg0fCgRCCLYWAYggNBCIJiHGAYDBBoJiFGAINBEwJwBMQowCOgQtFPwh0DH4TFEJgYYBOA4XBJgIYBaYRwEHwJMBBQLTDOAYlBJgIKBPwZwFHwIKB+ANCOA5KBD4INBOAwwBTQhwGGAN/BpBiBEQM/HYINBPwhiBS4X8GAR+EMQI4BBoJvCPwiFC/kPAIINGCof//oEDRgYxCAAwNDKgQAGTQZUCBpZUCAAqoDKgYNKKggADWwapDBpZGHBopGHBopGHBoqNHBoqNHBow4GBow4GBow4GBow4GTIgACfIYNJFQrREFRD7EKo/+Bg7HE/ANJDgQ2IeYZRHAH4AmgaYDn50HRgKLCv/8BpD6CZQINIC4QNBVgy2CBoYgCIojEDBoI4GBoRQBn7yHgLuDBoJGGBoQlBj7zIBAIlBh4uDAAhBBEoJYCKgwzCwBKCHgIAEGYY8EAAgzEHgaMHGYI8DPw5wEwBwTEoJwLUgatEMQ4uDPwzhNC4RPBEAKMGC4QNBEAINHC4INBEAIpGKAQgDBo8AnASDRYoAnA='))), + fontBitmap, 46, atob("ITZOMzs7SDxHNUdGIQ=="), 113+(scale<<8)+(1<<16) @@ -9,8 +14,6 @@ Graphics.prototype.setFontPaytoneOne = function(scale) { return this; }; -{ // must be inside our own scope here so that when we are unloaded everything disappears - // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global let drawTimeout; let g2 = Graphics.createArrayBuffer(g.getWidth(),90,1,{msb:true}); @@ -34,8 +37,9 @@ let draw = function() { x = R.w / 2; y = R.y + R.h / 2 - 12; // 12 = room for date var date = new Date(); - var hourStr = date.getHours(); - var minStr = date.getMinutes().toString().padStart(2,0); + var local_time = require("locale").time(date, 1); + var hourStr = local_time.split(":")[0].trim().padStart(2,'0'); + var minStr = local_time.split(":")[1].trim().padStart(2, '0'); dateStr = require("locale").dow(date, 1).toUpperCase()+ " "+ require("locale").date(date, 0).toUpperCase(); diff --git a/apps/slopeclock/metadata.json b/apps/slopeclock/metadata.json index 18820b2cc..d9d4d85ca 100644 --- a/apps/slopeclock/metadata.json +++ b/apps/slopeclock/metadata.json @@ -1,12 +1,12 @@ { "id": "slopeclock", "name": "Slope Clock", - "version":"0.02", + "version":"0.04", "description": "A clock where hours and minutes are divided by a sloping line. When the minute changes, the numbers slide off the screen", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], "type": "clock", "tags": "clock", - "supports" : ["BANGLEJS2"], + "supports" : ["BANGLEJS2"], "storage": [ {"name":"slopeclock.app.js","url":"app.js"}, {"name":"slopeclock.img","url":"app-icon.js","evaluate":true} diff --git a/apps/slopeclockpp/ChangeLog b/apps/slopeclockpp/ChangeLog index 1d28ea553..58299b236 100644 --- a/apps/slopeclockpp/ChangeLog +++ b/apps/slopeclockpp/ChangeLog @@ -4,3 +4,8 @@ 0.04: Changed to use clock_info for displayed data Made fonts smaller to avoid overlap when (eg) 22:00 Allowed black/white background (as that can look nice too) +0.05: Images in clkinfo are optional now +0.06: Added support for locale based time +0.07: README file update as UI interaction was not easy to understand +0.08: Stability improvements - ensure we continue even if a flat string can't be allocated + Stop ClockInfo text drawing outside the allocated area diff --git a/apps/slopeclockpp/README.md b/apps/slopeclockpp/README.md index 9352db685..c9e9d523d 100644 --- a/apps/slopeclockpp/README.md +++ b/apps/slopeclockpp/README.md @@ -10,6 +10,8 @@ to change (top right or bottom left). It should change color showing it is selected. * Swipe up or down to cycle through the info screens that can be displayed + when you have finished tap again towards the centre of the screen to unselect. + * Swipe left or right to change the type of info screens displayed (by default there is only one type of data so this will have no effect) diff --git a/apps/slopeclockpp/app.js b/apps/slopeclockpp/app.js index 99ef7272b..dca4a84e4 100644 --- a/apps/slopeclockpp/app.js +++ b/apps/slopeclockpp/app.js @@ -1,14 +1,3 @@ -Graphics.prototype.setFontPaytoneOne = function(scale) { - // Actual height 71 (81 - 11) - this.setFontCustom( - E.toString(require('heatshrink').decompress(atob('AFv4BZU/+ALJh//wALIgP//gYJj//8ALIgf//4YJv//HxMHDAI+JDAJkJDBgLBDBJvBDEZKYDBaVMn6VKY4P+cBfAXZQ9JEoIkKAGcDBZUBPhJkCBZU/DBSJBBZLUBDBLHMBYIYJdgIYJj4YKJAIYJHgQYIe4IYKBYYYHn4YKJAQYIQoIYJJAYYHJAgYHQoQYIJAn//iFIAAP+JBX/wBIJ//AQpAAB8BIK/CFJJAxtMDApIEDAxIFW5gYEJAoYFQooYGBYwYEJAoYFQooYFJAwYEQooYFJA4YEBZAYCQowYEJBAYCQo4YDJBIYCBZUBQo4A5WBKYDOhLWCDJE/cZUPBYT8HgYLDTY4LDGQ7VBEpIkEfw9/EpRJEEox6CJZJuDOI8HBYo+FBYo+FHow+EHoy9FHo3/4B7IK4wYHK4ZWGK4qUC/BCDK4ZWCIoIMDN4o4CIYQYGApAYCIgY3BOAYSBLoYlCRIQ4CR4b+BDAYFFCQoYGFYIYFYIgYHZooYebQhjTPhKVOVwwYFY5gGCcAz5CGQIECDAcHCYQAD/wYGAAhQDHAQYJn4MG4DaFAAiCDRIQAFN4ZeDAAbNEK44LDHw5WDK449EHw49EHww9EHwx7EEo57DEo7rDEo4kGEopJFZIpuEWAwwGPwh6FBgoLJAH4AVSgKRDRoKHFQoazBcIgYaX4oYFCQYYSXAIYKn74DAATeGAAgYEFYIYJFYIYWh4YLBYwYEN4IYJRAIYKN44YDN46bGDBJvHDH4Y0AAwSBBZIrBDH4YhAHF4BZUPLghjG//gAohjEh//4AFCj4YEgISBwAFBgYYFCQqIBAoYSFFQIYEn4+DFQQYF/wREDAgrBJQRiBDAgGB/hiEDBJPBDBJPCDAhvEDoIYELoP4MQgYIMQQYJMQQYIMQQYJBYQYIEgYYHEgYYG4BJDDAyuBEgRxBDAvwSYX3DAwAD/wYHAAfHDBX8DBeHY4xUEDArCCHoQSBDBPgDBX8DAr0DUoQYFVQYVBDAqeETAIYFSQSxCDApwEZQIYFaAoYGHwfgDAw+D/gYHV4Z2DBYZ9D4AYHEoRJBDA4TBGAIYHGQILCDA4A/ABMHBhd+Aws8NwjpBTYiZBcAZ7DBYIFEfILRBbIYFDVoIlDAooYCFYYeFgYxEDAwrBDAbyBY4YYB/AVBBAL9DZoeAFwIYGcwIYQCQQYE+AYDCQSIDCoIYIG4RNBDBRmBDEgIBDBWADBAIDDBAICDBACBZQIYHwACB4APBDAv8RAP+TAIYG+4CB/BNBDAoAGDAoAFDBjgFAAr5FDCyrBAAv+DAZdBAAvgDA3vAYSYBAASGBEAI1D4AMDA4XHN4xwDSYSIFK4Y1DKwY+D8A1DBYYlCFgI9HEoSNDHohLCHAI+CBYpbFPYYAFIQIkGIQiHEAH4ADPgKgEAAkBPZaIBDBLXCEhYYJVpYkCDBAkCDBIkCDBAkCDBAkDDBF/DBQkDDA4kDDBAkDDA4kC34YHgYLB8YYIEgP8OIIkJDYIYGEgXgDBAkB/AYIj5gCDA4kC4AYIEgQYIEgP+DgQYFEgYYIEgIUBDA8HVgawHVgYADIYIYKwAY/DH4Y/DF4AEn//BI4ABgf/+AMJDH4YjAH4AJj/ABRDiB/jzCdgcBdIfgOIIPBAAQLD/wnB/4oDh4MD+AeBDBCgBDAPgDBASBFAIYHwASBDBH4CQQYI4ASBZIYYEI4J0BDBJ8BDBAxBDAKJDJQoYBB4JjIDBSuCDAvwBAJsBDAyCBAQQYH8CFDDBLgDDAzQDDA7QDDBQxBOYQYGGgISBDBD5CDBAIBn4YJ/ybCDBClEDAylEDEZzBVwwACOYKuGAAalBDBKlBDAq3BAARvDDAS3BAASIDDAaSBKwwYCK4hWDDAY+DHogIBG4I9HgFgAQMDSgwAESwR7EAAh7GAAglCEhBCCJIgMGBZQA9j5JKcAKHJaYQMIUATrFAAT4Eb4gABdYjTFGAjsGVYYlJEgv/EhRLGJIjtHBYpxFNwYACfQkDBYpkFT4I+JHow+FBYx9EHox9EPYxXFPYoYFKw6WEDAXh/+DOApWC+E/+AFCN4v8FAJQCOAYSDv4hBRIpECcQISCDAYIBOwJTCIgIYFwEfNgI0BDAv4P4IYV+AIBDBIICDBZjBDCwIBR4IYIwBdCDA/8cwQYI+AkBY4YYEcA4SBfgrgF/AYLwAYERgIYJUoIACCoPAewIAC4ALCMAoABcwIYKN4YVBFYJWHgAVB8BBBKwyJDLQJWFRIXgK4Y9ECoIrBHwY9DOALACHo8AniADPYoAESwR7DAAokHAAaNCBZAMBBZQA5PAKoENYyDJXQYYQjgYKg4FEDAsDAogYGAowSEZIIYJfYLIEDAjuCwAYHagP//AYIBYIYJv4LBcQgYDHgIAB4AYGHgRdFAoQ8CAAJdDDAYLDOAgYCHgQABOAYYCHgYYHBwIADOAYJB8YLEOAgYBBYoYFAApjFAAzHFAAqIDDA7TEDAzGEDAw8EDA4LEDAw8EDAy4DDA48FDAr2EDA4LGDAiqDDA48GDAiFEDAw8HDAaFFDAw8HDAY8HDAY8IDAQ8IAH4AFv5nJgE/QBMAg6ZKgKBLEgIlGEIICCRwwhBFoN/WY4IB+DxDZA/Bfo5GC/0fco5GC+YLCHwhGC/+/AYXAdooAEDAhGDAAZXDHoQAESwhGDAAZXDgYLGOAhWCDBBWDDBCdCDB2DRIt//gzC8BpB/BvEwALBBAIrBDAYqBE4RdCDArVDLoQYE8ByCwCPBDAiOBCgIIBR4IYFUgXADBAUBYgIYHawQYJJoIcDMYoYCGoRjGOAZjGCIKJCPg/AUQWADA3/z4CB/goBDAoAD+LHGfMa4CDBJUCAAicBDBKYBAASbBDBJwC/5BDZQJwF+YYD4BXF/xBDRAY+D4IYDRAY+C/CZDN4Y+DQAZWEEoXAM4Y9EUYIGBHwRWEFAyUEDYp7GAAglBEhJLBJIoyGBZQA/MBDPEPI7DFfQy3FAAUBaAkBUQrdCGQSKFewYlBv41EEgQlCj//wBJFAAPwaoJbEbgTqCCIJOEHoQVBgbhFHoYuBGIJXDHoYVBAoLuECQJXDDAorBDAZvBOAhWDCoI3BOAYYEFwIYFKwYYBNIIYDN4gYBCQKJDAoPwAQIYCRIY3BMAgYFPIQPBDBA3Bv4YIBAIVBDBCCBn4YKOYIYY4ASBDBCuDDCn4cwR8FDAWAZoIYFAoM/+C0CY4b2CBIIFCY4xgB8DyCcAv+g/8j7jCcA7jEfI78DBYRTBAAp/BAAQ4CAAnABYR2CAAhvDgBFCAAgLDNQQAEN4aJCKxJXHHoZXHHog+HBYg+GPYY+HPYh9HdYZ9HEgolFEgwlFBYxLENwhxGGAzvET4gZGC5AA/ABl8AYV4BY0fdIU/OQx8BSYIDDUQv+AYokESgQDDcI2AWQTUHHwIDDY43AXwWADAz3Bv4YGCgQYJCgIYDAYIYKOAoYYJRZjOPhKVGDAqqBCgKuHYYKqBDgLHGHQPggEPcA8/NYU/HoolCIQQkGAEIA=='))), - 46, - atob("HTBFLTQ0PzU/Lz8+HQ=="), - 100+(scale<<8)+(1<<16) - ); - return this; -}; - { // must be inside our own scope here so that when we are unloaded everything disappears // we also define functions using 'let fn = function() {..}' for the same reason. function decls are global @@ -16,6 +5,17 @@ let settings = Object.assign( require("Storage").readJSON("slopeclockpp.default.json", true) || {}, require("Storage").readJSON("slopeclockpp.json", true) || {} ); +const fontBitmap = E.toString(require('heatshrink').decompress(atob('AFv4BZU/+ALJh//wALIgP//gYJj//8ALIgf//4YJv//HxMHDAI+JDAJkJDBgLBDBJvBDEZKYDBaVMn6VKY4P+cBfAXZQ9JEoIkKAGcDBZUBPhJkCBZU/DBSJBBZLUBDBLHMBYIYJdgIYJj4YKJAIYJHgQYIe4IYKBYYYHn4YKJAQYIQoIYJJAYYHJAgYHQoQYIJAn//iFIAAP+JBX/wBIJ//AQpAAB8BIK/CFJJAxtMDApIEDAxIFW5gYEJAoYFQooYGBYwYEJAoYFQooYFJAwYEQooYFJA4YEBZAYCQowYEJBAYCQo4YDJBIYCBZUBQo4A5WBKYDOhLWCDJE/cZUPBYT8HgYLDTY4LDGQ7VBEpIkEfw9/EpRJEEox6CJZJuDOI8HBYo+FBYo+FHow+EHoy9FHo3/4B7IK4wYHK4ZWGK4qUC/BCDK4ZWCIoIMDN4o4CIYQYGApAYCIgY3BOAYSBLoYlCRIQ4CR4b+BDAYFFCQoYGFYIYFYIgYHZooYebQhjTPhKVOVwwYFY5gGCcAz5CGQIECDAcHCYQAD/wYGAAhQDHAQYJn4MG4DaFAAiCDRIQAFN4ZeDAAbNEK44LDHw5WDK449EHw49EHww9EHwx7EEo57DEo7rDEo4kGEopJFZIpuEWAwwGPwh6FBgoLJAH4AVSgKRDRoKHFQoazBcIgYaX4oYFCQYYSXAIYKn74DAATeGAAgYEFYIYJFYIYWh4YLBYwYEN4IYJRAIYKN44YDN46bGDBJvHDH4Y0AAwSBBZIrBDH4YhAHF4BZUPLghjG//gAohjEh//4AFCj4YEgISBwAFBgYYFCQqIBAoYSFFQIYEn4+DFQQYF/wREDAgrBJQRiBDAgGB/hiEDBJPBDBJPCDAhvEDoIYELoP4MQgYIMQQYJMQQYIMQQYJBYQYIEgYYHEgYYG4BJDDAyuBEgRxBDAvwSYX3DAwAD/wYHAAfHDBX8DBeHY4xUEDArCCHoQSBDBPgDBX8DAr0DUoQYFVQYVBDAqeETAIYFSQSxCDApwEZQIYFaAoYGHwfgDAw+D/gYHV4Z2DBYZ9D4AYHEoRJBDA4TBGAIYHGQILCDA4A/ABMHBhd+Aws8NwjpBTYiZBcAZ7DBYIFEfILRBbIYFDVoIlDAooYCFYYeFgYxEDAwrBDAbyBY4YYB/AVBBAL9DZoeAFwIYGcwIYQCQQYE+AYDCQSIDCoIYIG4RNBDBRmBDEgIBDBWADBAIDDBAICDBACBZQIYHwACB4APBDAv8RAP+TAIYG+4CB/BNBDAoAGDAoAFDBjgFAAr5FDCyrBAAv+DAZdBAAvgDA3vAYSYBAASGBEAI1D4AMDA4XHN4xwDSYSIFK4Y1DKwY+D8A1DBYYlCFgI9HEoSNDHohLCHAI+CBYpbFPYYAFIQIkGIQiHEAH4ADPgKgEAAkBPZaIBDBLXCEhYYJVpYkCDBAkCDBIkCDBAkCDBAkDDBF/DBQkDDA4kDDBAkDDA4kC34YHgYLB8YYIEgP8OIIkJDYIYGEgXgDBAkB/AYIj5gCDA4kC4AYIEgQYIEgP+DgQYFEgYYIEgIUBDA8HVgawHVgYADIYIYKwAY/DH4Y/DF4AEn//BI4ABgf/+AMJDH4YjAH4AJj/ABRDiB/jzCdgcBdIfgOIIPBAAQLD/wnB/4oDh4MD+AeBDBCgBDAPgDBASBFAIYHwASBDBH4CQQYI4ASBZIYYEI4J0BDBJ8BDBAxBDAKJDJQoYBB4JjIDBSuCDAvwBAJsBDAyCBAQQYH8CFDDBLgDDAzQDDA7QDDBQxBOYQYGGgISBDBD5CDBAIBn4YJ/ybCDBClEDAylEDEZzBVwwACOYKuGAAalBDBKlBDAq3BAARvDDAS3BAASIDDAaSBKwwYCK4hWDDAY+DHogIBG4I9HgFgAQMDSgwAESwR7EAAh7GAAglCEhBCCJIgMGBZQA9j5JKcAKHJaYQMIUATrFAAT4Eb4gABdYjTFGAjsGVYYlJEgv/EhRLGJIjtHBYpxFNwYACfQkDBYpkFT4I+JHow+FBYx9EHox9EPYxXFPYoYFKw6WEDAXh/+DOApWC+E/+AFCN4v8FAJQCOAYSDv4hBRIpECcQISCDAYIBOwJTCIgIYFwEfNgI0BDAv4P4IYV+AIBDBIICDBZjBDCwIBR4IYIwBdCDA/8cwQYI+AkBY4YYEcA4SBfgrgF/AYLwAYERgIYJUoIACCoPAewIAC4ALCMAoABcwIYKN4YVBFYJWHgAVB8BBBKwyJDLQJWFRIXgK4Y9ECoIrBHwY9DOALACHo8AniADPYoAESwR7DAAokHAAaNCBZAMBBZQA5PAKoENYyDJXQYYQjgYKg4FEDAsDAogYGAowSEZIIYJfYLIEDAjuCwAYHagP//AYIBYIYJv4LBcQgYDHgIAB4AYGHgRdFAoQ8CAAJdDDAYLDOAgYCHgQABOAYYCHgYYHBwIADOAYJB8YLEOAgYBBYoYFAApjFAAzHFAAqIDDA7TEDAzGEDAw8EDA4LEDAw8EDAy4DDA48FDAr2EDA4LGDAiqDDA48GDAiFEDAw8HDAaFFDAw8HDAY8HDAY8IDAQ8IAH4AFv5nJgE/QBMAg6ZKgKBLEgIlGEIICCRwwhBFoN/WY4IB+DxDZA/Bfo5GC/0fco5GC+YLCHwhGC/+/AYXAdooAEDAhGDAAZXDHoQAESwhGDAAZXDgYLGOAhWCDBBWDDBCdCDB2DRIt//gzC8BpB/BvEwALBBAIrBDAYqBE4RdCDArVDLoQYE8ByCwCPBDAiOBCgIIBR4IYFUgXADBAUBYgIYHawQYJJoIcDMYoYCGoRjGOAZjGCIKJCPg/AUQWADA3/z4CB/goBDAoAD+LHGfMa4CDBJUCAAicBDBKYBAASbBDBJwC/5BDZQJwF+YYD4BXF/xBDRAY+D4IYDRAY+C/CZDN4Y+DQAZWEEoXAM4Y9EUYIGBHwRWEFAyUEDYp7GAAglBEhJLBJIoyGBZQA/MBDPEPI7DFfQy3FAAUBaAkBUQrdCGQSKFewYlBv41EEgQlCj//wBJFAAPwaoJbEbgTqCCIJOEHoQVBgbhFHoYuBGIJXDHoYVBAoLuECQJXDDAorBDAZvBOAhWDCoI3BOAYYEFwIYFKwYYBNIIYDN4gYBCQKJDAoPwAQIYCRIY3BMAgYFPIQPBDBA3Bv4YIBAIVBDBCCBn4YKOYIYY4ASBDBCuDDCn4cwR8FDAWAZoIYFAoM/+C0CY4b2CBIIFCY4xgB8DyCcAv+g/8j7jCcA7jEfI78DBYRTBAAp/BAAQ4CAAnABYR2CAAhvDgBFCAAgLDNQQAEN4aJCKxJXHHoZXHHog+HBYg+GPYY+HPYh9HdYZ9HEgolFEgwlFBYxLENwhxGGAzvET4gZGC5AA/ABl8AYV4BY0fdIU/OQx8BSYIDDUQv+AYokESgQDDcI2AWQTUHHwIDDY43AXwWADAz3Bv4YGCgQYJCgIYDAYIYKOAoYYJRZjOPhKVGDAqqBCgKuHYYKqBDgLHGHQPggEPcA8/NYU/HoolCIQQkGAEIA=='))); + +Graphics.prototype.setFontPaytoneOne = function(scale) { + // Actual height 71 (81 - 11) + this.setFontCustom(fontBitmap, + 46, + atob("HTBFLTQ0PzU/Lz8+HQ=="), + 100+(scale<<8)+(1<<16) + ); + return this; +}; let drawTimeout; @@ -47,12 +47,22 @@ let bgColor = bgColors[(Math.random()*bgColors.length)|0]||"#000"; // Draw the hour, and the minute into an offscreen buffer let draw = function() { + // queue next draw + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + animate(false, function() { + draw(); + }); + }, 60000 - (Date.now() % 60000)); + // Now draw this one R = Bangle.appRect; x = R.w / 2; y = R.y + R.h / 2 - 12; // 12 = room for date var date = new Date(); - var hourStr = date.getHours(); - var minStr = date.getMinutes().toString().padStart(2,0); + var local_time = require("locale").time(date, 1); + var hourStr = local_time.split(":")[0].trim().padStart(2,'0'); + var minStr = local_time.split(":")[1].trim().padStart(2, '0'); dateStr = require("locale").dow(date, 1).toUpperCase()+ " "+ require("locale").date(date, 0).toUpperCase(); @@ -69,15 +79,6 @@ let draw = function() { g2.setColor(0).fillPoly([0,0, g2.getWidth(),0, 0,slope*2]); // start the animation *in* animate(true); - - // queue next draw - if (drawTimeout) clearTimeout(drawTimeout); - drawTimeout = setTimeout(function() { - drawTimeout = undefined; - animate(false, function() { - draw(); - }); - }, 60000 - (Date.now() % 60000)); }; let isAnimIn = true; @@ -121,19 +122,23 @@ let animate = function(isIn, callback) { // clock info menus (scroll up/down for info) let clockInfoDraw = (itm, info, options) => { - let texty = options.y+26; - g.reset().setFont("6x15").setBgColor(options.bg).setColor(options.fg).clearRect(options.x, texty, options.x+options.w-2, texty+15); + let texty = options.y+41; + // set a cliprect to stop us drawing outside our box + g.reset().setClipRect(options.x, options.y, options.x+options.w-1, options.y+options.h-1); + g.setFont("6x15").setBgColor(options.bg).setColor(options.fg).clearRect(options.x, texty-15, options.x+options.w-2, texty); if (options.focus) g.setColor(options.hl); if (options.x < g.getWidth()/2) { // left align let x = options.x+2; - g.clearRect(x, options.y, x+23, options.y+23).drawImage(info.img, x, options.y); - g.setFontAlign(-1,-1).drawString(info.text, x,texty); + if (info.img) g.clearRect(x, options.y, x+23, options.y+23).drawImage(info.img, x, options.y); + g.setFontAlign(-1,1).drawString(info.text, x,texty); } else { // right align let x = options.x+options.w-3; - g.clearRect(x-23, options.y, x, options.y+23).drawImage(info.img, x-23, options.y); - g.setFontAlign(1,-1).drawString(info.text, x,texty); + if (info.img) g.clearRect(x-23, options.y, x, options.y+23).drawImage(info.img, x-23, options.y); + g.setFontAlign(1,1).drawString(info.text, x,texty); } + // return ClipRect + g.setClipRect(0,0,g.getWidth()-1, g.getHeight()-1); }; let clockInfoItems = require("clock_info").load(); let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:126, y:24, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg, hl : "#f00"/*red*/ }); diff --git a/apps/slopeclockpp/metadata.json b/apps/slopeclockpp/metadata.json index 3100d8b50..fbab02fca 100644 --- a/apps/slopeclockpp/metadata.json +++ b/apps/slopeclockpp/metadata.json @@ -1,6 +1,6 @@ { "id": "slopeclockpp", "name": "Slope Clock ++", - "version":"0.04", + "version":"0.08", "description": "A clock where hours and minutes are divided by a sloping line. When the minute changes, the numbers slide off the screen. This is a clone of the original Slope Clock which shows extra information and allows the colors to be selected.", "icon": "app.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/smpltmr/ChangeLog b/apps/smpltmr/ChangeLog index b09100f50..61111482e 100644 --- a/apps/smpltmr/ChangeLog +++ b/apps/smpltmr/ChangeLog @@ -1,4 +1,7 @@ 0.01: Release 0.02: Rewrite with new interface 0.03: Added clock infos to expose timer functionality to clocks. -0.04: Improvements of clock infos. \ No newline at end of file +0.04: Improvements of clock infos. +0.05: Updated clkinfo icon. +0.06: Ensure Timer supplies an image for clkinfo items +0.07: Update clock_info to avoid a redraw diff --git a/apps/smpltmr/clkinfo.js b/apps/smpltmr/clkinfo.js index 1a63a9b7e..ac01cfb59 100644 --- a/apps/smpltmr/clkinfo.js +++ b/apps/smpltmr/clkinfo.js @@ -63,15 +63,14 @@ } catch(ex){ } } - var img = atob("GBiBAeAAB+AAB/v/3/v/3/v/3/v/3/v/n/n/H/z+P/48//85//+b//+b//8p//4E//yCP/kBH/oAn/oAX/oAX/oAX/oAX+AAB+AABw==") var smpltmrItems = { name: "Timer", - img: img, + img: atob("GBiBAAB+AAB+AAAYAAAYAAB+AA3/sA+B8A4AcAwMMBgPGBgPmDAPjDAPzDAPzDP/zDP/zDH/jBn/mBj/GAw8MA4AcAeB4AH/gAB+AA=="), items: [ { name: null, - get: () => ({ text: getAlarmMinutesText() + (isAlarmEnabled() ? " min" : ""), img: null}), - show: function() { smpltmrItems.items[0].emit("redraw"); }, + get: () => ({ text: getAlarmMinutesText() + (isAlarmEnabled() ? " min" : ""), img: smpltmrItems.img }), + show: function() {}, hide: function () {}, run: function() { } }, @@ -82,8 +81,8 @@ offsets.forEach((o, i) => { smpltmrItems.items = smpltmrItems.items.concat({ name: null, - get: () => ({ text: (o > 0 ? "+" : "") + o + " min.", img: null}), - show: function() { smpltmrItems.items[i+1].emit("redraw"); }, + get: () => ({ text: (o > 0 ? "+" : "") + o + " min.", img: smpltmrItems.img }), + show: function() {}, hide: function () {}, run: function() { if(o > 0) increaseAlarm(o); @@ -95,4 +94,4 @@ }); return smpltmrItems; -}) \ No newline at end of file +}) diff --git a/apps/smpltmr/metadata.json b/apps/smpltmr/metadata.json index ce526d1ba..b0d1a34da 100644 --- a/apps/smpltmr/metadata.json +++ b/apps/smpltmr/metadata.json @@ -2,7 +2,7 @@ "id": "smpltmr", "name": "Simple Timer", "shortName": "Simple Timer", - "version": "0.04", + "version": "0.07", "description": "A very simple app to start a timer.", "icon": "app.png", "tags": "tool,alarm,timer,clkinfo", diff --git a/apps/spotrem/ChangeLog b/apps/spotrem/ChangeLog index 8e3d8b652..a92ed3de2 100644 --- a/apps/spotrem/ChangeLog +++ b/apps/spotrem/ChangeLog @@ -3,3 +3,6 @@ 0.03: change handling of intent extras. 0.04: New layout. 0.05: Add widgets field. Tweak layout. +0.06: Make compatible with Fastload Utils app. +0.07: Remove just the specific listeners to not interfere with Quick Launch +when fastloading. diff --git a/apps/spotrem/app.js b/apps/spotrem/app.js index 7e76d84bc..f9046c4a6 100644 --- a/apps/spotrem/app.js +++ b/apps/spotrem/app.js @@ -1,21 +1,23 @@ +{ /* Bluetooth.println(JSON.stringify({t:"intent", action:"", flags:["flag1", "flag2",...], categories:["category1","category2",...], mimetype:"", data:"", package:"", class:"", target:"", extra:{someKey:"someValueOrString"}})); */ -var R; -var backToMenu = false; -var isPaused = true; -var dark = g.theme.dark; // bool +let R; +let widgetUtils = require("widget_utils"); +let backToMenu = false; +let isPaused = true; +let dark = g.theme.dark; // bool // The main layout of the app -function gfx() { - //Bangle.drawWidgets(); +let gfx = function() { + widgetUtils.hide(); R = Bangle.appRect; marigin = 8; // g.drawString(str, x, y, solid) g.clearRect(R); g.reset(); - + if (dark) {g.setColor(0x07E0);} else {g.setColor(0x03E0);} // Green on dark theme, DarkGreen on light theme. g.setFont("4x6:2"); g.setFontAlign(1, 0, 0); @@ -44,10 +46,10 @@ function gfx() { g.setFontAlign(1, 1, 0); g.drawString("Saved", R.x + R.w - 2*marigin, R.y + R.h - 2*marigin); -} +}; // Touch handler for main layout -function touchHandler(_, xy) { +let touchHandler = function(_, xy) { x = xy.x; y = xy.y; len = (R.wb-1 instead of a>b. if ((R.x-1 { - if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup"); - } + {mode : "updown", + remove : ()=>{ + Bangle.removeListener("touch", touchHandler); + Bangle.removeListener("swipe", swipeHandler); + clearWatch(buttonHandler); + widgetUtils.show(); + } + }, + ud => { + if (ud) Bangle.musicControl(ud>0 ? "volumedown" : "volumeup"); + } ); Bangle.on("touch", touchHandler); Bangle.on("swipe", swipeHandler); -} - - + let buttonHandler = setWatch(()=>{load();}, BTN, {edge:'falling'}); +}; // Get back to the main layout -function backToGfx() { +let backToGfx = function() { E.showMenu(); g.clear(); g.reset(); - Bangle.removeAllListeners("touch"); - Bangle.removeAllListeners("swipe"); setUI(); gfx(); backToMenu = false; -} +}; /* The functions for interacting with Android and the Spotify app */ simpleSearch = ""; -function simpleSearchTerm() { // input a simple search term without tags, overrides search with tags (artist and track) +let simpleSearchTerm = function() { // input a simple search term without tags, overrides search with tags (artist and track) require("textinput").input({text:simpleSearch}).then(result => {simpleSearch = result;}).then(() => {E.showMenu(searchMenu);}); -} +}; artist = ""; -function artistSearchTerm() { // input artist to search for +let artistSearchTerm = function() { // input artist to search for require("textinput").input({text:artist}).then(result => {artist = result;}).then(() => {E.showMenu(searchMenu);}); -} +}; track = ""; -function trackSearchTerm() { // input track to search for +let trackSearchTerm = function() { // input track to search for require("textinput").input({text:track}).then(result => {track = result;}).then(() => {E.showMenu(searchMenu);}); -} +}; album = ""; -function albumSearchTerm() { // input album to search for +let albumSearchTerm = function() { // input album to search for require("textinput").input({text:album}).then(result => {album = result;}).then(() => {E.showMenu(searchMenu);}); -} +}; -function searchPlayWOTags() {//make a spotify search and play using entered terms +let searchPlayWOTags = function() {//make a spotify search and play using entered terms searchString = simpleSearch; Bluetooth.println(JSON.stringify({t:"intent", action:"android.media.action.MEDIA_PLAY_FROM_SEARCH", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", target:"activity", extra:{query:searchString}, flags:["FLAG_ACTIVITY_NEW_TASK"]})); -} +}; -function searchPlayWTags() {//make a spotify search and play using entered terms +let searchPlayWTags = function() {//make a spotify search and play using entered terms searchString = (artist=="" ? "":("artist:\""+artist+"\"")) + ((artist!="" && track!="") ? " ":"") + (track=="" ? "":("track:\""+track+"\"")) + (((artist!="" && album!="") || (track!="" && album!="")) ? " ":"") + (album=="" ? "":(" album:\""+album+"\"")); Bluetooth.println(JSON.stringify({t:"intent", action:"android.media.action.MEDIA_PLAY_FROM_SEARCH", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", target:"activity", extra:{query:searchString}, flags:["FLAG_ACTIVITY_NEW_TASK"]})); -} +}; -function playVreden() {//Play the track "Vreden" by Sara Parkman via spotify uri-link +let playVreden = function() {//Play the track "Vreden" by Sara Parkman via spotify uri-link Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:track:5QEFFJ5tAeRlVquCUNpAJY:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); -} +}; -function playVredenAlternate() {//Play the track "Vreden" by Sara Parkman via spotify uri-link +let playVredenAlternate = function() {//Play the track "Vreden" by Sara Parkman via spotify uri-link Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:track:5QEFFJ5tAeRlVquCUNpAJY:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK"]})); -} +}; -function searchPlayVreden() {//Play the track "Vreden" by Sara Parkman via search and play +let searchPlayVreden = function() {//Play the track "Vreden" by Sara Parkman via search and play Bluetooth.println(JSON.stringify({t:"intent", action:"android.media.action.MEDIA_PLAY_FROM_SEARCH", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", target:"activity", extra:{query:'artist:"Sara Parkman" track:"Vreden"'}, flags:["FLAG_ACTIVITY_NEW_TASK"]})); -} +}; -function openAlbum() {//Play EP "The Blue Room" by Coldplay +let openAlbum = function() {//Play EP "The Blue Room" by Coldplay Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:album:3MVb2CWB36x7VwYo5sZmf2", target:"activity", flags:["FLAG_ACTIVITY_NEW_TASK"]})); -} +}; -function searchPlayAlbum() {//Play EP "The Blue Room" by Coldplay via search and play +let searchPlayAlbum = function() {//Play EP "The Blue Room" by Coldplay via search and play Bluetooth.println(JSON.stringify({t:"intent", action:"android.media.action.MEDIA_PLAY_FROM_SEARCH", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", target:"activity", extra:{query:'album:"The blue room" artist:"Coldplay"', "android.intent.extra.focus":"vnd.android.cursor.item/album"}, flags:["FLAG_ACTIVITY_NEW_TASK"]})); -} +}; -function spotifyWidget(action) { +let spotifyWidget = function(action) { Bluetooth.println(JSON.stringify({t:"intent", action:("com.spotify.mobile.android.ui.widget."+action), package:"com.spotify.music", target:"broadcastreceiver"})); -} +}; -function gadgetbridgeWake() { +let gadgetbridgeWake = function() { Bluetooth.println(JSON.stringify({t:"intent", target:"activity", flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_CLEAR_TASK", "FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS", "FLAG_ACTIVITY_NO_ANIMATION"], package:"gadgetbridge", class:"nodomain.freeyourgadget.gadgetbridge.activities.WakeActivity"})); -} +}; -function spotifyPlaylistDW() { +let spotifyPlaylistDW = function() { Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZEVXcRfaeEbxXIgb:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); -} +}; -function spotifyPlaylistDM1() { +let spotifyPlaylistDM1 = function() { Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E365VyzxE0mxF:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); -} +}; -function spotifyPlaylistDM2() { +let spotifyPlaylistDM2 = function() { Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E38LZHLFnrM61:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); -} +}; -function spotifyPlaylistDM3() { +let spotifyPlaylistDM3 = function() { Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E36RU87qzgBFP:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); -} +}; -function spotifyPlaylistDM4() { +let spotifyPlaylistDM4 = function() { Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E396gGyCXEBFh:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); -} +}; -function spotifyPlaylistDM5() { +let spotifyPlaylistDM5 = function() { Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E37a0Tt6CKJLP:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); -} +}; -function spotifyPlaylistDM6() { +let spotifyPlaylistDM6 = function() { Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1E36UIQLQK79od:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); -} +}; -function spotifyPlaylistDD() { +let spotifyPlaylistDD = function() { Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZF1EfWFiI7QfIAKq:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); -} +}; -function spotifyPlaylistRR() { +let spotifyPlaylistRR = function() { Bluetooth.println(JSON.stringify({t:"intent", action:"android.intent.action.VIEW", categories:["android.intent.category.DEFAULT"], package:"com.spotify.music", data:"spotify:user:spotify:playlist:37i9dQZEVXbs0XkE2V8sMO:play", target:"activity" , flags:["FLAG_ACTIVITY_NEW_TASK", "FLAG_ACTIVITY_NO_ANIMATION"/*, "FLAG_ACTIVITY_CLEAR_TOP", "FLAG_ACTIVITY_PREVIOUS_IS_TOP"*/]})); -} +}; // Spotify Remote Menu -var spotifyMenu = { - "" : { title : " ", +let spotifyMenu = { + "" : { title : " Menu ", back: backToGfx }, "Controls" : ()=>{E.showMenu(controlMenu);}, "Search and play" : ()=>{E.showMenu(searchMenu);}, @@ -234,8 +240,8 @@ var spotifyMenu = { }; -var controlMenu = { - "" : { title : " ", +let controlMenu = { + "" : { title : " Controls ", back: () => {if (backToMenu) E.showMenu(spotifyMenu); if (!backToMenu) backToGfx();} }, "Play" : ()=>{Bangle.musicControl("play");}, @@ -246,8 +252,8 @@ var controlMenu = { "Messages Music Controls" : ()=>{load("messagesmusic.app.js");}, }; -var searchMenu = { - "" : { title : " ", +let searchMenu = { + "" : { title : " Search ", back: () => {if (backToMenu) E.showMenu(spotifyMenu); if (!backToMenu) backToGfx();} }, "Search term w/o tags" : ()=>{simpleSearchTerm();}, @@ -258,8 +264,8 @@ var searchMenu = { "Execute search and play with tags" : ()=>{searchPlayWTags();}, }; -var savedMenu = { - "" : { title : " ", +let savedMenu = { + "" : { title : " Saved ", back: () => {if (backToMenu) E.showMenu(spotifyMenu); if (!backToMenu) backToGfx();} }, "Play Discover Weekly" : ()=>{spotifyPlaylistDW();}, @@ -279,3 +285,4 @@ var savedMenu = { Bangle.loadWidgets(); setUI(); gfx(); +} diff --git a/apps/spotrem/metadata.json b/apps/spotrem/metadata.json index a0261ba13..5818d9aee 100644 --- a/apps/spotrem/metadata.json +++ b/apps/spotrem/metadata.json @@ -1,7 +1,7 @@ { "id": "spotrem", "name": "Remote for Spotify", - "version": "0.05", + "version": "0.07", "description": "Control spotify on your android device.", "readme": "README.md", "type": "app", diff --git a/apps/sunclock/ChangeLog b/apps/sunclock/ChangeLog new file mode 100644 index 000000000..d63f1567e --- /dev/null +++ b/apps/sunclock/ChangeLog @@ -0,0 +1,2 @@ +0.01: First commit +0.02: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps diff --git a/apps/sunclock/app.js b/apps/sunclock/app.js index 4609565a2..1685bc218 100644 --- a/apps/sunclock/app.js +++ b/apps/sunclock/app.js @@ -9,8 +9,8 @@ SunCalc = require("suncalc.js"); loc = require('locale'); const LOCATION_FILE = "mylocation.json"; const xyCenter = g.getWidth() / 2 + 3; -const yposTime = 60; -const yposDate = 100; +const yposTime = 60; +const yposDate = 100; const yposRS = 135; const yposPos = 160; var rise = "07:00"; @@ -19,13 +19,21 @@ var pos = {altitude: 20, azimuth: 135}; var noonpos = {altitude: 37, azimuth: 180}; let idTimeout = null; + + function updatePos() { + function radToDeg(pos) { + return { // instead of mofidying suncalc + azimuth: Math.round((pos.azimuth / rad + 180) % 360), + altitude: Math.round( pos.altitude / rad) + }; + } coord = require("Storage").readJSON(LOCATION_FILE,1)|| {"lat":53.3,"lon":10.1,"location":"Pattensen"}; - pos = SunCalc.getPosition(Date.now(), coord.lat, coord.lon); + pos = radToDeg(SunCalc.getPosition(Date.now(), coord.lat, coord.lon)); times = SunCalc.getTimes(Date.now(), coord.lat, coord.lon); rise = times.sunrise.toString().split(" ")[4].substr(0,5); set = times.sunset.toString().split(" ")[4].substr(0,5); - noonpos = SunCalc.getPosition(times.solarNoon, coord.lat, coord.lon); + noonpos = radToDeg(SunCalc.getPosition(times.solarNoon, coord.lat, coord.lon)); } function drawSimpleClock() { @@ -38,7 +46,7 @@ function drawSimpleClock() { var time = da[4].substr(0, 5); // draw time - g.setFont("Vector",60); + g.setFont("Vector",60); g.drawString(time, xyCenter, yposTime, true); var date = [loc.dow(new Date(),1), loc.date(d,1)].join(" "); // draw day of week, date @@ -51,7 +59,7 @@ function drawSimpleClock() { g.setFont("Vector",21); g.drawString(`H${pos.altitude}/${noonpos.altitude} Az${pos.azimuth}`, xyCenter, yposPos, true); // draw sun pos - + let t = d.getSeconds()*1000 + d.getMilliseconds(); idTimeout = setTimeout(drawSimpleClock, 60000 - t); // time till next minute } diff --git a/apps/sunclock/metadata.json b/apps/sunclock/metadata.json index 617d76821..a9155f6f6 100644 --- a/apps/sunclock/metadata.json +++ b/apps/sunclock/metadata.json @@ -1,7 +1,7 @@ { "id": "sunclock", "name": "Sun Clock", - "version": "0.01", + "version": "0.02", "description": "A clock with sunset/sunrise, sun height/azimuth", "icon": "app.png", "type": "clock", @@ -11,7 +11,6 @@ "allow_emulator": true, "storage": [ {"name":"sunclock.app.js","url":"app.js"}, - {"name":"sunclock.img","url":"app-icon.js","evaluate":true}, - {"name":"suncalc.js","url":"suncalc.js"} + {"name":"sunclock.img","url":"app-icon.js","evaluate":true} ] } diff --git a/apps/swp2clk/ChangeLog b/apps/swp2clk/ChangeLog index 083b20858..d6f9f6e8c 100644 --- a/apps/swp2clk/ChangeLog +++ b/apps/swp2clk/ChangeLog @@ -1,3 +1,4 @@ 0.01: Initial creation of "Swipe back to the Clock" App. Let's you swipe from left to right on any app to return back to the clock face. 0.02: Fix deleting from white and black lists. 0.03: Adapt to availability of Bangle.showClock and Bangle.load +0.04: Fix 'Uncaught ReferenceError: "__FILE__" is not defined' error (fix #2326) diff --git a/apps/swp2clk/boot.js b/apps/swp2clk/boot.js index 389c3dddf..f2c642adf 100644 --- a/apps/swp2clk/boot.js +++ b/apps/swp2clk/boot.js @@ -29,12 +29,13 @@ })(Bangle.load); let swipeHandler = (dir) => { - log("swipe:" + dir + " on app: " + __FILE__); + let currentFile = global.__FILE__||"default"; + log("swipe:" + dir + " on app: " + currentFile); - if (!inhibit && dir === 1 && !Bangle.CLOCK && __FILE__ != ".bootcde") { - log("on a not clock app " + __FILE__); - if ((settings.mode === 1 && settings.whiteList.includes(__FILE__)) || // "White List" - (settings.mode === 2 && !settings.blackList.includes(__FILE__)) || // "Black List" + if (!inhibit && dir === 1 && !Bangle.CLOCK) { + log("on a not clock app " + currentFile); + if ((settings.mode === 1 && settings.whiteList.includes(currentFile)) || // "White List" + (settings.mode === 2 && !settings.blackList.includes(currentFile)) || // "Black List" settings.mode === 3) { // "Always" log("load clock"); Bangle.showClock(); diff --git a/apps/swp2clk/metadata.json b/apps/swp2clk/metadata.json index 7552de26c..b4436bd39 100644 --- a/apps/swp2clk/metadata.json +++ b/apps/swp2clk/metadata.json @@ -2,7 +2,7 @@ "id": "swp2clk", "name": "Swipe back to the Clock", "shortName": "Swipe to Clock", - "version": "0.03", + "version": "0.04", "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": "bootloader", diff --git a/apps/swscroll/ChangeLog b/apps/swscroll/ChangeLog index c650baf72..c5fc9dcb4 100644 --- a/apps/swscroll/ChangeLog +++ b/apps/swscroll/ChangeLog @@ -1 +1,2 @@ 0.01: Inital release. +0.02: Rebasing on latest changes to showScroller_Q3 (https://github.com/espruino/Espruino/commit/2d3c34ef7c2b9fe2118e816aacd2e096adb99596). diff --git a/apps/swscroll/boot.js b/apps/swscroll/boot.js index fc5650cad..2b1b00de3 100644 --- a/apps/swscroll/boot.js +++ b/apps/swscroll/boot.js @@ -1,13 +1,14 @@ -E.showScroller = (function(options) { +E.showScroller = (function(options) { /* options = { h = height c = # of items scroll = initial scroll position scrollMin = minimum scroll amount (can be negative) draw = function(idx, rect) - select = function(idx) + remove = function() + select = function(idx, touch) } - + returns { draw = draw all drawItem(idx) = draw specific item @@ -15,46 +16,13 @@ E.showScroller = (function(options) { */ if (!options) return Bangle.setUI(); // remove existing handlers -var menuShowing = false; -var R = Bangle.appRect; -var Y = Bangle.appRect.y; -var n = Math.ceil(R.h/options.h); -var menuScrollMin = 0|options.scrollMin; -var menuScrollMax = options.h*options.c - R.h; -if (menuScrollMax { - g.reset().clearRect(R.x,R.y,R.x2,R.y2); - g.setClipRect(R.x,R.y,R.x2,R.y2); - var a = YtoIdx(R.y); - var b = Math.min(YtoIdx(R.y2),options.c-1); - for (var i=a;i<=b;i++) - options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h}); - g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); -}, drawItem : i => { - var y = idxToY(i); - g.reset().setClipRect(R.x,y,R.x2,y+options.h); - options.draw(i, {x:R.x,y:y,w:R.w,h:options.h}); - g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); -}}; -var rScroll = s.scroll&~1; // rendered menu scroll (we only shift by 2 because of dither) -s.draw(); // draw the full scroller -g.flip(); // force an update now to make this snappier Bangle.setUI({ mode : "custom", back : options.back, - swipe : (hor,ver)=>{ + remove : options.remove, + swipe : (_,UD)=>{ pixels = 120; - var dy = ver*pixels; + var dy = UD*pixels; if (s.scroll - dy > menuScrollMax) dy = s.scroll - menuScrollMax-8; // Makes it so the last 'page' has the same position as previous pages. This should be done dynamically (change the static 8 to be a variable) so the offset is correct even when no widget field or title field is present. if (s.scroll - dy < menuScrollMin) @@ -66,7 +34,7 @@ Bangle.setUI({ if (!dy || options.c<=3) return; //options.c<=3 should maybe be dynamic, so 3 would be replaced by a variable dependent on R=Bangle.appRect. It's here so we don't try to scroll if all entries fit in the app rectangle. g.reset().setClipRect(R.x,R.y,R.x2,R.y2); g.scroll(0,dy); - var d = ver*pixels; + var d = UD*pixels; if (d < 0) { g.setClipRect(R.x,R.y2-(1-d),R.x2,R.y2); let i = YtoIdx(R.y2-(1-d)); @@ -92,9 +60,47 @@ Bangle.setUI({ }, touch : (_,e)=>{ if (e.y=0) && i=0) && i { + g.reset().clearRect(R.x,R.y,R.x2,R.y2); + g.setClipRect(R.x,R.y,R.x2,R.y2); + var a = YtoIdx(R.y); + var b = Math.min(YtoIdx(R.y2),options.c-1); + for (var i=a;i<=b;i++) + options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h}); + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); +}, drawItem : i => { + var y = idxToY(i); + g.reset().setClipRect(R.x,y,R.x2,y+options.h); + options.draw(i, {x:R.x,y:y,w:R.w,h:options.h}); + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); +}}; +var rScroll = s.scroll&~1; // rendered menu scroll (we only shift by 2 because of dither) +s.draw(); // draw the full scroller +g.flip(); // force an update now to make this snappier return s; -}); +}) diff --git a/apps/swscroll/metadata.json b/apps/swscroll/metadata.json index cb345054e..4edbfa2ba 100644 --- a/apps/swscroll/metadata.json +++ b/apps/swscroll/metadata.json @@ -1,7 +1,7 @@ { "id": "swscroll", "name": "Swipe menus", - "version": "0.01", + "version": "0.02", "description": "Replace built in E.showScroller to act on swipe instead of drag. Navigate menus in discrete steps instead of a continuous motion.", "readme": "README.md", "icon": "app.png", diff --git a/apps/taglaunch/ChangeLog b/apps/taglaunch/ChangeLog index 5560f00bc..981f50386 100644 --- a/apps/taglaunch/ChangeLog +++ b/apps/taglaunch/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Use Bangle.showClock for changing to clock (Backport from launch) diff --git a/apps/taglaunch/app.js b/apps/taglaunch/app.js index 07a7021db..c940284c2 100644 --- a/apps/taglaunch/app.js +++ b/apps/taglaunch/app.js @@ -69,16 +69,6 @@ let tagKeys = Object.keys(tags).filter(tag => tag !== "clock" || settings.showCl if (!settings.fullscreen) Bangle.loadWidgets(); -let returnToClock = function() { - // unload everything manually - // ... or we could just call `load();` but it will be slower - Bangle.setUI(); // remove scroller's handling - if (lockTimeout) clearTimeout(lockTimeout); - Bangle.removeListener("lock", lockHandler); - // now load the default clock - just call .bootcde as this has the code already - setTimeout(eval,0,s.read(".bootcde")); -}; - let showTagMenu = (tag) => { E.showScroller({ h : 64*scaleval, c : appsByTag[tag].length, @@ -121,7 +111,12 @@ let showMainMenu = () => { let tag = tagKeys[i]; showTagMenu(tag); }, - back : returnToClock // button press or tap in top left calls returnToClock now + back : Bangle.showClock, // button press or tap in top left shows clock now + remove : () => { + // cleanup the timeout to not leave anything behind after being removed from ram + if (lockTimeout) clearTimeout(lockTimeout); + Bangle.removeListener("lock", lockHandler); + } }); }; showMainMenu(); @@ -134,7 +129,7 @@ let lockHandler = function(locked) { if (lockTimeout) clearTimeout(lockTimeout); lockTimeout = undefined; if (locked) { - lockTimeout = setTimeout(returnToClock, 10000); + lockTimeout = setTimeout(Bangle.showClock, 10000); } }; Bangle.on("lock", lockHandler); diff --git a/apps/taglaunch/metadata.json b/apps/taglaunch/metadata.json index aded51314..d7f1954b1 100644 --- a/apps/taglaunch/metadata.json +++ b/apps/taglaunch/metadata.json @@ -2,7 +2,7 @@ "id": "taglaunch", "name": "Tag Launcher", "shortName": "Taglauncher", - "version": "0.01", + "version": "0.02", "description": "Launcher that puts all applications into submenus based on their tag. With many applications installed this can result in a faster application selection than the linear access of the default launcher.", "readme": "README.md", "icon": "app.png", diff --git a/apps/tempmonitor/ChangeLog b/apps/tempmonitor/ChangeLog new file mode 100644 index 000000000..2d9536fb9 --- /dev/null +++ b/apps/tempmonitor/ChangeLog @@ -0,0 +1 @@ +0.01: 1st version: saves values to csv diff --git a/apps/tempmonitor/README.md b/apps/tempmonitor/README.md new file mode 100644 index 000000000..094a47868 --- /dev/null +++ b/apps/tempmonitor/README.md @@ -0,0 +1,40 @@ +# Temperature Monitor (with logging) +Temperature monitor that shows temperature on real time but also allows to store in a file for a later process. + +Compatible with BangleJS1,BangleJS2,and EMSCRIPTENx emulators + +## Pictures: + +Bangle JS1 + +![](photo_banglejs1.jpg) + +Screenshot BJS2 + +![](ss_emul_bjs2.png) + +Screenshot BJS1 + +![](ss_emul_bjs1.png) + + + +## Usage + +Open and see a temperature in the screen +Download the CSV file and process in your favourite spreadsheet software + +## Features + +Colours, all inputs , graph, widgets loaded +Counter for Times Display + + +## Controls + +exit: left side + + +## Creator + +Daniel Perez \ No newline at end of file diff --git a/apps/tempmonitor/app-icon.js b/apps/tempmonitor/app-icon.js new file mode 100644 index 000000000..807f58d38 --- /dev/null +++ b/apps/tempmonitor/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4X/AoPzvswnmT54cQgWAhWq1ALGlWoBZOptUKqtoBQsK0ta1QLHlWVqwLHgWpqtVtQLGEQILBrQLGCYIADBYgiDBY8KyoLJIQIAEtQiDHIQADrWALgYLFqyECEQpiDLggHC1QdBrWgEQVqAQOmLAQkBlIUB02lq28gFpKoQLBq2+1NW+2qGwVolQIB20prPaBYVq1ALCuwLB8sC02VBYMAhILB1Nb/uqgWVsBfBBYdZ/wLIEYP//TwBC41a/9v+wLHq1/7/6BYxjBz//9IXHqtpuqWBBY9aOwQjHAAYXHBZSNBAAOpBYsq3//AAPZBYda+wLJrawB34GBgwLEs9l0QLChILFz9i3+qxWrBYuvC4ILB1ALEtdqy2/4WKgALErPVuwXIrV/7QLIqxyB3+f4EoBYUqUgVVC4N60/ZtWolS4BAAO/qv1r/ZrWoV4iDEqtolSjDAAojBBZRIBABIA=")) \ No newline at end of file diff --git a/apps/tempmonitor/app.png b/apps/tempmonitor/app.png new file mode 100644 index 000000000..f1e576134 Binary files /dev/null and b/apps/tempmonitor/app.png differ diff --git a/apps/tempmonitor/metadata.json b/apps/tempmonitor/metadata.json new file mode 100644 index 000000000..bba8c6095 --- /dev/null +++ b/apps/tempmonitor/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "tempmonitor", + "name": "Temperature monitor", + "version": "0.01", + "description": "Displays the current temperature and stores in a CSV file", + "icon": "app.png", + "tags": "tool", + "supports": ["BANGLEJS", "BANGLEJS2"], + "screenshots": [{"url":"photo_banglejs1.jpg"}], + "allow_emulator": true, + "storage": [ + {"name":"tempmonitor.app.js","url":"tempmonitor.app.js"}, + {"name":"tempmonitor.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/tempmonitor/photo_banglejs1.jpg b/apps/tempmonitor/photo_banglejs1.jpg new file mode 100644 index 000000000..58c64b01c Binary files /dev/null and b/apps/tempmonitor/photo_banglejs1.jpg differ diff --git a/apps/tempmonitor/ss_emul_bjs1.png b/apps/tempmonitor/ss_emul_bjs1.png new file mode 100644 index 000000000..1865ebc85 Binary files /dev/null and b/apps/tempmonitor/ss_emul_bjs1.png differ diff --git a/apps/tempmonitor/ss_emul_bjs2.png b/apps/tempmonitor/ss_emul_bjs2.png new file mode 100644 index 000000000..1cc2140f9 Binary files /dev/null and b/apps/tempmonitor/ss_emul_bjs2.png differ diff --git a/apps/tempmonitor/tempmonitor.app.js b/apps/tempmonitor/tempmonitor.app.js new file mode 100644 index 000000000..36b4bb654 --- /dev/null +++ b/apps/tempmonitor/tempmonitor.app.js @@ -0,0 +1,137 @@ +// Temperature monitor that saves a log of measures +// Version 001 standalone for developer +// PEND +//test with small savefreq +var v_mode_debug=0; //, 0=no, 1 min, 2 prone detail +//var required for drawing with dynamic screen +var rect = Bangle.appRect; +var history = []; +var readFreq=5000; //ms //PEND add to settings +var saveFreq=30000; //ms +var v_saveToFile='Y'; //Y save //N +//with upload file º is not displayed properly +//with upload RAM º is displayed +var v_t_symbol="";//ºC +var v_saved_entries=0; +var filename ="temphistory.csv"; +var lastMeasure = new String(); +var v_model=process.env.BOARD; + +//EMSCRIPTEN,EMSCRIPTEN2 +if (v_model=='BANGLEJS'||v_model=='EMSCRIPTEN') { + v_font_size1=16; + v_font_size2=60; + //g.setColor("#0ff"); //light color + }else{ + v_font_size1=11; + v_font_size2=40; + //g.setColor("#000"); //black or dark + } + +function onTemperature(v_temp) { + if (v_mode_debug>1) console.log("v_temp in "+v_temp); + ClearBox(); + //g.setFont("6x8",2).setFontAlign(0,0); + g.setFontVector(v_font_size1).setFontAlign(0,0); + var x = (rect.x+rect.x2)/2; + var y = (rect.y+rect.y2)/2 + 20; + g.drawString("Records: "+v_saved_entries, x, rect.y+35); + g.drawString("Temperature:", x, rect.y+37+v_font_size1); + //dynamic font (g.getWidth() > 200 ? 60 : 40) + g.setFontVector(v_font_size2).setFontAlign(0,0); + // Avg of temperature readings + while (history.length>4) history.shift(); + history.push(v_temp); + var avrTemp = E.sum(history) / history.length; + //var t = require('locale').temp(avrTemp); + //.replace("'","°"); + lastMeasure=avrTemp.toString(); + if (lastMeasure.length>4) lastMeasure=lastMeasure.substr(0,4); + //DRAW temperature in the center + g.drawString(" ", x-20, y); + g.drawString(v_temp+v_t_symbol, x-20, y); + g.flip(); +} +// from: BJS2 pressure sensor, BJS1 inbuilt thermistor +function drawTemperature() { + if(v_model.substr(0,10)!='EMSCRIPTEN'){ + if (Bangle.getPressure) { + Bangle.getPressure().then(p =>{if (p) onTemperature(p);}); + } else onTemperature(E.getTemperature()); + } + else onTemperature(11);//fake temp for emulators +} + +function saveToFile() { + //input global vars: lastMeasure + var a=new Date(); + var strlastSaveTime=new String(); + strlastSaveTime=a.toISOString(); + //strlastSaveTime=strlastSaveTime.concat(a.getFullYear(),a.getMonth()+1,a.getDate(),a.getHours(),a.getMinutes());; + if (v_mode_debug==1) console.log("saving="+strlastSaveTime+","+lastMeasure); + + if (v_saveToFile=='Y'){ + require("Storage").open(filename,"a").write(strlastSaveTime+","+lastMeasure+"\n"); + //(getTime()+","); + v_saved_entries=v_saved_entries+1; + } +} + +function drawGraph(){ + var img_obj_thermo = { + width : 36, height : 36, bpp : 3, + transparent : 0, + buffer : require("heatshrink").decompress(atob("AEFt2AMKm3bsAMJjdt23ABhEB+/7tgaJ///DRUP//7tuADRP923YDRXbDRfymwaJhu/koaK7eyiwaK3cLDRlWDRY1NKBY1Ztu5kjmJg3cyVI7YMHgdu5Mkyu2fxHkyVJjdgDRFJkmRDRPsDQNbDQ5QBGoONKBJrBoxQIQwO2eRcbtu24AMIFIQLJAH4AMA==")) + }; + g.drawImage(img_obj_thermo,rect.x2-50,rect.y2/2); + g.flip(); +} +function ClearScreen(){ + //avoid widget areas + g.reset(1).clearRect(rect.x, rect.y+24, rect.x2, rect.y2-24); + g.flip(); +} +function ClearBox(){ + //custom boxarea , left space for static graph at right + g.reset(1).clearRect(rect.x, rect.y+24, rect.x2-50, rect.y2-24); + g.flip(); +} +function introPage(){ + //g.setFont("6x8",2).setFontAlign(0,0); + g.setFontVector(v_font_size1).setFontAlign(-1,0); + //x alignment. -1=left (default), 0=center, 1=right + var x=3; + //dynamic positions as height for BJS1 is double than BJS2 + var y = (rect.y+rect.y2)/2 + 10; + g.drawString(" Default values ", x, y - ((v_font_size1*3)+2)); + g.drawString("--------------------", x, y - ((v_font_size1*2)+2)); + g.drawString("Mode debug: "+v_mode_debug, x, y - ((v_font_size1*1)+2)); + g.drawString("Read freq(ms): "+readFreq, x, y ); + g.drawString("Save to file: "+v_saveToFile, x, y+ ((v_font_size1*1)+2) ); + g.drawString("Save freq(ms):"+saveFreq, x, y+((v_font_size1*2)+2) ); + fr=require("Storage").read(filename+"\1");//suffix required + if (fr) g.drawString("Current filesize:"+fr.length.toString()+"kb", x, y+((v_font_size1*3)+2) ); + else g.drawString("File not exist", x, y+((v_font_size1*3)+2)); +} +//MAIN +Bangle.loadWidgets(); +Bangle.setUI({ + mode : "custom", + back : function() {load();} +}); + +ClearScreen(); +introPage(); + +setInterval(function() { + drawTemperature(); +}, readFreq); //ms + +if (v_saveToFile=="Y") { + setInterval(function() { + saveToFile(); + }, saveFreq); //ms +} +setTimeout(ClearScreen, 3500); +setTimeout(drawGraph,4000); +setTimeout(drawTemperature,4500); \ No newline at end of file diff --git a/apps/tempmonitor/tempmonitor.img b/apps/tempmonitor/tempmonitor.img new file mode 100644 index 000000000..275109759 Binary files /dev/null and b/apps/tempmonitor/tempmonitor.img differ diff --git a/apps/tempmonitor/tempmonitor.info b/apps/tempmonitor/tempmonitor.info new file mode 100644 index 000000000..1824c5c86 --- /dev/null +++ b/apps/tempmonitor/tempmonitor.info @@ -0,0 +1 @@ +{"id":"tempmonitor","name":"tempmonitor","src":"tempmonitor.app.js","icon":"tempmonitor.img","version":"0.01","files":"tempmonitor.info,tempmonitor.app.js,tempmonitor.img"} \ No newline at end of file diff --git a/apps/testuserinput/README.md b/apps/testuserinput/README.md index 47c1779be..7e1160bfb 100644 --- a/apps/testuserinput/README.md +++ b/apps/testuserinput/README.md @@ -43,7 +43,7 @@ Colours, font, user input, image, load widgets - Press center area - Prints Touch3 - Swipe Left - Displays Switch OFF image - Swipe Right - Displays Switch ON image - - BTN1 - Prints Button1 + - BTN1 - Prints Button1, Down (moves selection to next row) - BTN2 - Prints Button2 - BTN3 - Quit to Launcher diff --git a/apps/tetris/README.md b/apps/tetris/README.md new file mode 100644 index 000000000..2c41657f4 --- /dev/null +++ b/apps/tetris/README.md @@ -0,0 +1,8 @@ +# Tetris + +Bangle version of the classic game of Tetris. + +## Controls + +Tapping the screen rotates the pieces once, swiping left, right or down moves the +piece in that direction, if possible. \ No newline at end of file diff --git a/apps/tetris/app-icon.js b/apps/tetris/app-icon.js new file mode 100644 index 000000000..b87ef84f4 --- /dev/null +++ b/apps/tetris/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+If4A/AH4A/AH4A/ABe5AA0jABwvYAIovBgABEFAQHFL7IuEL4QuFA45fcF4YuNL7i/FFwoHHL7QvFFxpfaF4wAOF/4nHF5+0AAy3SXYoHGW4QBDF4MAAIgvRFwwHHdAbqDFIQuDL6ouJL4ovDFwpfUAAoHFL4a/FFwhfTFxZfDF4ouFL6QANFopfDF/4vNjwAGF8ABFF4MAAIgvBX4IBDX4YBDL6TyFFIIuEL4QuEL4QuEL6ovDFwpfFF4YuFL6i/FFwhfEX4ouEL6YvFFwpfDF4ouFL6QvGAAwtFL4Yv/AAonHAB4vHG563CAIbuDA5i/CAIb2DA4hfJEwoHPFApZEGwpfLFyJfFFxJfMAAoHNFAa5GX54uTL4YuLL5QAVFowAIF+4A/AH4A/AH4A/AHY")) diff --git a/apps/tetris/metadata.json b/apps/tetris/metadata.json new file mode 100644 index 000000000..f683a5be7 --- /dev/null +++ b/apps/tetris/metadata.json @@ -0,0 +1,14 @@ +{ "id": "tetris", + "name": "Tetris", + "shortName":"Tetris", + "version":"0.01", + "description": "Tetris", + "icon": "tetris.png", + "readme": "README.md", + "tags": "game", + "supports" : ["BANGLEJS2"], + "storage": [ + {"name":"tetris.app.js","url":"tetris.app.js"}, + {"name":"tetris.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/tetris/tetris.app.js b/apps/tetris/tetris.app.js new file mode 100644 index 000000000..e24a731a9 --- /dev/null +++ b/apps/tetris/tetris.app.js @@ -0,0 +1,170 @@ +const block = Graphics.createImage(` +######## +# # # ## +## # ### +# # #### +## ##### +# ###### +######## +######## +`); +const tcols = [ {r:0, g:0, b:1}, {r:0, g:1, b:0}, {r:0, g:1, b:1}, {r:1, g:0, b:0}, {r:1, g:0, b:1}, {r:1, g:1, b:0}, {r:1, g:0.5, b:0.5} ]; +const tiles = [ + [[0, 0, 0, 0], + [0, 0, 0, 0], + [1, 1, 1, 1], + [0, 0, 0, 0]], + [[0, 0, 0], + [0, 1, 0], + [1, 1, 1]], + [[0, 0, 0], + [1, 0, 0], + [1, 1, 1]], + [[0, 0, 0], + [0, 0, 1], + [1, 1, 1]], + [[0, 0, 0], + [1, 1, 0], + [0, 1, 1]], + [[0, 0, 0], + [0, 1, 1], + [1, 1, 0]], + [[1, 1], + [1, 1]] +]; + +const ox = 176/2 - 5*8; +const oy = 8; + +var pf = Array(23).fill().map(()=>Array(12).fill(0)); // field is really 10x20, but adding a border for collision checks +pf[20].fill(1); +pf[21].fill(1); +pf[22].fill(1); +pf.forEach((x,i) => { pf[i][0] = 1; pf[i][11] = 1; }); + +function rotateTile(t, r) { + var nt = JSON.parse(JSON.stringify(t)); + if (t.length==2) return nt; + var s = t.length; + for (m=0; m0) + if (qClear) g.fillRect(x+8*i, y+8*j, x+8*(i+1)-1, y+8*(j+1)-1); + else g.drawImage(block, x+8*i, y+8*j); +} + +function showNext(n, r) { + var nt = rotateTile(tiles[n], r); + g.setColor(0).fillRect(176-33, 40, 176-33+33, 82); + drawTile(nt, ntn, 176-33, 40); +} + +var time = Date.now(); +var px=4, py=0; +var ctn = Math.floor(Math.random()*7); // current tile number +var ntn = Math.floor(Math.random()*7); // next tile number +var ntr = Math.floor(Math.random()*4); // next tile rotation +var ct = rotateTile(tiles[ctn], Math.floor(Math.random()*4)); // current tile (rotated) +var dropInterval = 450; +var nlines = 0; + +function redrawPF(ly) { + for (y=0; y<=ly; ++y) + for (x=1; x<11; ++x) { + c = pf[y][x]; + if (c>0) g.setColor(tcols[c-1].r, tcols[c-1].g, tcols[c-1].b).drawImage(block, ox+(x-1)*8, oy+y*8); + else g.setColor(0, 0, 0).fillRect(ox+(x-1)*8, oy+y*8, ox+x*8-1, oy+(y+1)*8-1); + } +} + +function insertAndCheck() { + for (y=0; y0) pf[py+y][px+x+1] = ctn+1; + // check for full lines + for (y=19; y>0; y--) { + var qFull = true; + for (x=1; x<11; ++x) qFull &= pf[y][x]>0; + if (qFull) { + nlines++; + dropInterval -= 5; + Bangle.buzz(30); + for (ny=y; ny>0; ny--) pf[ny] = JSON.parse(JSON.stringify(pf[ny-1])); + redrawPF(y); + g.setColor(0).fillRect(5, 30, 41, 80).setColor(1, 1, 1).drawString(nlines.toString(), 22, 50); + } + } + // spawn new tile + px = 4; py = 0; + ctn = ntn; + ntn = Math.floor(Math.random()*7); + ct = rotateTile(tiles[ctn], ntr); + ntr = Math.floor(Math.random()*4); + showNext(ntn, ntr); +} + +function moveOk(t, dx, dy) { + var ok = true; + for (y=0; y 0) ok = false; + return ok; +} + +function gameStep() { + if (Date.now()-time > dropInterval) { // drop one step + time = Date.now(); + if (moveOk(ct, 0, 1)) { + drawTile(ct, ctn, ox+px*8, oy+py*8, true); + py++; + } + else { // reached the bottom + insertAndCheck(ct, ctn, px, py); + } + drawTile(ct, ctn, ox+px*8, oy+py*8, false); + } +} + +Bangle.setUI(); +Bangle.on("touch", (e) => { + t = rotateTile(ct, 3); + if (moveOk(t, 0, 0)) { + drawTile(ct, ctn, ox+px*8, oy+py*8, true); + ct = t; + drawTile(ct, ctn, ox+px*8, oy+py*8, false); + } +}); + +Bangle.on("swipe", (x,y) => { + if (y<0) y = 0; + if (moveOk(ct, x, y)) { + drawTile(ct, ctn, ox+px*8, oy+py*8, true); + px += x; + py += y; + drawTile(ct, ctn, ox+px*8, oy+py*8, false); + } +}); + +drawBoundingBox(); +g.setColor(1, 1, 1).setFontAlign(0, 1, 0).setFont("6x15", 1).drawString("Lines", 22, 30).drawString("Next", 176-22, 30); +showNext(ntn, ntr); +g.setColor(0).fillRect(5, 30, 41, 80).setColor(1, 1, 1).drawString(nlines.toString(), 22, 50); +var gi = setInterval(gameStep, 20); diff --git a/apps/tetris/tetris.png b/apps/tetris/tetris.png new file mode 100644 index 000000000..8e884eaf3 Binary files /dev/null and b/apps/tetris/tetris.png differ diff --git a/apps/timerclk/ChangeLog b/apps/timerclk/ChangeLog index 7a357b1aa..5a954d58c 100644 --- a/apps/timerclk/ChangeLog +++ b/apps/timerclk/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Add sunrise/sunset. Fix timer bugs. 0.03: Use default Bangle formatter for booleans +0.04: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps diff --git a/apps/timerclk/app.js b/apps/timerclk/app.js index c750fcfde..ee30b059a 100644 --- a/apps/timerclk/app.js +++ b/apps/timerclk/app.js @@ -3,7 +3,7 @@ Graphics.prototype.setFontAnton = function(scale) { g.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgAAAAAAAAAAA/gAAAAAAAAAAP/gAAAAAAAAAH//gAAAAAAAAB///gAAAAAAAAf///gAAAAAAAP////gAAAAAAD/////gAAAAAA//////gAAAAAP//////gAAAAH///////gAAAB////////gAAAf////////gAAP/////////gAD//////////AA//////////gAA/////////4AAA////////+AAAA////////gAAAA///////wAAAAA//////8AAAAAA//////AAAAAAA/////gAAAAAAA////4AAAAAAAA///+AAAAAAAAA///gAAAAAAAAA//wAAAAAAAAAA/8AAAAAAAAAAA/AAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////AAAAAB///////8AAAAH////////AAAAf////////wAAA/////////4AAB/////////8AAD/////////+AAH//////////AAP//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wA//8AAAAAB//4A//wAAAAAAf/4A//gAAAAAAP/4A//gAAAAAAP/4A//gAAAAAAP/4A//wAAAAAAf/4A///////////4Af//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH//////////AAD/////////+AAB/////////8AAA/////////4AAAP////////gAAAD///////+AAAAAf//////4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/gAAAAAAAAAAP/gAAAAAAAAAAf/gAAAAAAAAAAf/gAAAAAAAAAAf/AAAAAAAAAAA//AAAAAAAAAAA/+AAAAAAAAAAB/8AAAAAAAAAAD//////////gAH//////////gAP//////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/4AAAAB/gAAD//4AAAAf/gAAP//4AAAB//gAA///4AAAH//gAB///4AAAf//gAD///4AAA///gAH///4AAD///gAP///4AAH///gAP///4AAP///gAf///4AAf///gAf///4AB////gAf///4AD////gA////4AH////gA////4Af////gA////4A/////gA//wAAB/////gA//gAAH/////gA//gAAP/////gA//gAA///8//gA//gAD///w//gA//wA////g//gA////////A//gA///////8A//gA///////4A//gAf//////wA//gAf//////gA//gAf/////+AA//gAP/////8AA//gAP/////4AA//gAH/////gAA//gAD/////AAA//gAB////8AAA//gAA////wAAA//gAAP///AAAA//gAAD//8AAAA//gAAAP+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB/+AAAAAD/wAAB//8AAAAP/wAAB///AAAA//wAAB///wAAB//wAAB///4AAD//wAAB///8AAH//wAAB///+AAP//wAAB///+AAP//wAAB////AAf//wAAB////AAf//wAAB////gAf//wAAB////gA///wAAB////gA///wAAB////gA///w//AAf//wA//4A//AAA//wA//gA//AAAf/wA//gB//gAAf/wA//gB//gAAf/wA//gD//wAA//wA//wH//8AB//wA///////////gA///////////gA///////////gA///////////gAf//////////AAf//////////AAP//////////AAP/////////+AAH/////////8AAH///+/////4AAD///+f////wAAA///8P////gAAAf//4H///+AAAAH//gB///wAAAAAP4AAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/wAAAAAAAAAA//wAAAAAAAAAP//wAAAAAAAAB///wAAAAAAAAf///wAAAAAAAH////wAAAAAAA/////wAAAAAAP/////wAAAAAB//////wAAAAAf//////wAAAAH///////wAAAA////////wAAAP////////wAAA///////H/wAAA//////wH/wAAA/////8AH/wAAA/////AAH/wAAA////gAAH/wAAA///4AAAH/wAAA//+AAAAH/wAAA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gA///////////gAAAAAAAAH/4AAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAH/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB//8AAA/////+B///AAA/////+B///wAA/////+B///4AA/////+B///8AA/////+B///8AA/////+B///+AA/////+B////AA/////+B////AA/////+B////AA/////+B////gA/////+B////gA/////+B////gA/////+A////gA//gP/gAAB//wA//gf/AAAA//wA//gf/AAAAf/wA//g//AAAAf/wA//g//AAAA//wA//g//gAAA//wA//g//+AAP//wA//g////////gA//g////////gA//g////////gA//g////////gA//g////////AA//gf///////AA//gf//////+AA//gP//////+AA//gH//////8AA//gD//////4AA//gB//////wAA//gA//////AAAAAAAH////8AAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//////gAAAAB///////+AAAAH////////gAAAf////////4AAB/////////8AAD/////////+AAH//////////AAH//////////gAP//////////gAP//////////gAf//////////wAf//////////wAf//////////wAf//////////wAf//////////4A//wAD/4AAf/4A//gAH/wAAP/4A//gAH/wAAP/4A//gAP/wAAP/4A//gAP/4AAf/4A//wAP/+AD//4A///wP//////4Af//4P//////wAf//4P//////wAf//4P//////wAf//4P//////wAP//4P//////gAP//4H//////gAH//4H//////AAH//4D/////+AAD//4D/////8AAB//4B/////4AAA//4A/////wAAAP/4AP////AAAAB/4AD///4AAAAAAAAAH/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//AAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAAAAA//gAAAAAAADgA//gAAAAAAP/gA//gAAAAAH//gA//gAAAAB///gA//gAAAAP///gA//gAAAD////gA//gAAAf////gA//gAAB/////gA//gAAP/////gA//gAB//////gA//gAH//////gA//gA///////gA//gD///////gA//gf///////gA//h////////gA//n////////gA//////////gAA/////////AAAA////////wAAAA///////4AAAAA///////AAAAAA//////4AAAAAA//////AAAAAAA/////4AAAAAAA/////AAAAAAAA////8AAAAAAAA////gAAAAAAAA///+AAAAAAAAA///4AAAAAAAAA///AAAAAAAAAA//4AAAAAAAAAA/+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//gB///wAAAAP//4H///+AAAA///8P////gAAB///+f////4AAD///+/////8AAH/////////+AAH//////////AAP//////////gAP//////////gAf//////////gAf//////////wAf//////////wAf//////////wA///////////wA//4D//wAB//4A//wB//gAA//4A//gA//gAAf/4A//gA//AAAf/4A//gA//gAAf/4A//wB//gAA//4A///P//8AH//4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////gAP//////////gAP//////////AAH//////////AAD/////////+AAD///+/////8AAB///8f////wAAAf//4P////AAAAH//wD///8AAAAA/+AAf//AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH//gAAAAAAAAB///+AA/+AAAAP////gA//wAAAf////wA//4AAB/////4A//8AAD/////8A//+AAD/////+A///AAH/////+A///AAP//////A///gAP//////A///gAf//////A///wAf//////A///wAf//////A///wAf//////A///wA///////AB//4A//4AD//AAP/4A//gAB//AAP/4A//gAA//AAP/4A//gAA/+AAP/4A//gAB/8AAP/4A//wAB/8AAf/4Af//////////wAf//////////wAf//////////wAf//////////wAf//////////wAP//////////gAP//////////gAH//////////AAH/////////+AAD/////////8AAB/////////4AAAf////////wAAAP////////AAAAB///////4AAAAAD/////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf/AAB/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAA//AAD/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="), 46, atob("EiAnGicnJycnJycnEw=="), 78+(scale<<8)+(1<<16)); }; -var SunCalc = require("https://raw.githubusercontent.com/mourner/suncalc/master/suncalc.js"); +var SunCalc = require("suncalc"); // from modules folder const LOCATION_FILE = "mylocation.json"; let location; var sunRise = "--:--"; @@ -72,7 +72,7 @@ function drawSpecial() { g.setFontAlign(0,0).setFont(settings.specialFont, settings.specialFontSize); var y = Bangle.appRect.y + g.stringMetrics("00:00").height/2; g.clearRect(Bangle.appRect.x, Bangle.appRect.y, Bangle.appRect.x2, Bangle.appRect.y+g.stringMetrics("00:00").height); - + if (stopwatches.length) { time = timerclk.getTime(stopwatches[stopwatch]); g.drawString(timerclk.formatTime(time, true), x, y); @@ -111,7 +111,7 @@ function draw() { var dateStr = require("locale").date(date,settings.shortDate).toUpperCase(); var dowStr = require("locale").dow(date).toUpperCase(); var srssStr = sunRise + sunIcons + sunSet; - + // draw time if (settings.timeFont == "Anton") { g.setFontAlign(0,0).setFont("Anton"); diff --git a/apps/timerclk/metadata.json b/apps/timerclk/metadata.json index 899eda59d..5bd6bee24 100644 --- a/apps/timerclk/metadata.json +++ b/apps/timerclk/metadata.json @@ -1,8 +1,8 @@ -{ +{ "id": "timerclk", "name": "Timer Clock", "shortName":"Timer Clock", - "version":"0.03", + "version":"0.04", "description": "A clock with stopwatches, timers and alarms build in.", "icon": "app-icon.png", "type": "clock", diff --git a/apps/torch/ChangeLog b/apps/torch/ChangeLog index fa69d5d04..35d3f5927 100644 --- a/apps/torch/ChangeLog +++ b/apps/torch/ChangeLog @@ -8,3 +8,4 @@ 0.08: Force background of widget field to the torch colour 0.09: Change code taking FW tweaks into account 0.10: Introduce fast switching. +0.11: Make compatible with Fastload Utils by loading and hiding widgets. diff --git a/apps/torch/app.js b/apps/torch/app.js index 5d288579c..b44cfb929 100644 --- a/apps/torch/app.js +++ b/apps/torch/app.js @@ -1,14 +1,16 @@ { const SETTINGS_FILE = "torch.json"; let settings; + let s = require("Storage"); + let wu = require("widget_utils"); let loadSettings = function() { - settings = require("Storage").readJSON(SETTINGS_FILE,1)|| {'bg': '#FFFFFF', 'color': 'White'}; + settings = s.readJSON(SETTINGS_FILE,1)|| {'bg': '#FFFFFF', 'color': 'White'}; }; loadSettings(); - let brightnessBackup = require("Storage").readJSON('setting.json').brightness; + let brightnessBackup = s.readJSON('setting.json').brightness; let optionsBackup = Bangle.getOptions(); Bangle.setLCDBrightness(1); Bangle.setLCDPower(1); @@ -18,6 +20,8 @@ g.setTheme({bg:settings.bg,fg:"#000"}); g.setColor(settings.bg); g.fillRect(0,0,g.getWidth(),g.getHeight()); + Bangle.loadWidgets(); + wu.hide(); Bangle.setUI({ mode : 'custom', back : Bangle.showClock, // B2: SW back button to exit @@ -26,6 +30,7 @@ Bangle.setLCDBrightness(brightnessBackup); Bangle.setOptions(optionsBackup); g.setTheme(themeBackup); + wu.show(); } }); } diff --git a/apps/torch/metadata.json b/apps/torch/metadata.json index 48399de96..4e8794663 100644 --- a/apps/torch/metadata.json +++ b/apps/torch/metadata.json @@ -2,7 +2,7 @@ "id": "torch", "name": "Torch", "shortName": "Torch", - "version": "0.10", + "version": "0.11", "description": "Turns screen white to help you see in the dark. Select from the launcher or press BTN1,BTN3,BTN1,BTN3 quickly to start when in any app that shows widgets on Bangle.js 1. You can also set the color through the app's setting menu.", "icon": "app.png", "tags": "tool,torch", diff --git a/apps/weather/ChangeLog b/apps/weather/ChangeLog index da28d8d5a..4b70d3531 100644 --- a/apps/weather/ChangeLog +++ b/apps/weather/ChangeLog @@ -13,4 +13,10 @@ 0.14: Use weather condition code for icon selection 0.15: Fix widget icon 0.16: Don't mark app as clock -0.17: Added clkinfo for clocks. \ No newline at end of file +0.17: Added clkinfo for clocks. +0.18: Added hasRange to clkinfo. +0.19: Added weather condition to clkinfo. +0.20: Added weather condition with temperature to clkinfo. +0.21: Updated clkinfo icon. +0.22: Automatic translation of strings, some left untranslated. +0.23: Update clock_info to avoid a redraw diff --git a/apps/weather/app.js b/apps/weather/app.js index f63b226b9..8988c5002 100644 --- a/apps/weather/app.js +++ b/apps/weather/app.js @@ -16,10 +16,10 @@ var layout = new Layout({type:"v", bgCol: g.theme.bg, c: [ {type: "txt", font: "12%", valign: -1, id: "tempUnit", label: "°C"}, ]}, {filly: 1}, - {type: "txt", font: "6x8", pad: 2, halign: 1, label: "Humidity"}, + {type: "txt", font: "6x8", pad: 2, halign: 1, label: /*LANG*/"Humidity"}, {type: "txt", font: "9%", pad: 2, halign: 1, id: "hum", label: "000%"}, {filly: 1}, - {type: "txt", font: "6x8", pad: 2, halign: -1, label: "Wind"}, + {type: "txt", font: "6x8", pad: 2, halign: -1, label: /*LANG*/"Wind"}, {type: "h", halign: -1, c: [ {type: "txt", font: "9%", pad: 2, id: "wind", label: "00"}, {type: "txt", font: "6x8", pad: 2, valign: -1, id: "windUnit", label: "km/h"}, @@ -27,22 +27,22 @@ var layout = new Layout({type:"v", bgCol: g.theme.bg, c: [ ]}, ]}, {filly: 1}, - {type: "txt", font: "9%", wrap: true, height: g.getHeight()*0.18, fillx: 1, id: "cond", label: "Weather condition"}, + {type: "txt", font: "9%", wrap: true, height: g.getHeight()*0.18, fillx: 1, id: "cond", label: /*LANG*/"Weather condition"}, {filly: 1}, {type: "h", c: [ {type: "txt", font: "6x8", pad: 4, id: "loc", label: "Toronto"}, {fillx: 1}, - {type: "txt", font: "6x8", pad: 4, id: "updateTime", label: "15 minutes ago"}, + {type: "txt", font: "6x8", pad: 4, id: "updateTime", label: /*LANG*/"15 minutes ago"}, ]}, {filly: 1}, ]}, {lazy: true}); function formatDuration(millis) { let pluralize = (n, w) => n + " " + w + (n == 1 ? "" : "s"); - if (millis < 60000) return "< 1 minute"; - if (millis < 3600000) return pluralize(Math.floor(millis/60000), "minute"); - if (millis < 86400000) return pluralize(Math.floor(millis/3600000), "hour"); - return pluralize(Math.floor(millis/86400000), "day"); + if (millis < 60000) return /*LANG*/"< 1 minute"; + if (millis < 3600000) return pluralize(Math.floor(millis/60000), /*LANG*/"minute"); + if (millis < 86400000) return pluralize(Math.floor(millis/3600000), /*LANG*/"hour"); + return pluralize(Math.floor(millis/86400000), /*LANG*/"day"); } function draw() { @@ -57,7 +57,7 @@ function draw() { layout.windUnit.label = wind[2] + " " + (current.wrose||'').toUpperCase(); layout.cond.label = current.txt.charAt(0).toUpperCase()+(current.txt||'').slice(1); layout.loc.label = current.loc; - layout.updateTime.label = `${formatDuration(Date.now() - current.time)} ago`; + layout.updateTime.label = `${formatDuration(Date.now() - current.time)} ago`; // How to autotranslate this and similar? layout.update(); layout.render(); } @@ -77,9 +77,9 @@ function update() { } else { layout.forgetLazyState(); if (NRF.getSecurityStatus().connected) { - E.showMessage("Weather\nunknown\n\nIs Gadgetbridge\nweather\nreporting set\nup on your\nphone?"); + E.showMessage(/*LANG*/"Weather\nunknown\n\nIs Gadgetbridge\nweather\nreporting set\nup on your\nphone?"); } else { - E.showMessage("Weather\nunknown\n\nGadgetbridge\nnot connected"); + E.showMessage(/*LANG*/"Weather\nunknown\n\nGadgetbridge\nnot connected"); NRF.on("connect", update); } } diff --git a/apps/weather/clkinfo.js b/apps/weather/clkinfo.js index caaebf273..f40924e06 100644 --- a/apps/weather/clkinfo.js +++ b/apps/weather/clkinfo.js @@ -3,37 +3,69 @@ temp: "?", hum: "?", wind: "?", + txt: "?", }; - var weatherJson = storage.readJSON('weather.json'); + var weatherJson = require("Storage").readJSON('weather.json'); if(weatherJson !== undefined && weatherJson.weather !== undefined){ weather = weatherJson.weather; - weather.temp = locale.temp(weather.temp-273.15); + weather.temp = require("locale").temp(weather.temp-273.15); weather.hum = weather.hum + "%"; - weather.wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); + weather.wind = require("locale").speed(weather.wind).match(/^(\D*\d*)(.*)$/); weather.wind = Math.round(weather.wind[1]) + "kph"; } + function weatherIcon(code) { + var ovr = Graphics.createArrayBuffer(24,24,1,{msb:true}); + require("weather").drawIcon({code:code},12,12,12,ovr); + var img = ovr.asImage(); + img.transparent = 0; + //for (var i=0;i ({ text: weather.temp, img: weatherIcon(weather.code), + v: parseInt(weather.temp), min: -30, max: 55}), + show: function() {}, + hide: function () {} + }, + { + name: "condition", + get: () => ({ text: weather.txt, img: weatherIcon(weather.code), + v: weather.code}), + show: function() {}, + hide: function () {} + }, { name: "temperature", - get: () => ({ text: weather.temp, img: atob("GBiBAAA8AAB+AADnAADDAADDAADDAADDAADDAADbAADbAADbAADbAADbAADbAAHbgAGZgAM8wAN+wAN+wAM8wAGZgAHDgAD/AAA8AA==")}), - show: function() { weatherItems.items[0].emit("redraw"); }, + hasRange : true, + get: () => ({ text: weather.temp, img: atob("GBiBAAA8AAB+AADnAADDAADDAADDAADDAADDAADbAADbAADbAADbAADbAADbAAHbgAGZgAM8wAN+wAN+wAM8wAGZgAHDgAD/AAA8AA=="), + v: parseInt(weather.temp), min: -30, max: 55}), + show: function() {}, hide: function () {} }, { name: "humidity", - get: () => ({ text: weather.hum, img: atob("GBiBAAAEAAAMAAAOAAAfAAAfAAA/gAA/gAI/gAY/AAcfAA+AQA+A4B/A4D/B8D/h+D/j+H/n/D/n/D/n/B/H/A+H/AAH/AAD+AAA8A==")}), - show: function() { weatherItems.items[1].emit("redraw"); }, + hasRange : true, + get: () => ({ text: weather.hum, img: atob("GBiBAAAEAAAMAAAOAAAfAAAfAAA/gAA/gAI/gAY/AAcfAA+AQA+A4B/A4D/B8D/h+D/j+H/n/D/n/D/n/B/H/A+H/AAH/AAD+AAA8A=="), + v: parseInt(weather.hum), min: 0, max: 100}), + show: function() {}, hide: function () {} }, { name: "wind", - get: () => ({ text: weather.wind, img: atob("GBiBAAHgAAPwAAYYAAwYAAwMfAAY/gAZh3/xg//hgwAAAwAABg///g//+AAAAAAAAP//wH//4AAAMAAAMAAYMAAYMAAMcAAP4AADwA==")}), - show: function() { weatherItems.items[2].emit("redraw"); }, + hasRange : true, + get: () => ({ text: weather.wind, img: atob("GBiBAAHgAAPwAAYYAAwYAAwMfAAY/gAZh3/xg//hgwAAAwAABg///g//+AAAAAAAAP//wH//4AAAMAAAMAAYMAAYMAAMcAAP4AADwA=="), + v: parseInt(weather.wind), min: 0, max: 118}), + show: function() {}, hide: function () {} }, ] diff --git a/apps/weather/lib.js b/apps/weather/lib.js index 1d48116e1..8c59fd3e3 100644 --- a/apps/weather/lib.js +++ b/apps/weather/lib.js @@ -62,12 +62,29 @@ scheduleExpiry(storage.readJSON('weather.json')||{}); * @param x Left * @param y Top * @param r Icon Size + * @param ovr Graphics instance (or undefined for g) */ -exports.drawIcon = function(cond, x, y, r) { +exports.drawIcon = function(cond, x, y, r, ovr) { var palette; - + var monochrome=1; + if(!ovr) { + ovr = g; + monochrome=0; + } + if(monochrome) { + palette = { + sun: '#FFF', + cloud: '#FFF', + bgCloud: '#FFF', + rain: '#FFF', + lightning: '#FFF', + snow: '#FFF', + mist: '#FFF', + background: '#000' + }; + } else if (B2) { - if (g.theme.dark) { + if (ovr.theme.dark) { palette = { sun: '#FF0', cloud: '#FFF', @@ -89,7 +106,7 @@ exports.drawIcon = function(cond, x, y, r) { }; } } else { - if (g.theme.dark) { + if (ovr.theme.dark) { palette = { sun: '#FE0', cloud: '#BBB', @@ -113,19 +130,19 @@ exports.drawIcon = function(cond, x, y, r) { } function drawSun(x, y, r) { - g.setColor(palette.sun); - g.fillCircle(x, y, r); + ovr.setColor(palette.sun); + ovr.fillCircle(x, y, r); } function drawCloud(x, y, r, c) { const u = r/12; if (c==null) c = palette.cloud; - g.setColor(c); - g.fillCircle(x-8*u, y+3*u, 4*u); - g.fillCircle(x-4*u, y-2*u, 5*u); - g.fillCircle(x+4*u, y+0*u, 4*u); - g.fillCircle(x+9*u, y+4*u, 3*u); - g.fillPoly([ + ovr.setColor(c); + ovr.fillCircle(x-8*u, y+3*u, 4*u); + ovr.fillCircle(x-4*u, y-2*u, 5*u); + ovr.fillCircle(x+4*u, y+0*u, 4*u); + ovr.fillCircle(x+9*u, y+4*u, 3*u); + ovr.fillPoly([ x-8*u, y+7*u, x-8*u, y+3*u, x-4*u, y-2*u, @@ -137,19 +154,23 @@ exports.drawIcon = function(cond, x, y, r) { function drawBrokenClouds(x, y, r) { drawCloud(x+1/8*r, y-1/8*r, 7/8*r, palette.bgCloud); + if(monochrome) + drawCloud(x-1/8*r, y+2/16*r, r, palette.background); drawCloud(x-1/8*r, y+1/8*r, 7/8*r); } function drawFewClouds(x, y, r) { drawSun(x+3/8*r, y-1/8*r, 5/8*r); + if(monochrome) + drawCloud(x-1/8*r, y+2/16*r, r, palette.background); drawCloud(x-1/8*r, y+1/8*r, 7/8*r); } function drawRainLines(x, y, r) { - g.setColor(palette.rain); + ovr.setColor(palette.rain); const y1 = y+1/2*r; const y2 = y+1*r; - const poly = g.fillPolyAA ? p => g.fillPolyAA(p) : p => g.fillPoly(p); + const poly = ovr.fillPolyAA ? p => ovr.fillPolyAA(p) : p => ovr.fillPoly(p); poly([ x-6/12*r, y1, x-8/12*r, y2, @@ -182,8 +203,8 @@ exports.drawIcon = function(cond, x, y, r) { function drawThunderstorm(x, y, r) { function drawLightning(x, y, r) { - g.setColor(palette.lightning); - g.fillPoly([ + ovr.setColor(palette.lightning); + ovr.fillPoly([ x-2/6*r, y-r, x-4/6*r, y+1/6*r, x-1/6*r, y+1/6*r, @@ -194,8 +215,9 @@ exports.drawIcon = function(cond, x, y, r) { ]); } - drawBrokenClouds(x, y-1/3*r, r); + if(monochrome) drawBrokenClouds(x, y-1/3*r, r); drawLightning(x-1/12*r, y+1/2*r, 1/2*r); + drawBrokenClouds(x, y-1/3*r, r); } function drawSnow(x, y, r) { @@ -210,7 +232,7 @@ exports.drawIcon = function(cond, x, y, r) { } } - g.setColor(palette.snow); + ovr.setColor(palette.snow); const w = 1/12*r; for(let i = 0; i<=6; ++i) { const points = [ @@ -220,7 +242,7 @@ exports.drawIcon = function(cond, x, y, r) { x+w, y+r, ]; rotatePoints(points, x, y, i/3*Math.PI); - g.fillPoly(points); + ovr.fillPoly(points); for(let j = -1; j<=1; j += 2) { const points = [ @@ -231,7 +253,7 @@ exports.drawIcon = function(cond, x, y, r) { ]; rotatePoints(points, x, y+7/12*r, j/3*Math.PI); rotatePoints(points, x, y, i/3*Math.PI); - g.fillPoly(points); + ovr.fillPoly(points); } } } @@ -245,18 +267,18 @@ exports.drawIcon = function(cond, x, y, r) { [-0.2, 0.3], ]; - g.setColor(palette.mist); + ovr.setColor(palette.mist); for(let i = 0; i<5; ++i) { - g.fillRect(x+layers[i][0]*r, y+(0.4*i-0.9)*r, x+layers[i][1]*r, + ovr.fillRect(x+layers[i][0]*r, y+(0.4*i-0.9)*r, x+layers[i][1]*r, y+(0.4*i-0.7)*r-1); - g.fillCircle(x+layers[i][0]*r, y+(0.4*i-0.8)*r-0.5, 0.1*r-0.5); - g.fillCircle(x+layers[i][1]*r, y+(0.4*i-0.8)*r-0.5, 0.1*r-0.5); + ovr.fillCircle(x+layers[i][0]*r, y+(0.4*i-0.8)*r-0.5, 0.1*r-0.5); + ovr.fillCircle(x+layers[i][1]*r, y+(0.4*i-0.8)*r-0.5, 0.1*r-0.5); } } function drawUnknown(x, y, r) { drawCloud(x, y, r, palette.bgCloud); - g.setColor(g.theme.fg).setFontAlign(0, 0).setFont('Vector', r*2).drawString("?", x+r/10, y+r/6); + ovr.setColor(ovr.theme.fg).setFontAlign(0, 0).setFont('Vector', r*2).drawString("?", x+r/10, y+r/6); } /* diff --git a/apps/weather/metadata.json b/apps/weather/metadata.json index e28a282d6..77ca37721 100644 --- a/apps/weather/metadata.json +++ b/apps/weather/metadata.json @@ -1,7 +1,7 @@ { "id": "weather", "name": "Weather", - "version": "0.17", + "version": "0.23", "description": "Show Gadgetbridge weather report", "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/widalarm/ChangeLog b/apps/widalarm/ChangeLog new file mode 100644 index 000000000..63568a9bd --- /dev/null +++ b/apps/widalarm/ChangeLog @@ -0,0 +1 @@ +0.01: Moved out of 'alarm' app diff --git a/apps/widalarm/app.png b/apps/widalarm/app.png new file mode 100644 index 000000000..a859dd2ef Binary files /dev/null and b/apps/widalarm/app.png differ diff --git a/apps/widalarm/metadata.json b/apps/widalarm/metadata.json new file mode 100644 index 000000000..b91457138 --- /dev/null +++ b/apps/widalarm/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "widalarm", + "name": "Alarms Widget", + "version": "0.01", + "description": "Displays an alarm icon in the widgets bar if any alarm is active", + "icon": "app.png", + "type": "widget", + "tags": "tool,alarm,widget", + "supports": [ "BANGLEJS", "BANGLEJS2" ], + "provides_widgets" : ["alarm"], + "default" : true, + "storage": [ + { "name": "widalarm.wid.js", "url": "widget.js" } + ] +} diff --git a/apps/alarm/widget.js b/apps/widalarm/widget.js similarity index 100% rename from apps/alarm/widget.js rename to apps/widalarm/widget.js diff --git a/apps/widalarmeta/metadata.json b/apps/widalarmeta/metadata.json index b6d8bd62b..ef9f55ba8 100644 --- a/apps/widalarmeta/metadata.json +++ b/apps/widalarmeta/metadata.json @@ -8,6 +8,7 @@ "type": "widget", "tags": "widget", "supports": ["BANGLEJS","BANGLEJS2"], + "provides_widgets" : ["alarm"], "screenshots" : [ { "url":"screenshot.png" } ], "storage": [ {"name":"widalarmeta.wid.js","url":"widget.js"} diff --git a/apps/widbat/metadata.json b/apps/widbat/metadata.json index 0f040396f..993310eb2 100644 --- a/apps/widbat/metadata.json +++ b/apps/widbat/metadata.json @@ -6,6 +6,8 @@ "icon": "widget.png", "type": "widget", "tags": "widget,battery", + "provides_widgets" : ["battery"], + "default" : true, "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"widbat.wid.js","url":"widget.js"} diff --git a/apps/widbata/metadata.json b/apps/widbata/metadata.json index 26968a7d7..ddc901e80 100644 --- a/apps/widbata/metadata.json +++ b/apps/widbata/metadata.json @@ -10,6 +10,7 @@ "readme": "README.md", "description": "Shows the current battery level status in the top right using the clocks colour theme", "tags": "widget,battery", + "provides_widgets" : ["battery"], "storage": [ {"name":"widbata.wid.js","url":"widbata.wid.js"} ] diff --git a/apps/widbatpc/metadata.json b/apps/widbatpc/metadata.json index 7da4e3e0c..953f8d345 100644 --- a/apps/widbatpc/metadata.json +++ b/apps/widbatpc/metadata.json @@ -7,6 +7,7 @@ "icon": "widget.png", "type": "widget", "tags": "widget,battery", + "provides_widgets" : ["battery"], "supports": ["BANGLEJS","BANGLEJS2"], "readme": "README.md", "screenshots": [{"url":"widbatpc.full.jpg"},{"url":"widbatpc.part.jpg"}], diff --git a/apps/widbatv/metadata.json b/apps/widbatv/metadata.json index 37cf6197b..d4ef28315 100644 --- a/apps/widbatv/metadata.json +++ b/apps/widbatv/metadata.json @@ -6,6 +6,7 @@ "icon": "widget.png", "type": "widget", "tags": "widget,battery", + "provides_widgets" : ["battery"], "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"widbatv.wid.js","url":"widget.js"} diff --git a/apps/widbt/metadata.json b/apps/widbt/metadata.json index e2d5082a5..1623db7a1 100644 --- a/apps/widbt/metadata.json +++ b/apps/widbt/metadata.json @@ -6,6 +6,8 @@ "icon": "widget.png", "type": "widget", "tags": "widget,bluetooth", + "provides_widgets" : ["bluetooth"], + "default" : true, "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"widbt.wid.js","url":"widget.js"} diff --git a/apps/widbt_notify/metadata.json b/apps/widbt_notify/metadata.json index 36905a340..5e3f15af2 100644 --- a/apps/widbt_notify/metadata.json +++ b/apps/widbt_notify/metadata.json @@ -6,6 +6,7 @@ "icon": "widget.png", "type": "widget", "tags": "widget,bluetooth", + "provides_widgets" : ["bluetooth"], "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"widbt_notify.wid.js","url":"widget.js"}, @@ -13,5 +14,5 @@ ], "data": [ {"name":"widbt_notify.json"} - ] + ] } diff --git a/apps/widbthide/metadata.json b/apps/widbthide/metadata.json index 59b13adb4..e3ac5cd54 100644 --- a/apps/widbthide/metadata.json +++ b/apps/widbthide/metadata.json @@ -6,6 +6,7 @@ "icon": "widget.png", "type": "widget", "tags": "widget,bluetooth", + "provides_widgets" : ["bluetooth"], "supports": ["BANGLEJS","BANGLEJS2"], "storage": [ {"name":"widbthide.wid.js","url":"widget.js"} diff --git a/apps/widclkbttm/ChangeLog b/apps/widclkbttm/ChangeLog index 9dc8f8d2c..373337378 100644 --- a/apps/widclkbttm/ChangeLog +++ b/apps/widclkbttm/ChangeLog @@ -2,3 +2,5 @@ 0.02: Modification for bottom widget area and text color 0.03: based in widclk v0.05 compatible at same time, bottom area and color 0.04: refactored to use less memory, and allow turning on/off when quick-switching apps +0.05: Remove cyan color, use theme foreground instead + diff --git a/apps/widclkbttm/metadata.json b/apps/widclkbttm/metadata.json index 7c5fe4b63..2bcd6bc58 100644 --- a/apps/widclkbttm/metadata.json +++ b/apps/widclkbttm/metadata.json @@ -2,7 +2,7 @@ "id": "widclkbttm", "name": "Digital clock (Bottom) widget", "shortName": "Digital clock Bottom Widget", - "version": "0.04", + "version": "0.05", "description": "Displays time in the bottom of the screen (may not be compatible with some apps)", "icon": "widclkbttm.png", "type": "widget", diff --git a/apps/widclkbttm/widclkbttm.wid.js b/apps/widclkbttm/widclkbttm.wid.js index c5e85318c..2278b5380 100644 --- a/apps/widclkbttm/widclkbttm.wid.js +++ b/apps/widclkbttm/widclkbttm.wid.js @@ -1,10 +1,10 @@ WIDGETS["wdclkbttm"]={area:"br",width:Bangle.CLOCK?0:60,draw:function() { if (!Bangle.CLOCK == !this.width) { // if we're the wrong size for if we have a clock or not... - this.width = Bangle.CLOCK?0:60; + this.width = Bangle.CLOCK?0:60; return setTimeout(Bangle.drawWidgets,1); // widget changed size - redraw } if (!this.width) return; // if size not right, return - g.reset().setFont("6x8", 2).setFontAlign(-1, 0).setColor("#0ff"); // cyan + g.reset().setFont("6x8", 2).setFontAlign(-1, 0); var time = require("locale").time(new Date(),1); g.drawString(time, this.x, this.y+11, true); // 5 * 6*2 = 60 // queue draw in one minute diff --git a/apps/widhwbttm/ChangeLog b/apps/widhwbttm/ChangeLog new file mode 100644 index 000000000..23192b302 --- /dev/null +++ b/apps/widhwbttm/ChangeLog @@ -0,0 +1,2 @@ +0.01: 1st ver, inspired in some code from widclkbttm (Digital clock bttom widget) +0.02: Correction, intervals, dynamic color and font size depending on device diff --git a/apps/widhwbttm/README.md b/apps/widhwbttm/README.md new file mode 100644 index 000000000..2427ff818 --- /dev/null +++ b/apps/widhwbttm/README.md @@ -0,0 +1,30 @@ +# hw stats bttom widget (bottom widget area) +A basic HW/performance monitor widget that shows on real time some technical info, such as free mem, free storage, trash mem, files, FW version. Also allows to test the unfrequently used widget bottom area. + +Compatible with BangleJS1,BangleJS2,and EMSCRIPTENx emulators + +forked from widclkbttm (Digital clock bttom widget) + + +## Photo + +Example of usage + +![](widhwbttm.ss1.jpg) +![](widhwbttm.ss2.jpg) + + + + +## Usage + +Upload the widget file +Open an app (not a clock/watchface) that supports displaying widgets (included the bottom one) +Different info is refreshed following a predefined frequency and sequence. + + +## Support + +This app is so basic that probably the easiest is to just edit the code ;) + +Otherwise you can contact me [here](https://github.com/dapgo/my_espruino_smartwatch_things) \ No newline at end of file diff --git a/apps/widhwbttm/metadata.json b/apps/widhwbttm/metadata.json new file mode 100644 index 000000000..f455e90a7 --- /dev/null +++ b/apps/widhwbttm/metadata.json @@ -0,0 +1,15 @@ +{ + "id": "widhwbttm", + "name": "HW stats (Bottom) widget", + "shortName": "Digital clock Bottom Widget", + "version": "0.02", + "description": "Displays technical info and mem stats in the bottom of the screen (may not be compatible with some apps)", + "icon": "widhwbttm.png", + "type": "widget", + "tags": "widget", + "supports": ["BANGLEJS","BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"widhwbttm.wid.js","url":"widhwbttm.wid.js"} + ] +} diff --git a/apps/widhwbttm/widhwbttm.png b/apps/widhwbttm/widhwbttm.png new file mode 100644 index 000000000..82ad43795 Binary files /dev/null and b/apps/widhwbttm/widhwbttm.png differ diff --git a/apps/widhwbttm/widhwbttm.ss1.jpg b/apps/widhwbttm/widhwbttm.ss1.jpg new file mode 100644 index 000000000..bfca08f06 Binary files /dev/null and b/apps/widhwbttm/widhwbttm.ss1.jpg differ diff --git a/apps/widhwbttm/widhwbttm.ss2.jpg b/apps/widhwbttm/widhwbttm.ss2.jpg new file mode 100644 index 000000000..17942d0be Binary files /dev/null and b/apps/widhwbttm/widhwbttm.ss2.jpg differ diff --git a/apps/widhwbttm/widhwbttm.ss3.jpg b/apps/widhwbttm/widhwbttm.ss3.jpg new file mode 100644 index 000000000..90fc5eecb Binary files /dev/null and b/apps/widhwbttm/widhwbttm.ss3.jpg differ diff --git a/apps/widhwbttm/widhwbttm.wid.js b/apps/widhwbttm/widhwbttm.wid.js new file mode 100644 index 000000000..ec7b568a0 --- /dev/null +++ b/apps/widhwbttm/widhwbttm.wid.js @@ -0,0 +1,57 @@ +(function() { + let intervalRef = null; + var v_switch; // show stats + var v_str_hw=new String(); + if (process.env.BOARD=='BANGLEJS'||process.env.BOARD=='EMSCRIPTEN') var v_font_size=16; + else var v_font_size=14; + if (v_switch == null || v_switch == '') v_switch=0; + function draw(){ + if (Bangle.CLOCK) return; + + if (v_switch==0) { + // var v_hw=process.env.VERSION; + v_str_hw="V "+process.env.VERSION.substr(0,6); + v_switch++; + } else if (v_switch==1) { + v_str_hw=process.env.BOARD.substr(0,3)+".."+process.env.BOARD.substr(process.env.BOARD.length-3,3); + v_switch++; + } + else if (v_switch==2) { + v_str_hw="Bat "+E.getBattery()+"%"; + v_switch++; + } + else { + // text prefix has to be 4char + stor=require("Storage").getStats(); + if (v_switch==3) { + v_str_hw="Fre "+process.memory().free; + //+"/"+process.memory().total; + v_switch++; + } + else if (v_switch==4) { + v_str_hw="Sto "+stor.freeBytes; + v_switch++; + } else if (v_switch==5) { + v_str_hw="Tra "+stor.trashBytes; + v_switch++; + } else if (v_switch==6) { + v_str_hw="Fil "+stor.fileCount; + v_switch=0; + } + // 4 char are prefix + if (v_str_hw.length>7) { + //replace 3 digits by k + //substring betw x and y + v_str_hw=v_str_hw.substr(0,v_str_hw.length-3)+"k"; + } + } //else storage + g.reset().setFontVector(v_font_size).setFontAlign(-1, 0); + //clean a longer previous string, care with br widgets + g.drawString(" ", this.x, this.y+11, true); + g.drawString(v_str_hw, this.x, this.y+11, true); + } //end draw + +WIDGETS["wdhwbttm"]={area:"bl",width:60,draw:draw}; +//{area:"bl",width:Bangle.CLOCK?0:60,draw:draw}; +if (Bangle.isLCDOn) intervalRef = setInterval(()=>WIDGETS["wdhwbttm"].draw(), 10*1000); +})() diff --git a/apps/widlock/ChangeLog b/apps/widlock/ChangeLog index bb84c2d44..abceba48e 100644 --- a/apps/widlock/ChangeLog +++ b/apps/widlock/ChangeLog @@ -4,3 +4,5 @@ 0.04: Set sortorder to -1 so that widget always takes up the furthest left position 0.05: Set sortorder to -10 so that others can take -1 etc 0.06: Set sortorder to -10 in widget code +0.07: Remove check for .isLocked (extremely old firmwares), speed up widget loading +0.08: Don't completely remove the lock widget when screen unlocked (use 1px) to ensure appRect/drawWidgets still thinks there are widgets diff --git a/apps/widlock/metadata.json b/apps/widlock/metadata.json index 8635a5434..509a5b7a5 100644 --- a/apps/widlock/metadata.json +++ b/apps/widlock/metadata.json @@ -1,7 +1,7 @@ { "id": "widlock", "name": "Lock Widget", - "version": "0.06", + "version": "0.08", "description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked", "icon": "widget.png", "type": "widget", diff --git a/apps/widlock/widget.js b/apps/widlock/widget.js index 592361cd9..b5787e09d 100644 --- a/apps/widlock/widget.js +++ b/apps/widlock/widget.js @@ -1,11 +1,8 @@ -(function(){ - if (!Bangle.isLocked) return; // bail out on old firmware - Bangle.on("lock", function(on) { - WIDGETS["lock"].width = Bangle.isLocked()?16:0; - Bangle.drawWidgets(); - }); - WIDGETS["lock"]={area:"tl",sortorder:10,width:Bangle.isLocked()?16:0,draw:function(w) { - if (Bangle.isLocked()) - g.reset().drawImage(atob("DhABH+D/wwMMDDAwwMf/v//4f+H/h/8//P/z///f/g=="), w.x+1, w.y+4); - }}; -})() +Bangle.on("lock", function() { + WIDGETS["lock"].width = Bangle.isLocked()?16:1; + Bangle.drawWidgets(); +}); +WIDGETS["lock"]={area:"tl",sortorder:10,width:Bangle.isLocked()?16:1,draw:function(w) { + if (Bangle.isLocked()) + g.reset().drawImage(atob("DhABH+D/wwMMDDAwwMf/v//4f+H/h/8//P/z///f/g=="), w.x+1, w.y+4); +}}; diff --git a/apps/widlockunlock/ChangeLog b/apps/widlockunlock/ChangeLog new file mode 100644 index 000000000..b4d1ae593 --- /dev/null +++ b/apps/widlockunlock/ChangeLog @@ -0,0 +1 @@ +0.01: First commit diff --git a/apps/widlockunlock/metadata.json b/apps/widlockunlock/metadata.json new file mode 100644 index 000000000..d701279b9 --- /dev/null +++ b/apps/widlockunlock/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widlockunlock", + "name": "Lock/Unlock Widget", + "version": "0.01", + "description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked, or an unlock icon otherwise", + "icon": "widget.png", + "type": "widget", + "tags": "widget,lock", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widlockunlock.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widlockunlock/widget.js b/apps/widlockunlock/widget.js new file mode 100644 index 000000000..0716a9edf --- /dev/null +++ b/apps/widlockunlock/widget.js @@ -0,0 +1,6 @@ +Bangle.on("lockunlock", function() { + Bangle.drawWidgets(); +}); +WIDGETS["lockunlock"]={area:"tl",sortorder:10,width:14,draw:function(w) { + g.reset().drawImage(atob(Bangle.isLocked() ? "DBGBAAAA8DnDDCBCBP////////n/n/n//////z/A" : "DBGBAAAA8BnDDCBABP///8A8A8Y8Y8Y8A8A//z/A"), w.x+1, w.y+3); +}}; diff --git a/apps/widlockunlock/widget.png b/apps/widlockunlock/widget.png new file mode 100644 index 000000000..3a6b98161 Binary files /dev/null and b/apps/widlockunlock/widget.png differ diff --git a/apps/widmessages/ChangeLog b/apps/widmessages/ChangeLog new file mode 100644 index 000000000..348d49528 --- /dev/null +++ b/apps/widmessages/ChangeLog @@ -0,0 +1,5 @@ +0.01: Moved messages widget into standalone widget app +0.02: Fix 'srcs' being defined in global scope + Remove library stub +0.03: Fix messages not showing if UI auto-open is disabled +0.04: Now shows message icons again (#2416) diff --git a/apps/widmessages/README.md b/apps/widmessages/README.md new file mode 100644 index 000000000..398cb4fa8 --- /dev/null +++ b/apps/widmessages/README.md @@ -0,0 +1,30 @@ +# Messages widget + +The default widget to show icons for new messages +It is installed automatically if you install `Android Integration` or `iOS Integration`. + +![screenshot](screenshot.gif) + +## Settings +You can change settings by going to the global `Settings` app, then `App Settings` +and `Messages`: + +* `Flash icon` Toggle flashing of the widget icons. + +* `Widget messages` Not used by this widget. + +## Requests + +Please file any issues on https://github.com/espruino/BangleApps/issues/new?title=widmessages%widget + +## Creator + +Gordon Williams + +## Contributors + +[Jeroen Peters](https://github.com/jeroenpeters1986) + +## Attributions + +Icons used in this app are from https://icons8.com diff --git a/apps/widmessages/app.png b/apps/widmessages/app.png new file mode 100644 index 000000000..c9177692e Binary files /dev/null and b/apps/widmessages/app.png differ diff --git a/apps/widmessages/metadata.json b/apps/widmessages/metadata.json new file mode 100644 index 000000000..0e399f71f --- /dev/null +++ b/apps/widmessages/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "widmessages", + "name": "Message Widget", + "version": "0.04", + "description": "Widget showing new messages", + "icon": "app.png", + "type": "widget", + "tags": "tool,system", + "supports": ["BANGLEJS","BANGLEJS2"], + "screenshots": [{"url": "screenshot.gif"}], + "dependencies" : { "messageicons":"module" }, + "provides_widgets" : ["message"], + "default" : true, + "readme": "README.md", + "storage": [ + {"name":"widmessages.wid.js","url":"widget.js"} + ] +} diff --git a/apps/messages/screenshot-notify.gif b/apps/widmessages/screenshot.gif similarity index 100% rename from apps/messages/screenshot-notify.gif rename to apps/widmessages/screenshot.gif diff --git a/apps/widmessages/widget.js b/apps/widmessages/widget.js new file mode 100644 index 000000000..44f525ec8 --- /dev/null +++ b/apps/widmessages/widget.js @@ -0,0 +1,76 @@ +(() => { + if ((require("Storage").readJSON("messages.settings.json", true) || {}).maxMessages===0) return; + + function filterMessages(msgs) { + return msgs.filter(msg => msg.new && msg.id != "music") + .map(m => m.src) // we only need this for icon/color + .filter((msg, i, arr) => arr.findIndex(nmsg => msg.src == nmsg.src) == i); + } + + // NOTE when adding a custom "essages" widget: + // the name still needs to be "messages": the library calls WIDGETS["messages'].hide()/show() + // see e.g. widmsggrid + WIDGETS["messages"] = { + area: "tl", width: 0, srcs: [], draw: function(recall) { + // If we had a setTimeout queued from the last time we were called, remove it + if (WIDGETS["messages"].i) { + clearTimeout(WIDGETS["messages"].i); + delete WIDGETS["messages"].i; + } + Bangle.removeListener("touch", this.touch); + if (!this.width) return; + let settings = Object.assign({flash: true, maxMessages: 3}, require("Storage").readJSON("messages.settings.json", true) || {}); + if (recall!==true || settings.flash) { + const msgsShown = E.clip(this.srcs.length, 0, settings.maxMessages); + g.reset().clearRect(this.x, this.y, this.x+this.width, this.y+23); + for(let i = 0; isettings.maxMessages ? atob("EASBAGGG88/zz2GG") : require("messageicons").getImage(src), + this.x+12+i*24, this.y+12, {rotate: 0/*force centering*/}); + } + } + WIDGETS["messages"].i = setTimeout(() => WIDGETS["messages"].draw(true), 1000); + if (process.env.HWVERSION>1) Bangle.on("touch", this.touch); + }, onMsg: function(type, msg) { + if (this.hidden) return; + if (type==="music") return; + if (msg.id && !msg.new && msg.t!=="remove") return; + this.srcs = filterMessages(require("messages").getMessages(msg)); + const settings = Object.assign({maxMessages:3},require('Storage').readJSON("messages.settings.json", true) || {}); + this.width = 24 * E.clip(this.srcs.length, 0, settings.maxMessages); + if (type!=="init") Bangle.drawWidgets(); // "init" is not a real message type: see below + }, touch: function(b, c) { + var w = WIDGETS["messages"]; + if (!w || !w.width || c.xw.x+w.width || c.yw.y+24) return; + require("messages").openGUI(); + }, + // hide() and show() are required by the "message" library! + hide() { + this.hidden=true; + if (this.width) { + // hide widget + this.width = 0; + Bangle.drawWidgets(); + } + }, show() { + delete this.hidden + this.onMsg("show", {}); // reload messages+redraw + } + }; + + Bangle.on("message", WIDGETS["messages"].onMsg.bind(WIDGETS["messages"])); + WIDGETS["messages"].onMsg("init", {}); // abuse type="init" to prevent Bangle.drawWidgets(); +})(); diff --git a/apps/widmsggrid/ChangeLog b/apps/widmsggrid/ChangeLog index 75259c4f0..9be40817a 100644 --- a/apps/widmsggrid/ChangeLog +++ b/apps/widmsggrid/ChangeLog @@ -1,2 +1,4 @@ 0.01: New widget! 0.02: Adjust to message icons moving to messageicons lib +0.03: Use new message library +0.04: Remove library stub \ No newline at end of file diff --git a/apps/widmsggrid/README.md b/apps/widmsggrid/README.md index 86a80c403..36aad20e2 100644 --- a/apps/widmsggrid/README.md +++ b/apps/widmsggrid/README.md @@ -12,17 +12,12 @@ Example: one SMS, one Signal, and two WhatsApp messages: ![screenshot](screenshot.png) ## Installation -This widget needs the [`messages`](/?id=messages) app to handle notifications. - -You probably want to disable the default widget, to do so: -1. Open `Settings` -2. Navigate to `Apps`>`Messages` -3. Scroll down to the `Widget messages` entry, and change it to `Hide` +There can only be one messages widget, so you should uninstall the default "Message Widget". ## Settings -This widget uses the `Widget` settings from the `messages` app: +You can change settings by going to the global `Settings` app, then `App Settings` +and `Messages`: -### Widget * `Flash icon` Toggle flashing of the widget icons. -* `Widget messages` Not used by this widget, but you should select `Hide` to hide the default widget. \ No newline at end of file +* `Widget messages` Not used by this widget. \ No newline at end of file diff --git a/apps/widmsggrid/metadata.json b/apps/widmsggrid/metadata.json index c9ed5bbe0..17d3573ad 100644 --- a/apps/widmsggrid/metadata.json +++ b/apps/widmsggrid/metadata.json @@ -1,14 +1,14 @@ { "id": "widmsggrid", "name": "Messages Grid Widget", - "version": "0.02", - "description": "Widget that display notification icons in a grid", + "version": "0.04", + "description": "Widget that displays notification icons in a grid", "icon": "widget.png", "type": "widget", - "dependencies": {"messages":"app"}, "tags": "tool,system", "supports": ["BANGLEJS","BANGLEJS2"], - "dependencies" : { "messageicons":"module" }, + "dependencies" : { "messages":"module" }, + "provides_widgets" : ["message"], "readme": "README.md", "storage": [ {"name":"widmsggrid.wid.js","url":"widget.js"} diff --git a/apps/widmsggrid/widget.js b/apps/widmsggrid/widget.js index 431adf479..6a5b175ac 100644 --- a/apps/widmsggrid/widget.js +++ b/apps/widmsggrid/widget.js @@ -6,7 +6,8 @@ showRead: !!settings.showRead, }; delete settings; - WIDGETS["msggrid"] = { + // widget name needs to be "messages": the library calls WIDGETS["messages'].hide()/show() + WIDGETS["messages"] = { area: "tl", width: 0, flash: s.flash, showRead: s.showRead, @@ -24,7 +25,7 @@ clearTimeout(w.t); delete w.t; } - if (!w.width) return; + if (!w.width || this.hidden) return; const b = w.flash && w.status === "new" && ((Date.now() / 1000) & 1), // Blink(= inverse colors) on this second? // show multiple icons in a grid, by scaling them down cols = Math.ceil(Math.sqrt(w.srcs.length - 0.1)); // cols===rows, -0.1 to work around rounding error @@ -57,9 +58,12 @@ .drawString(w.total, w.x + w.width - 1, w.y + 24, w.total > 9); } if (w.flash && w.status === "new") w.t = setTimeout(w.draw, 1000); // schedule redraw while blinking - }, show: function () { + }, + // show() and hide() are required by the "message" library! + show: function (m) { + delete w.hidden; w.width = 24; - w.srcs = require("messages").getMessages() + w.srcs = require("messages").getMessages(m) .filter(m => !['call', 'map', 'music'].includes(m.id)) .filter(m => m.new || w.showRead) .map(m => m.src); @@ -68,6 +72,7 @@ Bangle.drawWidgets(); Bangle.setLCDPower(1); // turns screen on }, hide: function () { + w.hidden = true; w.width = 0; w.srcs = []; w.total = 0; @@ -82,13 +87,16 @@ } // Bangle.js 2: open app when touching the widget else if (c.x < w.x || c.x > w.x + w.width || c.y < w.y || c.y > w.y + 24) return; - load("messages.app.js"); - }, listener: function () { - w.status = require("messages").status(); - if (w.status === "new" || (w.status === "old" && w.showRead)) w.show(); + require("messages").openGUI(); + }, listener: function (t,m) { + if (this.hidden) return; + w.status = require("messages").status(m); + if (w.status === "new" || (w.status === "old" && w.showRead)) w.show(m); else w.hide(); + delete w.hidden; // always set by w.hide(), but we checked it wasn't there before } }; delete s; - const w = WIDGETS["msggrid"]; + const w = WIDGETS["messages"]; + Bangle.on("message", w.listener); })(); diff --git a/bin/apploader.js b/bin/apploader.js index 26c4c1f09..d4a5f828e 100755 --- a/bin/apploader.js +++ b/bin/apploader.js @@ -14,7 +14,6 @@ for Noble. var SETTINGS = { pretokenise : true }; -var APPSDIR = __dirname+"/../apps/"; var noble; ["@abandonware/noble", "noble"].forEach(module => { if (!noble) try { @@ -37,36 +36,18 @@ function ERROR(msg) { process.exit(1); } -//eval(require("fs").readFileSync(__dirname+"../core/js/utils.js")); -var AppInfo = require("../core/js/appinfo.js"); -global.Const = { - /* Are we only putting a single app on a device? If so - apps should all be saved as .bootcde and we write info - about the current app into app.info */ - SINGLE_APP_ONLY : false, -}; var deviceId = "BANGLEJS2"; -var apps = []; -var dirs = require("fs").readdirSync(APPSDIR, {withFileTypes: true}); -dirs.forEach(dir => { - var appsFile; - if (dir.name.startsWith("_example") || !dir.isDirectory()) - return; - try { - appsFile = require("fs").readFileSync(APPSDIR+dir.name+"/metadata.json").toString(); - } catch (e) { - ERROR(dir.name+"/metadata.json does not exist"); - return; - } - apps.push(JSON.parse(appsFile)); -}); +var apploader = require("./lib/apploader.js"); var args = process.argv; var bangleParam = args.findIndex(arg => /-b\d/.test(arg)); if (bangleParam!==-1) { deviceId = "BANGLEJS"+args.splice(bangleParam, 1)[0][2]; } +apploader.init({ + DEVICEID : deviceId +}); if (args.length==3 && args[2]=="list") cmdListApps(); else if (args.length==3 && args[2]=="devices") cmdListDevices(); else if (args.length==4 && args[2]=="install") cmdInstallApp(args[3]); @@ -90,7 +71,7 @@ process.exit(0); } function cmdListApps() { - console.log(apps.map(a=>a.id).join("\n")); + console.log(apploader.apps.map(a=>a.id).join("\n")); } function cmdListDevices() { var foundDevices = []; @@ -113,19 +94,10 @@ function cmdListDevices() { } function cmdInstallApp(appId, deviceAddress) { - var app = apps.find(a=>a.id==appId); + var app = apploader.apps.find(a=>a.id==appId); if (!app) ERROR(`App ${JSON.stringify(appId)} not found`); if (app.custom) ERROR(`App ${JSON.stringify(appId)} requires HTML customisation`); - return AppInfo.getFiles(app, { - fileGetter:function(url) { - console.log(__dirname+"/"+url); - return Promise.resolve(require("fs").readFileSync(__dirname+"/../"+url).toString("binary")); - }, - settings : SETTINGS, - device : { id : deviceId } - }).then(files => { - //console.log(files); - var command = files.map(f=>f.cmd).join("\n")+"\n"; + return apploader.getAppFilesString(app).then(command => { bangleSend(command, deviceAddress).then(() => process.exit(0)); }); } diff --git a/bin/firmwaremaker.js b/bin/firmwaremaker.js index 1dc5ec073..4535c4a5e 100755 --- a/bin/firmwaremaker.js +++ b/bin/firmwaremaker.js @@ -1,17 +1,12 @@ -#!/usr/bin/env nodejs +#!/usr/bin/env node /* Mashes together a bunch of different apps to make a single firmware JS file which can be uploaded. */ -var SETTINGS = { - pretokenise : true -}; - var path = require('path'); var ROOTDIR = path.join(__dirname, '..'); -var APPDIR = ROOTDIR+'/apps'; var OUTFILE = ROOTDIR+'/firmware.js'; -var DEVICE = "BANGLEJS"; +var DEVICEID = "BANGLEJS"; var APPS = [ // IDs of apps to install "boot","launch","mclock","setting", "about","alarm","widbat","widbt","welcome" @@ -19,53 +14,17 @@ var APPS = [ // IDs of apps to install var MINIFY = true; var fs = require("fs"); -global.Const = { - /* Are we only putting a single app on a device? If so - apps should all be saved as .bootcde and we write info - about the current app into app.info */ - SINGLE_APP_ONLY : false, -}; +var apploader = require("./lib/apploader.js"); +apploader.init({ + DEVICEID : DEVICEID +}); -var AppInfo = require(ROOTDIR+"/core/js/appinfo.js"); var appfiles = []; -function fileGetter(url) { - console.log("Loading "+url) - if (MINIFY) { - /*if (url.endsWith(".js")) { - var f = url.slice(0,-3); - console.log("MINIFYING "+f); - const execSync = require('child_process').execSync; - // --config PRETOKENISE=true - // --minify - code = execSync(`espruino --config SET_TIME_ON_WRITE=false --minify --board BANGLEJS ${f}.js -o ${f}.min.js`); - console.log(code.toString()); - url = f+".min.js"; - }*/ - if (url.endsWith(".json")) { - var f = url.slice(0,-5); - console.log("MINIFYING JSON "+f); - var j = eval("("+fs.readFileSync(url).toString("binary")+")"); - var code = JSON.stringify(j); - //console.log(code); - url = f+".min.json"; - fs.writeFileSync(url, code); - } - } - return Promise.resolve(fs.readFileSync(url).toString("binary")); -} - Promise.all(APPS.map(appid => { - try { - var app = JSON.parse(fs.readFileSync(APPDIR + "/" + appid + "/metadata.json").toString()); - } catch (e) { - throw new Error(`App ${appid} not found`); - } - return AppInfo.getFiles(app, { - fileGetter : fileGetter, - settings : SETTINGS, - device : { id : DEVICE } - }).then(files => { + var app = apploader.apps.find(a => a.id==appid); + if (!app) throw new Error(`App ${appid} not found`); + return apploader.getAppFiles(app).then(files => { appfiles = appfiles.concat(files); }); })).then(() => { diff --git a/bin/firmwaremaker_c.js b/bin/firmwaremaker_c.js index 08fc6fe83..54b63d5d9 100755 --- a/bin/firmwaremaker_c.js +++ b/bin/firmwaremaker_c.js @@ -7,25 +7,20 @@ to populate Storage initially. Bangle.js 1 doesn't really have anough flash space for this, but we have enough on v2. */ -var SETTINGS = { - pretokenise : true -}; - -var DEVICE = process.argv[2]; +var DEVICEID = process.argv[2]; var path = require('path'); +var fs = require("fs"); var ROOTDIR = path.join(__dirname, '..'); -var APPDIR = ROOTDIR+'/apps'; -var MINIFY = true; var OUTFILE, APPS; -if (DEVICE=="BANGLEJS") { +if (DEVICEID=="BANGLEJS") { var OUTFILE = path.join(ROOTDIR, '../Espruino/libs/banglejs/banglejs1_storage_default.c'); var APPS = [ // IDs of apps to install "boot","launch","mclock","setting", "about","alarm","sched","widbat","widbt","welcome" ]; -} else if (DEVICE=="BANGLEJS2") { +} else if (DEVICEID=="BANGLEJS2") { var OUTFILE = path.join(ROOTDIR, '../Espruino/libs/banglejs/banglejs2_storage_default.c'); var APPS = [ // IDs of apps to install "boot","launch","antonclk","setting", @@ -37,16 +32,12 @@ if (DEVICE=="BANGLEJS") { console.log(" bin/firmwaremaker_c.js BANGLEJS2"); process.exit(1); } -console.log("Device = ",DEVICE); +console.log("Device = ",DEVICEID); - -var fs = require("fs"); -global.Const = { - /* Are we only putting a single app on a device? If so - apps should all be saved as .bootcde and we write info - about the current app into app.info */ - SINGLE_APP_ONLY : false, -}; +var apploader = require("./lib/apploader.js"); +apploader.init({ + DEVICEID : DEVICEID +}); function atob(input) { @@ -84,31 +75,8 @@ function atob(input) { return new Uint8Array(output); } -var AppInfo = require(ROOTDIR+"/core/js/appinfo.js"); var appfiles = []; -function fileGetter(url) { - console.log("Loading "+url) - if (MINIFY) { - if (url.endsWith(".json")) { - var f = url.slice(0,-5); - console.log("MINIFYING JSON "+f); - var j = eval("("+fs.readFileSync(url).toString("binary")+")"); - var code = JSON.stringify(j); - //console.log(code); - url = f+".min.json"; - fs.writeFileSync(url, code); - } - } - var blob = fs.readFileSync(url); - var data; - if (url.endsWith(".js") || url.endsWith(".json")) - data = blob.toString(); // allow JS/etc to be written in UTF-8 - else - data = blob.toString("binary") - return Promise.resolve(data); -} - // If file should be evaluated, try and do it... function evaluateFile(file) { var hsStart = 'require("heatshrink").decompress(atob("'; @@ -132,16 +100,9 @@ function evaluateFile(file) { } Promise.all(APPS.map(appid => { - try { - var app = JSON.parse(fs.readFileSync(APPDIR + "/" + appid + "/metadata.json").toString()); - } catch (e) { - throw new Error(`App ${appid} not found`); - } - return AppInfo.getFiles(app, { - fileGetter : fileGetter, - settings : SETTINGS, - device : { id : DEVICE } - }).then(files => { + var app = apploader.apps.find(a => a.id==appid); + if (!app) throw new Error(`App ${appid} not found`); + return apploader.getAppFiles(app).then(files => { appfiles = appfiles.concat(files); }); })).then(() => { diff --git a/bin/lib/apploader.js b/bin/lib/apploader.js new file mode 100644 index 000000000..6bf74eb7e --- /dev/null +++ b/bin/lib/apploader.js @@ -0,0 +1,82 @@ +/* Node.js library with utilities to handle using the app loader from node.js */ + +var DEVICEID = "BANGLEJS2"; +var MINIFY = true; // minify JSON? +var BASE_DIR = __dirname + "/../.."; +var APPSDIR = BASE_DIR+"/apps/"; + +//eval(require("fs").readFileSync(__dirname+"../core/js/utils.js")); +var Espruino = require(__dirname + "/../../core/lib/espruinotools.js"); +//eval(require("fs").readFileSync(__dirname + "/../../core/lib/espruinotools.js").toString()); +//eval(require("fs").readFileSync(__dirname + "/../../core/js/utils.js").toString()); +var AppInfo = require(__dirname+"/../../core/js/appinfo.js"); + +var SETTINGS = { + pretokenise : true +}; +global.Const = { + /* Are we only putting a single app on a device? If so + apps should all be saved as .bootcde and we write info + about the current app into app.info */ + SINGLE_APP_ONLY : false, +}; + +var apps = []; + +// call with {DEVICEID:"BANGLEJS/BANGLEJS2"} +exports.init = function(options) { + if (options.DEVICEID) + DEVICEID = options.DEVICEID; + // Load app metadata + var dirs = require("fs").readdirSync(APPSDIR, {withFileTypes: true}); + dirs.forEach(dir => { + var appsFile; + if (dir.name.startsWith("_example") || !dir.isDirectory()) + return; + try { + appsFile = require("fs").readFileSync(APPSDIR+dir.name+"/metadata.json").toString(); + } catch (e) { + ERROR(dir.name+"/metadata.json does not exist"); + return; + } + apps.push(JSON.parse(appsFile)); + }); +}; + +exports.AppInfo = AppInfo; +exports.apps = apps; + +// used by getAppFiles +function fileGetter(url) { + url = BASE_DIR+"/"+url; + console.log("Loading "+url) + var data; + if (MINIFY && url.endsWith(".json")) { + var f = url.slice(0,-5); + console.log("MINIFYING JSON "+f); + var j = eval("("+require("fs").readFileSync(url).toString("binary")+")"); + data = JSON.stringify(j); + } else { + var blob = require("fs").readFileSync(url); + if (url.endsWith(".js") || url.endsWith(".json")) + data = blob.toString(); // allow JS/etc to be written in UTF-8 + else + data = blob.toString("binary") + } + return Promise.resolve(data); +} + +exports.getAppFiles = function(app) { + return AppInfo.getFiles(app, { + fileGetter:fileGetter, + settings : SETTINGS, + device : { id : DEVICEID } + }); +}; + +// Get all the files for this app as a string of Storage.write commands +exports.getAppFilesString = function(app) { + return exports.getAppFiles(app).then(files => { + return files.map(f=>f.cmd).join("\n")+"\n" + }) +}; diff --git a/bin/lib/emulator.js b/bin/lib/emulator.js new file mode 100644 index 000000000..f7c82ec3c --- /dev/null +++ b/bin/lib/emulator.js @@ -0,0 +1,115 @@ +/* Node.js library with utilities to handle using the emulator from node.js */ + +var EMULATOR = "banglejs2"; +var DEVICEID = "BANGLEJS2"; + +var BASE_DIR = __dirname + "/../.."; +var DIR_IDE = BASE_DIR + "/../EspruinoWebIDE"; + +/* we factory reset ONCE, get this, then we can use it to reset +state quickly for each new app */ +var factoryFlashMemory; + +// Log of messages from app +var appLog = ""; +var lastOutputLine = ""; + +function onConsoleOutput(txt) { + appLog += txt + "\n"; + lastOutputLine = txt; +} + +exports.init = function(options) { + if (options.EMULATOR) + EMULATOR = options.EMULATOR; + if (options.DEVICEID) + DEVICEID = options.DEVICEID; + + eval(require("fs").readFileSync(DIR_IDE + "/emu/emulator_"+EMULATOR+".js").toString()); + eval(require("fs").readFileSync(DIR_IDE + "/emu/emu_"+EMULATOR+".js").toString()); + eval(require("fs").readFileSync(DIR_IDE + "/emu/common.js").toString()/*.replace('console.log("EMSCRIPTEN:"', '//console.log("EMSCRIPTEN:"')*/); + + jsRXCallback = function() {}; + jsUpdateGfx = function() {}; + + factoryFlashMemory = new Uint8Array(FLASH_SIZE); + factoryFlashMemory.fill(255); + + exports.flashMemory = flashMemory; + exports.GFX_WIDTH = GFX_WIDTH; + exports.GFX_HEIGHT = GFX_HEIGHT; + exports.tx = jsTransmitString; + exports.idle = jsIdle; + exports.stopIdle = jsStopIdle; + exports.getGfxContents = jsGetGfxContents; + + return new Promise(resolve => { + setTimeout(function() { + console.log("Emulator Loaded..."); + jsInit(); + jsIdle(); + console.log("Emulator Factory reset"); + exports.tx("Bangle.factoryReset()\n"); + factoryFlashMemory.set(flashMemory); + console.log("Emulator Ready!"); + + resolve(); + },0); + }); +}; + +// Factory reset +exports.factoryReset = function() { + exports.flashMemory.set(factoryFlashMemory); + exports.tx("reset()\n"); + appLog=""; +}; + +// Transmit a string +exports.tx = function() {}; // placeholder +exports.idle = function() {}; // placeholder +exports.stopIdle = function() {}; // placeholder +exports.getGfxContents = function() {}; // placeholder + +exports.flashMemory = undefined; // placeholder +exports.GFX_WIDTH = undefined; // placeholder +exports.GFX_HEIGHT = undefined; // placeholder + +// Get last line sent to console +exports.getLastLine = function() { + return lastOutputLine; +}; + +// Gets the screenshot as RGBA Uint32Array +exports.getScreenshot = function() { + var rgba = new Uint8Array(exports.GFX_WIDTH*exports.GFX_HEIGHT*4); + exports.getGfxContents(rgba); + var rgba32 = new Uint32Array(rgba.buffer); + return rgba32; +} + +// Write the screenshot to a file options={errorIfBlank} +exports.writeScreenshot = function(imageFn, options) { + options = options||{}; + return new Promise((resolve,reject) => { + var rgba32 = exports.getScreenshot(); + + if (options.errorIfBlank) { + var firstPixel = rgba32[0]; + var blankImage = rgba32.every(col=>col==firstPixel); + if (blankImage) reject("Image is blank"); + } + + var Jimp = require("jimp"); + let image = new Jimp(exports.GFX_WIDTH, exports.GFX_HEIGHT, function (err, image) { + if (err) throw err; + let buffer = image.bitmap.data; + buffer.set(new Uint8Array(rgba32.buffer)); + image.write(imageFn, (err) => { + if (err) return reject(err); + console.log("Image written as "+imageFn); + resolve(); + }); + }); + }); +} diff --git a/bin/runapptests.js b/bin/runapptests.js new file mode 100755 index 000000000..8a415b109 --- /dev/null +++ b/bin/runapptests.js @@ -0,0 +1,138 @@ +#!/usr/bin/node +/* + +This allows us to test apps using the Bangle.js emulator + +IT IS UNFINISHED + +It searches for `test.json` in each app's directory and will +run them in sequence. + +TODO: + +* more code to test with +* run tests that we have found and loaded (currently we just use TEST) +* documentation +* actual tests +* detecting 'Uncaught Error' +* logging of success/fail +* ... + +*/ + +// A si +var TEST = { + app : "android", + tests : [ { + load : "messagesgui.app.js", + steps : [ + {t:"gb", "obj":{"t":"notify","id":1234,"src":"Twitter","title":"A Name","body":"message contents"}}, + {t:"cmd", "js":"X='hello';"}, + {t:"eval", "js":"X", eq:"hello"} + ] + }] +}; + +var EMULATOR = "banglejs2"; +var DEVICEID = "BANGLEJS2"; + +var BASE_DIR = __dirname + "/.."; +var APP_DIR = BASE_DIR + "/apps"; +var DIR_IDE = BASE_DIR + "/../EspruinoWebIDE"; + + +if (!require("fs").existsSync(DIR_IDE)) { + console.log("You need to:"); + console.log(" git clone https://github.com/espruino/EspruinoWebIDE"); + console.log("At the same level as this project"); + process.exit(1); +} + +var apploader = require(BASE_DIR+"/bin/lib/apploader.js"); +apploader.init({ + DEVICEID : DEVICEID +}); +var emu = require(BASE_DIR+"/bin/lib/emulator.js"); + +// Last set of text received +var lastTxt; + +function ERROR(s) { + console.error(s); + process.exit(1); +} + +function runTest(test) { + var app = apploader.apps.find(a=>a.id==test.app); + if (!app) ERROR(`App ${JSON.stringify(test.app)} not found`); + if (app.custom) ERROR(`App ${JSON.stringify(appId)} requires HTML customisation`); + return apploader.getAppFilesString(app).then(command => { + // What about dependencies?? + test.tests.forEach((subtest,subtestIdx) => { + console.log(`==============================`); + console.log(`"${test.app}" Test ${subtestIdx}`); + console.log(`==============================`); + emu.factoryReset(); + console.log("Sending app "+test.app); + emu.tx(command); + console.log("Sent app"); + emu.tx(test.load ? `load(${JSON.stringify(test.load)})\n` : "load()\n"); + console.log("App Loaded."); + var ok = true; + subtest.steps.forEach(step => { + if (ok) switch(step.t) { + case "cmd" : emu.tx(`${step.js}\n`); break; + case "gb" : emu.tx(`GB(${JSON.stringify(step.obj)})\n`); break; + case "tap" : emu.tx(`Bangle.emit(...)\n`); break; + case "eval" : + emu.tx(`\x10print(JSON.stringify(${step.js}))\n`); + var result = emu.getLastLine(); + var expected = JSON.stringify(step.eq); + console.log("GOT "+result); + if (result!=expected) { + console.log("EXPECTED "+expected); + ok = false; + } + break; + // tap/touch/drag/button press + // delay X milliseconds? + case "screenshot" : + console.log("Compare screenshots"); + break; + default: ERROR("Unknown step type "+step.t); + } + }); + }); + emu.stopIdle(); + }); +} + + +emu.init({ + EMULATOR : EMULATOR, + DEVICEID : DEVICEID +}).then(function() { + // Emulator is now loaded + console.log("Loading tests"); + var tests = []; + apploader.apps.forEach(app => { + var testFile = APP_DIR+"/"+app.id+"/test.json"; + if (!require("fs").existsSync(testFile)) return; + var test = JSON.parse(require("fs").readFileSync(testFile).toString()); + test.app = app.id; + tests.push(test); + }); + // Running tests + runTest(TEST); +}); +/* + if (erroredApps.length) { + erroredApps.forEach(app => { + console.log(`::error file=${app.id}::${app.id}`); + console.log("::group::Log"); + app.log.split("\n").forEach(line => console.log(`\u001b[38;2;255;0;0m${line}`)); + console.log("::endgroup::"); + }); + process.exit(1); + } +*/ diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index 7ee07bebc..82b2896b8 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -76,7 +76,7 @@ const APP_KEYS = [ 'id', 'name', 'shortName', 'version', 'icon', 'screenshots', 'description', 'tags', 'type', 'sortorder', 'readme', 'custom', 'customConnect', 'interface', 'storage', 'data', 'supports', 'allow_emulator', - 'dependencies', 'provides_modules' + 'dependencies', 'provides_modules', 'provides_widgets', "default" ]; const STORAGE_KEYS = ['name', 'url', 'content', 'evaluate', 'noOverwite', 'supports', 'noOverwrite']; const DATA_KEYS = ['name', 'wildcard', 'storageFile', 'url', 'content', 'evaluate']; @@ -92,7 +92,9 @@ const INTERNAL_FILES_IN_APP_TYPE = { // list of app types and files they SHOULD }; /* These are warnings we know about but don't want in our output */ var KNOWN_WARNINGS = [ -"App gpsrec data file wildcard .gpsrc? does not include app ID" +"App gpsrec data file wildcard .gpsrc? does not include app ID", +"App owmweather data file weather.json is also listed as data file for app weather", + "App messagegui storage file messagegui is also listed as storage file for app messagelist", ]; function globToRegex(pattern) { @@ -167,8 +169,8 @@ apps.forEach((app,appIdx) => { if (app.dependencies) { if (("object"==typeof app.dependencies) && !Array.isArray(app.dependencies)) { Object.keys(app.dependencies).forEach(dependency => { - if (!["type","app","module"].includes(app.dependencies[dependency])) - ERROR(`App ${app.id} 'dependencies' must all be tagged 'type/app/module' right now`, {file:metadataFile}); + if (!["type","app","module","widget"].includes(app.dependencies[dependency])) + ERROR(`App ${app.id} 'dependencies' must all be tagged 'type/app/module/widget' right now`, {file:metadataFile}); if (app.dependencies[dependency]=="type" && !METADATA_TYPES.includes(dependency)) ERROR(`App ${app.id} 'type' dependency must be one of `+METADATA_TYPES, {file:metadataFile}); }); @@ -244,7 +246,7 @@ apps.forEach((app,appIdx) => { if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id} file ${file.name} has unknown key ${key}`, {file:appDirRelative+file.url}); } // warn if JS icon is the wrong size - if (file.name == app.id+".img") { + if (file.name == app.id+".img" && file.evaluate) { let icon; let match = fileContents.match(/^\s*E\.toArrayBuffer\(atob\(\"([^"]*)\"\)\)\s*$/); if (match==null) match = fileContents.match(/^\s*atob\(\"([^"]*)\"\)\s*$/); diff --git a/bin/thumbnailer.js b/bin/thumbnailer.js index 0895098e9..22cdc27a5 100755 --- a/bin/thumbnailer.js +++ b/bin/thumbnailer.js @@ -6,6 +6,10 @@ var DEVICEID = "BANGLEJS2"; */ var EMULATOR = "banglejs1"; var DEVICEID = "BANGLEJS"; +var SCREENSHOT_DIR = __dirname+"/../screenshots/"; + +var emu = require("./lib/emulator.js"); +var apploader = require("./lib/apploader.js"); var singleAppId; @@ -20,130 +24,66 @@ if (process.argv.length!=3 && process.argv.length!=2) { if (process.argv.length==3) singleAppId = process.argv[2]; -if (!require("fs").existsSync(__dirname + "/../../EspruinoWebIDE")) { - console.log("You need to:"); - console.log(" git clone https://github.com/espruino/EspruinoWebIDE"); - console.log("At the same level as this project"); - process.exit(1); -} - -eval(require("fs").readFileSync(__dirname + "/../../EspruinoWebIDE/emu/emulator_"+EMULATOR+".js").toString()); -eval(require("fs").readFileSync(__dirname + "/../../EspruinoWebIDE/emu/emu_"+EMULATOR+".js").toString()); -eval(require("fs").readFileSync(__dirname + "/../../EspruinoWebIDE/emu/common.js").toString()); - -var SETTINGS = { - pretokenise : true -}; -var Const = { -}; -module = undefined; -var Espruino = require(__dirname + "/../core/lib/espruinotools.js"); -//eval(require("fs").readFileSync(__dirname + "/../core/lib/espruinotools.js").toString()); -eval(require("fs").readFileSync(__dirname + "/../core/js/utils.js").toString()); -eval(require("fs").readFileSync(__dirname + "/../core/js/appinfo.js").toString()); -var apps = JSON.parse(require("fs").readFileSync(__dirname+"/../apps.json")); - -/* we factory reset ONCE, get this, then we can use it to reset -state quickly for each new app */ -var factoryFlashMemory = new Uint8Array(FLASH_SIZE); -// Log of messages from app -var appLog = ""; // List of apps that errored var erroredApps = []; -jsRXCallback = function() {}; -jsUpdateGfx = function() {}; - function ERROR(s) { console.error(s); process.exit(1); } -function onConsoleOutput(txt) { - appLog += txt + "\n"; -} - function getThumbnail(appId, imageFn) { console.log("Thumbnail for "+appId); - var app = apps.find(a=>a.id==appId); + var app = apploader.apps.find(a=>a.id==appId); if (!app) ERROR(`App ${JSON.stringify(appId)} not found`); if (app.custom) ERROR(`App ${JSON.stringify(appId)} requires HTML customisation`); - return new Promise(resolve => { - AppInfo.getFiles(app, { - fileGetter:function(url) { - console.log(__dirname+"/"+url); - return Promise.resolve(require("fs").readFileSync(__dirname+"/../"+url).toString("binary")); - }, - settings : SETTINGS, - device : { id : DEVICEID } - }).then(files => { - console.log(`AppInfo returned for ${appId}`);//, files); - flashMemory.set(factoryFlashMemory); - jsTransmitString("reset()\n"); - console.log("Uploading..."); - jsTransmitString("g.clear()\n"); - var command = files.map(f=>f.cmd).join("\n")+"\n"; - command += `load("${appId}.app.js")\n`; - appLog = ""; - jsTransmitString(command); - console.log("Done."); - jsTransmitString("Bangle.setLCDMode();clearInterval();clearTimeout();\n"); - jsStopIdle(); - - var rgba = new Uint8Array(GFX_WIDTH*GFX_HEIGHT*4); - jsGetGfxContents(rgba); - var rgba32 = new Uint32Array(rgba.buffer); - var firstPixel = rgba32[0]; - var blankImage = rgba32.every(col=>col==firstPixel) - - if (appLog.replace("Uncaught Storage Updated!", "").indexOf("Uncaught")>=0) - erroredApps.push( { id : app.id, log : appLog } ); - - if (!blankImage) { - var Jimp = require("jimp"); - let image = new Jimp(GFX_WIDTH, GFX_HEIGHT, function (err, image) { - if (err) throw err; - let buffer = image.bitmap.data; - buffer.set(rgba); - image.write(imageFn, (err) => { - if (err) throw err; - console.log("Image written as "+imageFn); - resolve(true); - }); - }); - } else { - console.log("Image is empty"); - resolve(false); - } + return apploader.getAppFilesString(app).then(command => { + console.log(`AppInfo returned for ${appId}`);//, files); + emu.factoryReset(); + console.log("Uploading..."); + emu.tx("g.clear()\n"); + command += `load("${appId}.app.js")\n`; + appLog = ""; + emu.tx(command); + console.log("Done."); + emu.tx("Bangle.setLCDMode();clearInterval();clearTimeout();\n"); + emu.stopIdle(); + return emu.writeScreenshot(imageFn, { errorIfBlank : true }).then(() => console.log("X")).catch( err => { + console.log("Error", err); }); }); } var screenshots = []; +apploader.init({ + EMULATOR : EMULATOR, + DEVICEID : DEVICEID +}); // wait until loaded... -setTimeout(function() { - console.log("Loaded..."); - jsInit(); - jsIdle(); - console.log("Factory reset"); - jsTransmitString("Bangle.factoryReset()\n"); - factoryFlashMemory.set(flashMemory); - console.log("Ready!"); - +emu.init({ + EMULATOR : EMULATOR, + DEVICEID : DEVICEID +}).then(function() { if (singleAppId) { - getThumbnail(singleAppId, "screenshots/"+singleAppId+"-"+EMULATOR+".png"); + console.log("Single Screenshot"); + getThumbnail(singleAppId, SCREENSHOT_DIR+singleAppId+"-"+EMULATOR+".png"); return; } - var appList = apps.filter(app => (!app.type || app.type=="clock") && !app.custom); + console.log("Screenshot ALL"); + var appList = apploader.apps.filter(app => (!app.type || app.type=="clock") && !app.custom); appList = appList.filter(app => !app.screenshots && app.supports.includes(DEVICEID)); var promise = Promise.resolve(); appList.forEach(app => { + if (!app.supports.includes(DEVICEID)) { + console.log(`App ${app.id} isn't designed for ${DEVICEID}`); + return; + } promise = promise.then(() => { var imageFile = "screenshots/"+app.id+"-"+EMULATOR+".png"; return getThumbnail(app.id, imageFile).then(ok => { diff --git a/core b/core index 8840c7248..376824068 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 8840c7248d4ce757e0011dcedf906b8e3dfe7125 +Subproject commit 376824068d90986c245b46970fd80ccdca44e431 diff --git a/defaultapps_banglejs2.json b/defaultapps_banglejs2.json index 04bd44504..f96f81f60 100644 --- a/defaultapps_banglejs2.json +++ b/defaultapps_banglejs2.json @@ -1 +1 @@ -["boot","launch","antonclk","health","setting","about","widbat","widbt","widlock","widid"] +["boot","launch","antonclk","health","setting","about","alarm","widbat","widbt","widlock","widid"] diff --git a/lang/de_DE.json b/lang/de_DE.json index ef22b588f..5ae7e449f 100644 --- a/lang/de_DE.json +++ b/lang/de_DE.json @@ -20,7 +20,7 @@ "On": "Ein", "Off": "Aus", "Ok": "OK", - "New Timer": "Neuer Kurzzeitwecker", + "New Timer": "Neuer Timer", "(repeat)": "(Wiederholung)", "music": "Musik", "Keep Msgs": "Nachrichten behalten", @@ -148,7 +148,7 @@ "Whitelist": "Whitelist", "Select Clock": "Uhr auswählen", "Disable": "Deaktivieren", - "Timer": "Kurzzeitwecker", + "Timer": "Timer", "Error in settings": "Fehler in den Einstellungen", "Set Time": "Zeit einstellen", "ALARM": "ALARM", @@ -193,7 +193,15 @@ "cyan": "Cyan", "orange": "Orange", "purple": "Violett", - "grey": "Grau" + "grey": "Grau", + "New Moon": "Neumond", + "Waxing Crescent": "Zunehmender Sichelmond ", + "First Quarter": "Zunehmender Halbmond", + "Waxing Gibbous": "Zunehmender Mond", + "Full Moon": "Vollmond,", + "Waning Gibbous": "Abnehmender Mond", + "Last Quater": "Abnehmender Halbmond", + "Waning Crescent": "Abnehmender Sichelmond" }, "alarm": { "//": "App-specific overrides", diff --git a/lang/sv_SE.json b/lang/sv_SE.json index 6e0f30fa4..2c2ff5acf 100644 --- a/lang/sv_SE.json +++ b/lang/sv_SE.json @@ -6,34 +6,34 @@ "Hours": "Timmar", "Minutes": "Minuter", "Enabled": "Aktiverad", - "New Alarm": "Ny alarm", + "New Alarm": "Nytt larm", "Save": "Spara", "Back": "Tillbaka", "Repeat": "Upprepning", "Delete": "Radera", - "ALARM!": "ALURH!", + "ALARM!": "LARM!", "Sleep": "Sömn", "circle 3": "cirkel 3", "circle 1": "cirkel 1", "music": "musik", "week": "vecka", "Keep Msgs": "Behåll meddelanden", - "Auto snooze": "Automatisk snooze", + "Auto snooze": "Auto-snooza", "step length": "steglängd", "Circle": "Cirkel", "data": "uppgifter", - "colorize icon": "färglägga ikonen", - "min. confidence": "Min. förtroende", - "show widgets": "visa widgets", + "colorize icon": "färglägg ikon", + "min. confidence": "min. konfidens", + "show widgets": "visa widgetar", "valid period": "giltig period", "Heartrate": "Hjärtfrekvens", - "distance goal": "mål för distans", + "distance goal": "distansmål", "circle 4": "cirkel 4", "circle count": "antal cirklar", "minimum": "minimum", "maximum": "maximal", "New Timer": "Ny timer", - "battery warn": "batteri varning", + "battery warn": "batterivarning", "heartrate": "hjärtfrekvens", "circle 2": "cirkel 2", "(repeat)": "(upprepning)", @@ -42,30 +42,30 @@ "No Messages": "Inga meddelanden", "Show clocks": "Visa klockor", "STEPS": "STEG", - "TAP right top/bottom": "TAP höger upp/ner", + "TAP right top/bottom": "KNACKA höger upp/ner", "View Message": "Visa meddelande", "Mark Unread": "Markera oläst", - "Are you sure": "Är du säker på att", + "Are you sure": "Är du säker", "Delete all messages": "Radera alla meddelanden", - "Record Run": "Rekordkörning", + "Record Run": "Spåra löprunda", "Unread timer": "Oläst timer", "Vibration": "Vibrationer", - "Utils": "Användaruppgifter", + "Utils": "Verktyg", "Quiet Mode": "Tyst läge", "Passkey BETA": "Passkey BETA", "Dark BW": "Mörk BW", - "BTNs 1:startlap 2:exit 3:reset": "BTN 1:startlap 2:exit 3:reset", - "start&lap/reset, BTN1: EXIT": "start&lap/återställning, BTN1: EXIT", + "BTNs 1:startlap 2:exit 3:reset": "BTN 1:starta varv 2:lämna 3:återställ", + "start&lap/reset, BTN1: EXIT": "starta&varv/återställ, BTN1: LÄMNA", "BLE": "BLE", "Programmable": "Programmerbar", - "Launcher Settings": "Inställningar för lanseringen", - "Vector font size": "Vektor teckensnittsstorlek", + "Launcher Settings": "Inställningar för launchern", + "Vector font size": "Storlek vektortypsnitt", "Font": "Typsnitt", "Yes\ndefinitely": "Ja\ndefinitivt", "App Source\nNot found": "App-källa\nEj funnen", - "Make Connectable": "Gör det möjligt att ansluta", + "Make Connectable": "Gör anslutningsbar", "HID": "HID", - "Bluetooth": "Bluetooth", + "Bluetooth": "Blåtand", "Apps": "Appar", "Piezo": "Piezo", "LCD": "LCD", @@ -73,46 +73,46 @@ "Light BW": "Ljus BW", "Background": "Bakgrund", "Remove": "Ta bort", - "Highlight BG": "Markera BG", + "Highlight BG": "Markering BG", "Customize": "Anpassa", - "Highlight FG": "Highlight FG", + "Highlight FG": "Markering FG", "Background 2": "Bakgrund 2", "LCD Brightness": "Ljusstyrka på LCD-skärmen", "Add Device": "Lägg till enhet", - "Wake on BTN1": "Vakna på BTN1", - "Wake on BTN2": "Vakna på BTN2", - "Twist Timeout": "Twist Timeout", + "Wake on BTN1": "Vakna av BTN1", + "Wake on BTN2": "Vakna av BTN2", + "Twist Timeout": "Vridning Timeout", "Wake on Touch": "Vakna vid beröring", "LCD Timeout": "LCD Timeout", "Foreground": "Förgrund", - "Connect device\nto add to\nwhitelist": "Anslut enhet\nför att lägga till\nvitlista", - "Wake on FaceUp": "Vakna på FaceUp", + "Connect device\nto add to\nwhitelist": "Anslut enhet\nför att lägga till\ni vitlistan", + "Wake on FaceUp": "Skärm upp väcker", "Twist Threshold": "Tröskelvärde för vridning", - "Wake on BTN3": "Wake på BTN3", + "Wake on BTN3": "Vakna av BTN3", "Clock Style": "Klockstil", "Time Zone": "Tidszon", "Twist Max Y": "Vridning Max Y", - "Stay Connectable": "Håll dig tillgänglig", - "This will remove everything": "Detta kommer att ta bort allt", + "Stay Connectable": "Håll anslutningsbar", + "This will remove everything": "Detta kommer ta bort allt", "Turn Off": "Stäng av", "Connectable": "Anslutningsbar", - "Flattening battery - this can take hours.\nLong-press button to cancel": "Platta batterier - detta kan ta flera timmar.\nTryck länge på knappen för att avbryta", - "Reset to Defaults": "Återställ till standardvärden", + "Flattening battery - this can take hours.\nLong-press button to cancel": "Töm batteri - detta kan ta flera timmar.\nTryck länge på knappen för att avbryta", + "Reset to Defaults": "Återställ standardvärden", "Utilities": "Verktyg", - "Flatten Battery": "Platta batterier", - "Debug Info": "Info om felsökning", - "Reset Settings": "Återställa inställningar", - "Wake on Twist": "Väckning på Twist", - "Compact Storage": "Kompakt förvaring", + "Flatten Battery": "Töm batteri", + "Debug Info": "Felsökningsinfo", + "Reset Settings": "Återställ inställningar", + "Wake on Twist": "Vakna av vridning", + "Compact Storage": "Komprimera lagring", "Log": "Logg", "Rewrite Settings": "Omskrivning av inställningar", - "Compacting...\nTakes approx\n1 minute": "Komprimering...\nTar ca.\n1 minut", + "Compacting...\nTakes approx\n1 minute": "Komprimerar...\nTar ca.\n1 minut", "Storage": "Lagring", "Second": "Andra", "App Settings": "App-inställningar", "Invalid settings": "Ogiltiga inställningar", - "Minute": "Protokoll", - "Sleep Phase Alarm": "Larm om sömnfas", + "Minute": "Minut", + "Sleep Phase Alarm": "Sömnfaslarm", "No app has settings": "Ingen app har inställningar", "Hour": "Timme", "No Clocks Found": "Inga klockor hittades", @@ -124,16 +124,16 @@ "TIMER": "TIMER", "on": "på", "OFF": "OFF", - "Side": "Sidan", + "Side": "Sida", "Sort Order": "Sortering", "Left": "Vänster", "Right": "Höger", "Reset All": "Återställ alla", - "Widgets": "Widgets", + "Widgets": "Widgetar", "goal": "mål", "Vibrate": "Vibrera", "Message": "Meddelande", - "Beep": "Piper", + "Beep": "Pip", "Disable": "Inaktivera", "Select Clock": "Välj klocka", "Locale": "Lokalisering", @@ -145,14 +145,14 @@ "Timer": "Timer", "BACK": "TILLBAKA", "Error in settings": "Fel i inställningarna", - "Whitelist": "Whitelist", - "ALARM": "ALARM", + "Whitelist": "Vitlista", + "ALARM": "LARM", "Hide": "Dölj", "Connected": "Ansluten", "Show": "Visa", "On": "På", "Ok": "Ok", - "No": "Ingen", + "No": "Nej", "Settings": "Inställningar", "steps": "steg", "back": "tillbaka", @@ -162,7 +162,7 @@ "Loading": "Laddar", "Music": "Musik", "color": "färg", - "off": "off", + "off": "av", "Off": "Av", "Theme": "Tema", "one": "ett", @@ -197,4 +197,4 @@ "ten to *$2": "tio i *$2", "five to *$2": "fem i *$2" } -} \ No newline at end of file +} diff --git a/loader.js b/loader.js index 251f94ff1..a6e51192e 100644 --- a/loader.js +++ b/loader.js @@ -16,7 +16,7 @@ if (window.location.host=="banglejs.com") { 'This is not the official Bangle.js App Loader - you can try the Official Version here.'; } -var RECOMMENDED_VERSION = "2v15"; +var RECOMMENDED_VERSION = "2v16"; // could check http://www.espruino.com/json/BANGLEJS.json for this // We're only interested in Bangles @@ -49,7 +49,7 @@ function onFoundDeviceInfo(deviceId, deviceVersion) { if (deviceId != "BANGLEJS" && deviceId != "BANGLEJS2") { showToast(`You're using ${deviceId}, not a Bangle.js. Did you want espruino.com/apps instead?` ,"warning", 20000); } else if (versionLess(deviceVersion, RECOMMENDED_VERSION)) { - showToast(`You're using an old Bangle.js firmware (${deviceVersion}) and ${RECOMMENDED_VERSION} is available (see changes). You can update ${fwExtraText}with the instructions here` ,"warning", 20000); + showToast(`You're using an old Bangle.js firmware (${deviceVersion}) and ${RECOMMENDED_VERSION} is available (see changes). You can update ${fwExtraText}with the instructions here` ,"warning", 20000); } // check against features shown? filterAppsForDevice(deviceId); diff --git a/modules/ClockFace.js b/modules/ClockFace.js index c12360626..bf64d418a 100644 --- a/modules/ClockFace.js +++ b/modules/ClockFace.js @@ -9,7 +9,7 @@ function ClockFace(options) { if (![ "precision", "init", "draw", "update", - "pause", "resume", + "pause", "resume", "remove", "up", "down", "upDown", "settingsFile", ].includes(k)) throw `Invalid ClockFace option: ${k}`; @@ -27,6 +27,7 @@ function ClockFace(options) { if (options.init) this.init = options.init; if (options.pause) this._pause = options.pause; if (options.resume) this._resume = options.resume; + if (options.remove) this._remove = options.remove; if ((options.up || options.down) && options.upDown) throw "ClockFace up/down and upDown cannot be used together"; if (options.up || options.down) this._upDown = (dir) => { if (dir<0 && options.up) options.up.apply(this); @@ -44,12 +45,20 @@ function ClockFace(options) { ["showDate", "loadWidgets"].forEach(k => { if (this[k]===undefined) this[k] = true; }); + let s = require("Storage").readJSON("setting.json",1)||{}; + if ((global.__FILE__===undefined || global.__FILE__===s.clock) + && s.clockHasWidgets!==this.loadWidgets) { + // save whether we can Fast Load + s.clockHasWidgets = this.loadWidgets; + require("Storage").writeJSON("setting.json", s); + } // use global 24/12-hour setting if not set by clock-settings - if (!('is12Hour' in this)) this.is12Hour = !!(require("Storage").readJSON("setting.json", true) || {})["12hour"]; + if (!('is12Hour' in this)) this.is12Hour = !!(s["12hour"]); } ClockFace.prototype.tick = function() { "ram" + if (this._removed) return; const time = new Date(); const now = { d: `${time.getFullYear()}-${time.getMonth()}-${time.getDate()}`, @@ -85,16 +94,27 @@ ClockFace.prototype.start = function() { Bangle.CLOCK = 1; if (this.loadWidgets) Bangle.loadWidgets(); if (this.init) this.init.apply(this); - if (this._upDown) Bangle.setUI("clockupdown", d=>this._upDown.apply(this,[d])); - else Bangle.setUI("clock"); + const uiRemove = this._remove ? () => this.remove() : undefined; + if (this._upDown) { + Bangle.setUI({ + mode: "clockupdown", + remove: uiRemove, + }, d => this._upDown.apply(this, [d])); + } else { + Bangle.setUI({ + mode: "clock", + remove: uiRemove, + }); + } delete this._last; this.paused = false; this.tick(); - Bangle.on("lcdPower", on => { + this._onLcd = on => { if (on) this.resume(); else this.pause(); - }); + }; + Bangle.on("lcdPower", this._onLcd); }; ClockFace.prototype.pause = function() { @@ -111,6 +131,12 @@ ClockFace.prototype.resume = function() { if (this._resume) this._resume.apply(this); this.tick(); }; +ClockFace.prototype.remove = function() { + this._removed = true; + if (this._timeout) clearTimeout(this._timeout); + Bangle.removeListener("lcdPower", this._onLcd); + if (this._remove) this._remove.apply(this); +}; /** * Force a complete redraw diff --git a/modules/ClockFace.md b/modules/ClockFace.md index 85482213c..f123d38c0 100644 --- a/modules/ClockFace.md +++ b/modules/ClockFace.md @@ -77,6 +77,11 @@ var clock = new ClockFace({ resume: function() { // optional, called when the screen turns on // for example: turn GPS/compass back on }, + remove: function() { // optional, used for Fast Loading + // for example: remove listeners + // Fast Loading will not be used unless this function is present, + // if there is nothing to clean up, you can just leave it empty. + }, up: function() { // optional, up handler }, down: function() { // optional, down handler diff --git a/modules/Layout.js b/modules/Layout.js index fdcf0ceae..f8e27b66b 100644 --- a/modules/Layout.js +++ b/modules/Layout.js @@ -10,18 +10,20 @@ */ -function Layout(layout, options) { +function Layout(layout, options) { this._l = this.l = layout; // Do we have >1 physical buttons? - this.physBtns = (process.env.HWVERSION==2) ? 1 : 3; + this.options = options || {}; this.lazy = this.options.lazy || false; + this.physBtns = 1; let btnList; if (process.env.HWVERSION!=2) { + this.physBtns = 3; // no touchscreen, find any buttons in 'layout' btnList = []; - function btnRecurser(l) { + function btnRecurser(l) {"ram"; if (l.type=="btn") btnList.push(l); if (l.c) l.c.forEach(btnRecurser); } @@ -64,7 +66,7 @@ function Layout(layout, options) { this.setUI(); // recurse over layout doing some fixing up if needed var ll = this; - function recurser(l) { + function recurser(l) {"ram"; // add IDs if (l.id) ll[l.id] = l; // fix type up @@ -81,7 +83,7 @@ Layout.prototype.setUI = function() { let uiSet; if (this.buttons) { // multiple buttons so we'll jus use back/next/select - Bangle.setUI({mode:"updown", back:this.options.back}, dir=>{ + Bangle.setUI({mode:"updown", back:this.options.back, remove:this.options.remove}, dir=>{ var s = this.selectedButton, l=this.buttons.length; if (dir===undefined && this.buttons[s]) return this.buttons[s].cb(); @@ -98,7 +100,7 @@ Layout.prototype.setUI = function() { }); uiSet = true; } - if (this.options.back && !uiSet) Bangle.setUI({mode: "custom", back: this.options.back}); + if ((this.options.back || this.options.remove) && !uiSet) Bangle.setUI({mode: "custom", back: this.options.back, remove: this.options.remove}); // physical buttons -> actual applications if (this.b) { // Handler for button watch events @@ -153,26 +155,25 @@ Layout.prototype.render = function (l) { if (!l) l = this._l; if (this.updateNeeded) this.update(); - function render(l) {"ram" - g.reset(); - if (l.col!==undefined) g.setColor(l.col); - if (l.bgCol!==undefined) g.setBgColor(l.bgCol).clearRect(l.x,l.y,l.x+l.w-1,l.y+l.h-1); + var gfx=g; // define locally, because this is faster + function render(l) {"ram"; + gfx.reset(); + if (l.col!==undefined) gfx.setColor(l.col); + if (l.bgCol!==undefined) gfx.setBgColor(l.bgCol).clearRect(l.x,l.y,l.x+l.w-1,l.y+l.h-1); cb[l.type](l); } var cb = { "":function(){}, - "txt":function(l){ + "txt":function(l){"ram"; if (l.wrap) { - g.setFont(l.font).setFontAlign(0,-1); - var lines = g.wrapString(l.label, l.w); - var y = l.y+((l.h-g.getFontHeight()*lines.length)>>1); - // TODO: on 2v11 we can just render in a single drawString call - lines.forEach((line, i) => g.drawString(line, l.x+(l.w>>1), y+g.getFontHeight()*i)); + var lines = gfx.setFont(l.font).setFontAlign(0,-1).wrapString(l.label, l.w); + var y = l.y+((l.h-gfx.getFontHeight()*lines.length)>>1); + gfx.drawString(lines.join("\n"), l.x+(l.w>>1), y); } else { - g.setFont(l.font).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1)); + gfx.setFont(l.font).setFontAlign(0,0,l.r).drawString(l.label, l.x+(l.w>>1), l.y+(l.h>>1)); } - }, "btn":function(l){ + }, "btn":function(l){"ram"; var x = l.x+(0|l.pad), y = l.y+(0|l.pad), w = l.w-(l.pad<<1), h = l.h-(l.pad<<1); var poly = [ @@ -185,27 +186,26 @@ Layout.prototype.render = function (l) { x+4,y+h-1, 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); - if (l.col!==undefined) g.setColor(l.col); - if (l.src) g.setBgColor(bg).drawImage( + ], bg = l.selected?gfx.theme.bgH:gfx.theme.bg2; + gfx.setColor(bg).fillPoly(poly).setColor(l.selected ? gfx.theme.fgH : gfx.theme.fg2).drawPoly(poly); + if (l.col!==undefined) gfx.setColor(l.col); + if (l.src) gfx.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( + else gfx.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){"ram"; + gfx.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); }, - "v":function(l) { l.c.forEach(render); } + }, "custom":function(l){"ram"; l.render(l); + }, "h":function(l) { "ram"; l.c.forEach(render); + }, "v":function(l) { "ram"; l.c.forEach(render); } }; if (this.lazy) { @@ -218,7 +218,7 @@ Layout.prototype.render = function (l) { prepareLazyRender(l, rectsToClear, drawList, this.rects, null); for (var h in rectsToClear) delete this.rects[h]; var clearList = Object.keys(rectsToClear).map(k=>rectsToClear[k]).reverse(); // Rects are cleared in reverse order so that the original bg color is restored - for (var r of clearList) g.setBgColor(r.bg).clearRect.apply(g, r); + for (var r of clearList) gfx.setBgColor(r.bg).clearRect.apply(g, r); drawList.forEach(render); } else { // non-lazy render(l); @@ -231,9 +231,8 @@ Layout.prototype.forgetLazyState = function () { Layout.prototype.layout = function (l) { // l = current layout element - // exw,exh = extra width/height available - switch (l.type) { - case "h": { + var cb = { + "h" : function(l) {"ram"; var acc_w = l.x + (0|l.pad); var accfillx = 0; var fillx = l.c && l.c.reduce((a,l)=>a+(0|l.fillx),0); @@ -247,11 +246,10 @@ Layout.prototype.layout = function (l) { c.w = 0|(x - c.x); c.h = 0|(c.filly ? l.h - (l.pad<<1) : c._h); c.y = 0|(l.y + (0|l.pad) + ((1+(0|c.valign))*(l.h-(l.pad<<1)-c.h)>>1)); - if (c.c) this.layout(c); + if (c.c) cb[c.type](c); }); - break; - } - case "v": { + }, + "v" : function(l) {"ram"; var acc_h = l.y + (0|l.pad); var accfilly = 0; var filly = l.c && l.c.reduce((a,l)=>a+(0|l.filly),0); @@ -265,11 +263,11 @@ Layout.prototype.layout = function (l) { c.h = 0|(y - c.y); c.w = 0|(c.fillx ? l.w - (l.pad<<1) : c._w); c.x = 0|(l.x + (0|l.pad) + ((1+(0|c.halign))*(l.w-(l.pad<<1)-c.w)>>1)); - if (c.c) this.layout(c); + if (c.c) cb[c.type](c); }); - break; } - } + }; + cb[l.type](l); }; Layout.prototype.debug = function(l,c) { if (!l) l = this._l; @@ -282,50 +280,51 @@ Layout.prototype.debug = function(l,c) { }; Layout.prototype.update = function() { delete this.updateNeeded; + var gfx=g; // define locally, because this is faster // update sizes - function updateMin(l) {"ram" + function updateMin(l) {"ram"; cb[l.type](l); if (l.r&1) { // rotation var t = l._w;l._w=l._h;l._h=t; } - l._w = 0|Math.max(l._w + (l.pad<<1), 0|l.width); - l._h = 0|Math.max(l._h + (l.pad<<1), 0|l.height); + l._w = Math.max(l._w + (l.pad<<1), 0|l.width); + l._h = Math.max(l._h + (l.pad<<1), 0|l.height); } var cb = { - "txt" : function(l) { + "txt" : function(l) {"ram"; if (l.font.endsWith("%")) - l.font = "Vector"+Math.round(g.getHeight()*l.font.slice(0,-1)/100); + l.font = "Vector"+Math.round(gfx.getHeight()*l.font.slice(0,-1)/100); if (l.wrap) { l._h = l._w = 0; } else { var m = g.setFont(l.font).stringMetrics(l.label); l._w = m.width; l._h = m.height; } - }, "btn": function(l) { + }, "btn": function(l) {"ram"; 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.font = "Vector"+Math.round(gfx.getHeight()*l.font.slice(0,-1)/100); + var m = l.src?gfx.imageMetrics("function"==typeof l.src?l.src():l.src):gfx.setFont(l.font||"6x8:2").stringMetrics(l.label); l._h = 16 + m.height; l._w = 20 + m.width; - }, "img": function(l) { - var m = g.imageMetrics("function"==typeof l.src?l.src():l.src), s=l.scale||1; // get width and height out of image + }, "img": function(l) {"ram"; + var m = gfx.imageMetrics("function"==typeof l.src?l.src():l.src), s=l.scale||1; // get width and height out of image l._w = m.width*s; l._h = m.height*s; - }, "": function(l) { + }, "": function(l) {"ram"; // size should already be set up in width/height l._w = 0; l._h = 0; - }, "custom": function(l) { + }, "custom": function(l) {"ram"; // size should already be set up in width/height l._w = 0; l._h = 0; - }, "h": function(l) { + }, "h": function(l) {"ram"; l.c.forEach(updateMin); l._h = l.c.reduce((a,b)=>Math.max(a,b._h),0); l._w = l.c.reduce((a,b)=>a+b._w,0); if (l.fillx == null && l.c.some(c=>c.fillx)) l.fillx = 1; if (l.filly == null && l.c.some(c=>c.filly)) l.filly = 1; - }, "v": function(l) { + }, "v": function(l) {"ram"; l.c.forEach(updateMin); l._h = l.c.reduce((a,b)=>a+b._h,0); l._w = l.c.reduce((a,b)=>Math.max(a,b._w),0); @@ -336,6 +335,7 @@ Layout.prototype.update = function() { var l = this._l; updateMin(l); + delete cb; if (l.fillx || l.filly) { // fill all l.w = Bangle.appRect.w; l.h = Bangle.appRect.h; diff --git a/modules/Layout.md b/modules/Layout.md index 7a4177957..67db21858 100644 --- a/modules/Layout.md +++ b/modules/Layout.md @@ -59,6 +59,7 @@ layout.render(); - `cb` - a callback function - `cbl` - a callback function for long presses - `back` - a callback function, passed as `back` into Bangle.setUI (which usually adds an icon in the top left) +- `remove` - a cleanup function, passed as `remove` into Bangle.setUI (allows to cleanly remove the app from memory) 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. diff --git a/modules/Layout.min.js b/modules/Layout.min.js index b5a924358..19e60f7a0 100644 --- a/modules/Layout.min.js +++ b/modules/Layout.min.js @@ -1,14 +1,14 @@ -function p(b,k){function d(h){h.id&&(f[h.id]=h);h.type||(h.type="");h.c&&h.c.forEach(d)}this._l=this.l=b;this.physBtns=2==process.env.HWVERSION?1:3;this.options=k||{};this.lazy=this.options.lazy||!1;let a;if(2!=process.env.HWVERSION){a=[];function h(m){"btn"==m.type&&a.push(m);m.c&&m.c.forEach(h)}h(b);a.length&&(this.physBtns=0,this.buttons=a,this.selectedButton=-1)}if(this.options.btns)if(b=this.options.btns,this.physBtns>=b.length){this.b=b;let h=Math.floor(Bangle.appRect.h/ -this.physBtns);for(2b.length;)b.push({label:""});this._l.width=g.getWidth()-8;this._l={type:"h",filly:1,c:[this._l,{type:"v",pad:1,filly:1,c:b.map(m=>(m.type="txt",m.font="6x8",m.height=h,m.r=1,m))}]}}else this._l.width=g.getWidth()-32,this._l={type:"h",c:[this._l,{type:"v",c:b.map(h=>(h.type="btn",h.filly=1,h.width=32,h.r=1,h))}]},a&&a.push.apply(a,this._l.c[1].c);this.setUI();var f=this;d(this._l);this.updateNeeded=!0}function r(b, -k,d,a,f){var h=null==b.bgCol?f:g.toColor(b.bgCol);if(h!=f||"txt"==b.type||"btn"==b.type||"img"==b.type||"custom"==b.type){var m=b.c;delete b.c;var c="H"+E.CRC32(E.toJS(b));m&&(b.c=m);delete k[c]||((a[c]=[b.x,b.y,b.x+b.w-1,b.y+b.h-1]).bg=null==f?g.theme.bg:f,d&&(d.push(b),d=null))}if(b.c)for(var l of b.c)r(l,k,d,a,h)}p.prototype.setUI=function(){Bangle.setUI();let b;this.buttons&&(Bangle.setUI({mode:"updown",back:this.options.back},k=>{var d=this.selectedButton,a=this.buttons.length;if(void 0===k&& -this.buttons[d])return this.buttons[d].cb();this.buttons[d]&&(delete this.buttons[d].selected,this.render(this.buttons[d]));d=(d+a+k)%a;this.buttons[d]&&(this.buttons[d].selected=1,this.render(this.buttons[d]));this.selectedButton=d}),b=!0);this.options.back&&!b&&Bangle.setUI({mode:"custom",back:this.options.back});if(this.b){function k(d,a){.75=d.x&&a.y>=d.y&&a.x<=d.x+d.w&&a.y<=d.y+d.h&&(2==a.type&&d.cbl?d.cbl(a):d.cb&&d.cb(a));d.c&&d.c.forEach(f=>k(f,a))}Bangle.touchHandler=(d,a)=>k(this._l,a);Bangle.on("touch",Bangle.touchHandler)}}; -p.prototype.render=function(b){function k(c){"ram";g.reset();void 0!==c.col&&g.setColor(c.col);void 0!==c.bgCol&&g.setBgColor(c.bgCol).clearRect(c.x,c.y,c.x+c.w-1,c.y+c.h-1);d[c.type](c)}b||(b=this._l);this.updateNeeded&&this.update();var d={"":function(){},txt:function(c){if(c.wrap){g.setFont(c.font).setFontAlign(0,-1);var l=g.wrapString(c.label,c.w),e=c.y+(c.h-g.getFontHeight()*l.length>>1);l.forEach((n,q)=>g.drawString(n,c.x+(c.w>>1),e+g.getFontHeight()*q))}else g.setFont(c.font).setFontAlign(0, -0,c.r).drawString(c.label,c.x+(c.w>>1),c.y+(c.h>>1))},btn:function(c){var l=c.x+(0|c.pad),e=c.y+(0|c.pad),n=c.w-(c.pad<<1),q=c.h-(c.pad<<1);l=[l,e+4,l+4,e,l+n-5,e,l+n-1,e+4,l+n-1,e+q-5,l+n-5,e+q-1,l+4,e+q-1,l,e+q-5,l,e+4];e=c.selected?g.theme.bgH:g.theme.bg2;g.setColor(e).fillPoly(l).setColor(c.selected?g.theme.fgH:g.theme.fg2).drawPoly(l);void 0!==c.col&&g.setColor(c.col);c.src?g.setBgColor(e).drawImage("function"==typeof c.src?c.src():c.src,c.x+c.w/2,c.y+c.h/2,{scale:c.scale||void 0,rotate:.5*Math.PI* -(c.r||0)}):g.setFont(c.font||"6x8:2").setFontAlign(0,0,c.r).drawString(c.label,c.x+c.w/2,c.y+c.h/2)},img:function(c){g.drawImage("function"==typeof c.src?c.src():c.src,c.x+c.w/2,c.y+c.h/2,{scale:c.scale||void 0,rotate:.5*Math.PI*(c.r||0)})},custom:function(c){c.render(c)},h:function(c){c.c.forEach(k)},v:function(c){c.c.forEach(k)}};if(this.lazy){this.rects||(this.rects={});var a=this.rects.clone(),f=[];r(b,a,f,this.rects,null);for(var h in a)delete this.rects[h];b=Object.keys(a).map(c=>a[c]).reverse(); -for(var m of b)g.setBgColor(m.bg).clearRect.apply(g,m);f.forEach(k)}else k(b)};p.prototype.forgetLazyState=function(){this.rects={}};p.prototype.layout=function(b){switch(b.type){case "h":var k=b.x+(0|b.pad),d=0,a=b.c&&b.c.reduce((e,n)=>e+(0|n.fillx),0);a||(k+=b.w-b._w>>1,a=1);var f=k;b.c.forEach(e=>{e.x=0|f;k+=e._w;d+=0|e.fillx;f=k+Math.floor(d*(b.w-b._w)/a);e.w=0|f-e.x;e.h=0|(e.filly?b.h-(b.pad<<1):e._h);e.y=0|b.y+(0|b.pad)+((1+(0|e.valign))*(b.h-(b.pad<<1)-e.h)>>1);e.c&&this.layout(e)});break; -case "v":var h=b.y+(0|b.pad),m=0,c=b.c&&b.c.reduce((e,n)=>e+(0|n.filly),0);c||(h+=b.h-b._h>>1,c=1);var l=h;b.c.forEach(e=>{e.y=0|l;h+=e._h;m+=0|e.filly;l=h+Math.floor(m*(b.h-b._h)/c);e.h=0|l-e.y;e.w=0|(e.fillx?b.w-(b.pad<<1):e._w);e.x=0|b.x+(0|b.pad)+((1+(0|e.halign))*(b.w-(b.pad<<1)-e.w)>>1);e.c&&this.layout(e)})}};p.prototype.debug=function(b,k){b||(b=this._l);k=k||1;g.setColor(k&1,k&2,k&4).drawRect(b.x+k-1,b.y+k-1,b.x+b.w-k,b.y+b.h-k);b.pad&&g.drawRect(b.x+b.pad-1,b.y+b.pad-1,b.x+b.w-b.pad,b.y+ -b.h-b.pad);k++;b.c&&b.c.forEach(d=>this.debug(d,k))};p.prototype.update=function(){function b(a){"ram";k[a.type](a);if(a.r&1){var f=a._w;a._w=a._h;a._h=f}a._w=0|Math.max(a._w+(a.pad<<1),0|a.width);a._h=0|Math.max(a._h+(a.pad<<1),0|a.height)}delete this.updateNeeded;var k={txt:function(a){a.font.endsWith("%")&&(a.font="Vector"+Math.round(g.getHeight()*a.font.slice(0,-1)/100));if(a.wrap)a._h=a._w=0;else{var f=g.setFont(a.font).stringMetrics(a.label);a._w=f.width;a._h=f.height}},btn:function(a){a.font&& -a.font.endsWith("%")&&(a.font="Vector"+Math.round(g.getHeight()*a.font.slice(0,-1)/100));var f=a.src?g.imageMetrics("function"==typeof a.src?a.src():a.src):g.setFont(a.font||"6x8:2").stringMetrics(a.label);a._h=16+f.height;a._w=20+f.width},img:function(a){var f=g.imageMetrics("function"==typeof a.src?a.src():a.src),h=a.scale||1;a._w=f.width*h;a._h=f.height*h},"":function(a){a._w=0;a._h=0},custom:function(a){a._w=0;a._h=0},h:function(a){a.c.forEach(b);a._h=a.c.reduce((f,h)=>Math.max(f,h._h),0);a._w= -a.c.reduce((f,h)=>f+h._w,0);null==a.fillx&&a.c.some(f=>f.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(f=>f.filly)&&(a.filly=1)},v:function(a){a.c.forEach(b);a._h=a.c.reduce((f,h)=>f+h._h,0);a._w=a.c.reduce((f,h)=>Math.max(f,h._w),0);null==a.fillx&&a.c.some(f=>f.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(f=>f.filly)&&(a.filly=1)}},d=this._l;b(d);d.fillx||d.filly?(d.w=Bangle.appRect.w,d.h=Bangle.appRect.h,d.x=Bangle.appRect.x,d.y=Bangle.appRect.y):(d.w=d._w,d.h=d._h,d.x=Bangle.appRect.w-d.w>>1,d.y= -Bangle.appRect.y+(Bangle.appRect.h-d.h>>1));this.layout(d)};p.prototype.clear=function(b){b||(b=this._l);g.reset();void 0!==b.bgCol&&g.setBgColor(b.bgCol);g.clearRect(b.x,b.y,b.x+b.w-1,b.y+b.h-1)};exports=p \ No newline at end of file +function p(d,h){function b(e){"ram";e.id&&(a[e.id]=e);e.type||(e.type="");e.c&&e.c.forEach(b)}this._l=this.l=d;this.options=h||{};this.lazy=this.options.lazy||!1;this.physBtns=1;let f;if(2!=process.env.HWVERSION){this.physBtns=3;f=[];function e(l){"ram";"btn"==l.type&&f.push(l);l.c&&l.c.forEach(e)}e(d);f.length&&(this.physBtns=0,this.buttons=f,this.selectedButton=-1)}if(this.options.btns)if(d=this.options.btns,this.physBtns>=d.length){this.b=d;let e=Math.floor(Bangle.appRect.h/this.physBtns); +for(2d.length;)d.push({label:""});this._l.width=g.getWidth()-8;this._l={type:"h",filly:1,c:[this._l,{type:"v",pad:1,filly:1,c:d.map(l=>(l.type="txt",l.font="6x8",l.height=e,l.r=1,l))}]}}else this._l.width=g.getWidth()-32,this._l={type:"h",c:[this._l,{type:"v",c:d.map(e=>(e.type="btn",e.filly=1,e.width=32,e.r=1,e))}]},f&&f.push.apply(f,this._l.c[1].c);this.setUI();var a=this;b(this._l);this.updateNeeded=!0}function t(d,h,b,f,a){var e= +null==d.bgCol?a:g.toColor(d.bgCol);if(e!=a||"txt"==d.type||"btn"==d.type||"img"==d.type||"custom"==d.type){var l=d.c;delete d.c;var k="H"+E.CRC32(E.toJS(d));l&&(d.c=l);delete h[k]||((f[k]=[d.x,d.y,d.x+d.w-1,d.y+d.h-1]).bg=null==a?g.theme.bg:a,b&&(b.push(d),b=null))}if(d.c)for(var c of d.c)t(c,h,b,f,e)}p.prototype.setUI=function(){Bangle.setUI();let d;this.buttons&&(Bangle.setUI({mode:"updown",back:this.options.back,remove:this.options.remove},h=>{var b=this.selectedButton,f=this.buttons.length;if(void 0=== +h&&this.buttons[b])return this.buttons[b].cb();this.buttons[b]&&(delete this.buttons[b].selected,this.render(this.buttons[b]));b=(b+f+h)%f;this.buttons[b]&&(this.buttons[b].selected=1,this.render(this.buttons[b]));this.selectedButton=b}),d=!0);!this.options.back&&!this.options.remove||d||Bangle.setUI({mode:"custom",back:this.options.back,remove:this.options.remove});if(this.b){function h(b,f){.75=b.x&&f.y>=b.y&&f.x<=b.x+b.w&&f.y<=b.y+b.h&&(2==f.type&&b.cbl?b.cbl(f):b.cb&&b.cb(f));b.c&&b.c.forEach(a=>h(a,f))}Bangle.touchHandler=(b,f)=>h(this._l,f);Bangle.on("touch", +Bangle.touchHandler)}};p.prototype.render=function(d){function h(c){"ram";b.reset();void 0!==c.col&&b.setColor(c.col);void 0!==c.bgCol&&b.setBgColor(c.bgCol).clearRect(c.x,c.y,c.x+c.w-1,c.y+c.h-1);f[c.type](c)}d||(d=this._l);this.updateNeeded&&this.update();var b=g,f={"":function(){},txt:function(c){"ram";if(c.wrap){var m=b.setFont(c.font).setFontAlign(0,-1).wrapString(c.label,c.w),n=c.y+(c.h-b.getFontHeight()*m.length>>1);b.drawString(m.join("\n"),c.x+(c.w>>1),n)}else b.setFont(c.font).setFontAlign(0, +0,c.r).drawString(c.label,c.x+(c.w>>1),c.y+(c.h>>1))},btn:function(c){"ram";var m=c.x+(0|c.pad),n=c.y+(0|c.pad),q=c.w-(c.pad<<1),r=c.h-(c.pad<<1);m=[m,n+4,m+4,n,m+q-5,n,m+q-1,n+4,m+q-1,n+r-5,m+q-5,n+r-1,m+4,n+r-1,m,n+r-5,m,n+4];n=c.selected?b.theme.bgH:b.theme.bg2;b.setColor(n).fillPoly(m).setColor(c.selected?b.theme.fgH:b.theme.fg2).drawPoly(m);void 0!==c.col&&b.setColor(c.col);c.src?b.setBgColor(n).drawImage("function"==typeof c.src?c.src():c.src,c.x+c.w/2,c.y+c.h/2,{scale:c.scale||void 0,rotate:.5* +Math.PI*(c.r||0)}):b.setFont(c.font||"6x8:2").setFontAlign(0,0,c.r).drawString(c.label,c.x+c.w/2,c.y+c.h/2)},img:function(c){"ram";b.drawImage("function"==typeof c.src?c.src():c.src,c.x+c.w/2,c.y+c.h/2,{scale:c.scale||void 0,rotate:.5*Math.PI*(c.r||0)})},custom:function(c){"ram";c.render(c)},h:function(c){"ram";c.c.forEach(h)},v:function(c){"ram";c.c.forEach(h)}};if(this.lazy){this.rects||(this.rects={});var a=this.rects.clone(),e=[];t(d,a,e,this.rects,null);for(var l in a)delete this.rects[l];d= +Object.keys(a).map(c=>a[c]).reverse();for(var k of d)b.setBgColor(k.bg).clearRect.apply(g,k);e.forEach(h)}else h(d)};p.prototype.forgetLazyState=function(){this.rects={}};p.prototype.layout=function(d){var h={h:function(b){"ram";var f=b.x+(0|b.pad),a=0,e=b.c&&b.c.reduce((k,c)=>k+(0|c.fillx),0);e||(f+=b.w-b._w>>1,e=1);var l=f;b.c.forEach(k=>{k.x=0|l;f+=k._w;a+=0|k.fillx;l=f+Math.floor(a*(b.w-b._w)/e);k.w=0|l-k.x;k.h=0|(k.filly?b.h-(b.pad<<1):k._h);k.y=0|b.y+(0|b.pad)+((1+(0|k.valign))*(b.h-(b.pad<< +1)-k.h)>>1);if(k.c)h[k.type](k)})},v:function(b){"ram";var f=b.y+(0|b.pad),a=0,e=b.c&&b.c.reduce((k,c)=>k+(0|c.filly),0);e||(f+=b.h-b._h>>1,e=1);var l=f;b.c.forEach(k=>{k.y=0|l;f+=k._h;a+=0|k.filly;l=f+Math.floor(a*(b.h-b._h)/e);k.h=0|l-k.y;k.w=0|(k.fillx?b.w-(b.pad<<1):k._w);k.x=0|b.x+(0|b.pad)+((1+(0|k.halign))*(b.w-(b.pad<<1)-k.w)>>1);if(k.c)h[k.type](k)})}};h[d.type](d)};p.prototype.debug=function(d,h){d||(d=this._l);h=h||1;g.setColor(h&1,h&2,h&4).drawRect(d.x+h-1,d.y+h-1,d.x+d.w-h,d.y+d.h-h); +d.pad&&g.drawRect(d.x+d.pad-1,d.y+d.pad-1,d.x+d.w-d.pad,d.y+d.h-d.pad);h++;d.c&&d.c.forEach(b=>this.debug(b,h))};p.prototype.update=function(){function d(a){"ram";b[a.type](a);if(a.r&1){var e=a._w;a._w=a._h;a._h=e}a._w=Math.max(a._w+(a.pad<<1),0|a.width);a._h=Math.max(a._h+(a.pad<<1),0|a.height)}delete this.updateNeeded;var h=g,b={txt:function(a){"ram";a.font.endsWith("%")&&(a.font="Vector"+Math.round(h.getHeight()*a.font.slice(0,-1)/100));if(a.wrap)a._h=a._w=0;else{var e=g.setFont(a.font).stringMetrics(a.label); +a._w=e.width;a._h=e.height}},btn:function(a){"ram";a.font&&a.font.endsWith("%")&&(a.font="Vector"+Math.round(h.getHeight()*a.font.slice(0,-1)/100));var e=a.src?h.imageMetrics("function"==typeof a.src?a.src():a.src):h.setFont(a.font||"6x8:2").stringMetrics(a.label);a._h=16+e.height;a._w=20+e.width},img:function(a){"ram";var e=h.imageMetrics("function"==typeof a.src?a.src():a.src),l=a.scale||1;a._w=e.width*l;a._h=e.height*l},"":function(a){"ram";a._w=0;a._h=0},custom:function(a){"ram";a._w=0;a._h=0}, +h:function(a){"ram";a.c.forEach(d);a._h=a.c.reduce((e,l)=>Math.max(e,l._h),0);a._w=a.c.reduce((e,l)=>e+l._w,0);null==a.fillx&&a.c.some(e=>e.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(e=>e.filly)&&(a.filly=1)},v:function(a){"ram";a.c.forEach(d);a._h=a.c.reduce((e,l)=>e+l._h,0);a._w=a.c.reduce((e,l)=>Math.max(e,l._w),0);null==a.fillx&&a.c.some(e=>e.fillx)&&(a.fillx=1);null==a.filly&&a.c.some(e=>e.filly)&&(a.filly=1)}},f=this._l;d(f);delete b;f.fillx||f.filly?(f.w=Bangle.appRect.w,f.h=Bangle.appRect.h, +f.x=Bangle.appRect.x,f.y=Bangle.appRect.y):(f.w=f._w,f.h=f._h,f.x=Bangle.appRect.w-f.w>>1,f.y=Bangle.appRect.y+(Bangle.appRect.h-f.h>>1));this.layout(f)};p.prototype.clear=function(d){d||(d=this._l);g.reset();void 0!==d.bgCol&&g.setBgColor(d.bgCol);g.clearRect(d.x,d.y,d.x+d.w-1,d.y+d.h-1)};exports=p \ No newline at end of file diff --git a/modules/README.md b/modules/README.md index 3f8e90b06..fcb403bd5 100644 --- a/modules/README.md +++ b/modules/README.md @@ -18,28 +18,6 @@ so you may see the error "Module not found" in the IDE when sendin To fix this you have three options: -### Host your own App Loader and upload from that - -This is reasonably easy to set up, but it's more difficult to make changes and upload: - -* Follow the steps here to set up your own App Loader: https://www.espruino.com/Bangle.js+App+Loader -* Make changes to that repository -* Refresh and upload your app from the app loader (you can have the IDE connected - at the same time so you can see any error messages) - -### Upload the module to the Bangle's internal storage - -This allows you to develop both the app and module very quickly, but the app is -uploaded in a slightly different way to what you'd get when you use the App Loader -or the method below: - -* Load the module's source file in the Web IDE -* Click the down-arrow below the upload button, then `Storage` -* Click `New File`, type `your_module_name` as the name (with no `.js` extension), click `Ok` -* Now Click the `Upload` icon. - -You can now upload the app direct from the IDE. You can even leave a second Web IDE window open -(one for the app, one for the module) to allow you to change the module. ### Change the Web IDE search path to include Bangle.js modules @@ -56,3 +34,30 @@ The next time you upload your app, the module will automatically be included. **Note:** You can optionally use `https://raw.githubusercontent.com/espruino/BangleApps/master/modules|https://www.espruino.com/modules` as the module URL to pull in modules direct from the development app loader (which could be slightly newer than the ones on https://banglejs.com/apps) + + +### Host your own App Loader and upload from that + +This is reasonably easy to set up, but it's more difficult to make changes and upload: + +* Follow the steps here to set up your own App Loader: https://www.espruino.com/Bangle.js+App+Loader +* Make changes to that repository +* Refresh and upload your app from the app loader (you can have the IDE connected + at the same time so you can see any error messages) + + +### Upload the module to the Bangle's internal storage + +This allows you to develop both the app and module very quickly, but the app is +uploaded in a slightly different way to what you'd get when you use the App Loader +or the method below: + +* Load the module's source file in the Web IDE +* Click the down-arrow below the upload button, then `Storage` +* Click `New File`, type `your_module_name` as the name (with no `.js` extension), click `Ok` +* Now Click the `Upload` icon. + +You can now upload the app direct from the IDE. You can even leave a second Web IDE window open +(one for the app, one for the module) to allow you to change the module. + + diff --git a/modules/clock_info.js b/modules/clock_info.js index 4ce2a08fc..643a9f6f7 100644 --- a/modules/clock_info.js +++ b/modules/clock_info.js @@ -2,25 +2,29 @@ that can be scrolled through on the clock face. `load()` returns an array of menu objects, where each object contains a list of menu items: -* 'name' : text to display and identify menu object (e.g. weather) -* 'img' : a 24x24px image -* 'items' : menu items such as temperature, humidity, wind etc. +* `name` : text to display and identify menu object (e.g. weather) +* `img` : a 24x24px image +* `dynamic` : if `true`, items are not constant but are sorted (e.g. calendar events sorted by date) +* `items` : menu items such as temperature, humidity, wind etc. Note that each item is an object with: -* 'item.name' : friendly name to identify an item (e.g. temperature) -* 'item.hasRange' : if `true`, `.get` returns `v/min/max` values (for progress bar/guage) -* 'item.get' : function that resolves with: - { - 'text' : the text to display for this item - 'img' : a 24x24px image to display for this item - 'v' : (if hasRange==true) a numerical value - 'min','max' : (if hasRange==true) a minimum and maximum numerical value (if this were to be displayed as a guage) - } -* 'item.show' : called when item should be shown. Enables updates. Call BEFORE 'get' -* 'item.hide' : called when item should be hidden. Disables updates. -* .on('redraw', ...) : event that is called when 'get' should be called again (only after 'item.show') -* 'item.run' : (optional) called if the info screen is tapped - can perform some action. Return true if the caller should feedback the user. +* `item.name` : friendly name to identify an item (e.g. temperature) +* `item.hasRange` : if `true`, `.get` returns `v/min/max` values (for progress bar/guage) +* `item.get` : function that returns an object: + +{ + 'text' // the text to display for this item + 'short' : (optional) a shorter text to display for this item (at most 6 characters) + 'img' // optional: a 24x24px image to display for this item + 'v' // (if hasRange==true) a numerical value + 'min','max' // (if hasRange==true) a minimum and maximum numerical value (if this were to be displayed as a guage) +} + +* `item.show` : called when item should be shown. Enables updates. Call BEFORE 'get' +* `item.hide` : called when item should be hidden. Disables updates. +* `.on('redraw', ...)` : event that is called when 'get' should be called again (only after 'item.show') +* `item.run` : (optional) called if the info screen is tapped - can perform some action. Return true if the caller should feedback the user. See the bottom of this file for example usage... @@ -33,9 +37,11 @@ example.clkinfo.js : items: [ { name : "Item1", get : () => ({ text : "TextOfItem1", v : 10, min : 0, max : 100, - img : atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==") }), + img : atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA==") + }), show : () => {}, hide : () => {} + // run : () => {} optional (called when tapped) } ] }; @@ -43,15 +49,27 @@ example.clkinfo.js : */ +let storage = require("Storage"); +let stepGoal = undefined; +// Load step goal from health app and pedometer widget +let d = storage.readJSON("health.json", true) || {}; +stepGoal = d != undefined && d.settings != undefined ? d.settings.stepGoal : undefined; +if (stepGoal == undefined) { + d = storage.readJSON("wpedom.json", true) || {}; + stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000; +} exports.load = function() { // info used for drawing... - var hrm = "--"; + var hrm = 0; var alt = "--"; // callbacks (needed for easy removal of listeners) function batteryUpdateHandler() { bangleItems[0].emit("redraw"); } function stepUpdateHandler() { bangleItems[1].emit("redraw"); } - function hrmUpdateHandler() { bangleItems[2].emit("redraw"); } + function hrmUpdateHandler(e) { + if (e && e.confidence>60) hrm = Math.round(e.bpm); + bangleItems[2].emit("redraw"); + } function altUpdateHandler() { Bangle.getPressure().then(data=>{ if (!data) return; @@ -62,7 +80,7 @@ exports.load = function() { // actual menu var menu = [{ name: "Bangle", - img: atob("GBiBAf8B//4B//4B//4B//4A//x4//n+f/P/P+fPn+fPn+fP3+/Px+/Px+fn3+fzn+f/n/P/P/n+f/x4//4A//4B//4B//4B//8B/w=="), + img: atob("GBiBAAD+AAH+AAH+AAH+AAH/AAOHAAYBgAwAwBgwYBgwYBgwIBAwOBAwOBgYIBgMYBgAYAwAwAYBgAOHAAH/AAH+AAH+AAH+AAD+AA=="), items: [ { name : "Battery", hasRange : true, @@ -76,7 +94,7 @@ exports.load = function() { { name : "Steps", hasRange : true, get : () => { let v = Bangle.getHealthStatus("day").steps; return { - text : v, v : v, min : 0, max : 10000, // TODO: do we have a target step amount anywhere? + text : v, v : v, min : 0, max : stepGoal, img : atob("GBiBAAcAAA+AAA/AAA/AAB/AAB/gAA/g4A/h8A/j8A/D8A/D+AfH+AAH8AHn8APj8APj8AHj4AHg4AADAAAHwAAHwAAHgAAHgAADAA==") }}, show : function() { Bangle.on("step", stepUpdateHandler); stepUpdateHandler(); }, @@ -84,12 +102,12 @@ exports.load = function() { }, { name : "HRM", hasRange : true, - get : () => { let v = Math.round(Bangle.getHealthStatus("last").bpm); return { - text : v + " bpm", v : v, min : 40, max : 200, + get : () => { return { + text : (hrm||"--") + " bpm", v : hrm, min : 40, max : 200, img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAADAAADAAAHAAAHjAAHjgAPngH9n/n82/gA+AAA8AAA8AAAcAAAYAAAYAAAAAAAAAAAAAAAAAAAAAA==") }}, - show : function() { Bangle.setHRMPower(1,"clkinfo"); Bangle.on("HRM", hrmUpdateHandler); hrm = Math.round(Bangle.getHealthStatus("last").bpm); hrmUpdateHandler(); }, - hide : function() { Bangle.setHRMPower(0,"clkinfo"); Bangle.removeListener("HRM", hrmUpdateHandler); hrm = "--"; }, + show : function() { Bangle.setHRMPower(1,"clkinfo"); Bangle.on("HRM", hrmUpdateHandler); hrm = Math.round(Bangle.getHealthStatus().bpm||Bangle.getHealthStatus("last").bpm); hrmUpdateHandler(); }, + hide : function() { Bangle.setHRMPower(0,"clkinfo"); Bangle.removeListener("HRM", hrmUpdateHandler); hrm = 0; }, } ], }]; @@ -115,7 +133,7 @@ exports.load = function() { if(b) b.items = b.items.concat(a.items); else menu = menu.concat(a); } catch(e){ - console.log("Could not load clock info.") + console.log("Could not load clock info "+E.toJS(fn)) } }); @@ -128,14 +146,15 @@ Simply supply the menu data (from .load) and a function to draw the clock info. For example: -let clockInfoMenu = require("clock_info").addInteractive(require("clock_info").load(), { +let clockInfoItems = require("clock_info").load(); +let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x : 20, y: 20, w: 80, h:80, // dimensions of area used for clock_info draw : (itm, info, options) => { g.reset().clearRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1); if (options.focus) g.drawRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1); // show if focused var midx = options.x+options.w/2; - g.drawImage(info.img, midx-12,options.y+4); - g.setFont("6x8:2").setFontAlign(0,0).drawString(info.text, midx,options.y+36); + if (info.img) g.drawImage(info.img, midx-12,options.y+4); + g.setFont("6x8:2").setFontAlign(0,1).drawString(info.text, midx,options.y+44); } }); // then when clock 'unloads': @@ -148,12 +167,12 @@ and delete clockInfoMenu clockInfoMenu is the 'options' parameter, with the following added: -* 'index' : int - which instance number are we? Starts at 0 -* 'menuA' : int - index in 'menu' of showing clockInfo item -* 'menuB' : int - index in 'menu[menuA].items' of showing clockInfo item -* 'remove' : function - remove this clockInfo item -* 'redraw' : function - force a redraw -* 'focus' : function - bool to show if menu is focused or not +* `index` : int - which instance number are we? Starts at 0 +* `menuA` : int - index in 'menu' of showing clockInfo item +* `menuB` : int - index in 'menu[menuA].items' of showing clockInfo item +* `remove` : function - remove this clockInfo item +* `redraw` : function - force a redraw +* `focus` : function - bool to show if menu is focused or not You can have more than one clock_info at once as well, sfor instance: @@ -161,8 +180,8 @@ let clockInfoDraw = (itm, info, options) => { g.reset().setBgColor(options.bg).setColor(options.fg).clearRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1); if (options.focus) g.drawRect(options.x, options.y, options.x+options.w-2, options.y+options.h-1) var midx = options.x+options.w/2; - g.drawImage(info.img, midx-12,options.y); - g.setFont("6x15").setFontAlign(0,-1).drawString(info.text, midx,options.y+26); + if (info.img) g.drawImage(info.img, midx-12,options.y); + g.setFont("6x15").setFontAlign(0,1).drawString(info.text, midx,options.y+41); }; let clockInfoItems = require("clock_info").load(); let clockInfoMenu = require("clock_info").addInteractive(clockInfoItems, { x:126, y:24, w:50, h:40, draw : clockInfoDraw, bg : g.theme.bg, fg : g.theme.fg }); @@ -220,10 +239,15 @@ exports.addInteractive = function(menu, options) { } else if (lr) { if (menu.length==1) return; // 1 item - can't move oldMenuItem = menu[options.menuA].items[options.menuB]; - options.menuA += ud; - if (options.menuA<0) options.menuA = menu.length-1; - if (options.menuA>=menu.length) options.menuA = 0; - options.menuB = 0; + do { + options.menuA += lr; + if (options.menuA<0) options.menuA = menu.length-1; + if (options.menuA>=menu.length) options.menuA = 0; + options.menuB = 0; + //get the next one if the menu is empty + //can happen for dynamic ones (alarms, events) + //in the worst case we come back to 0 + } while(menu[options.menuA].items.length==0); } if (oldMenuItem) { menuHideItem(oldMenuItem); diff --git a/modules/graphics_utils.js b/modules/graphics_utils.js new file mode 100644 index 000000000..5c08188bc --- /dev/null +++ b/modules/graphics_utils.js @@ -0,0 +1,35 @@ +// draw an arc between radii minR and maxR, and between angles minAngle and maxAngle centered at X,Y. All angles are radians. +exports.fillArc = function(graphics, X, Y, minR, maxR, minAngle, maxAngle, stepAngle) { + var step = stepAngle || 0.2; + var angle = minAngle; + var inside = []; + var outside = []; + var c, s; + while (angle < maxAngle) { + c = Math.cos(angle); + s = Math.sin(angle); + inside.push(X+c*minR); // x + inside.push(Y+s*minR); // y + // outside coordinates are built up in reverse order + outside.unshift(Y+s*maxR); // y + outside.unshift(X+c*maxR); // x + angle += step; + } + c = Math.cos(maxAngle); + s = Math.sin(maxAngle); + inside.push(X+c*minR); + inside.push(Y+s*minR); + outside.unshift(Y+s*maxR); + outside.unshift(X+c*maxR); + + var vertices = inside.concat(outside); + graphics.fillPoly(vertices, true); +} + +exports.degreesToRadians = function(degrees){ + return Math.PI/180 * degrees; +} + +exports.radiansToDegrees = function(radians){ + return 180/Math.PI * degrees; +} \ No newline at end of file diff --git a/apps/sunclock/suncalc.js b/modules/suncalc.js similarity index 75% rename from apps/sunclock/suncalc.js rename to modules/suncalc.js index b1af0a0d9..fe17148e1 100644 --- a/apps/sunclock/suncalc.js +++ b/modules/suncalc.js @@ -1,17 +1,34 @@ -/* Module suncalc.js +/* (c) 2011-2015, Vladimir Agafonkin SunCalc is a JavaScript library for calculating sun/moon position and light phases. https://github.com/mourner/suncalc -PB: Usage: -E.setTimeZone(2); // 1 = MEZ, 2 = MESZ -SunCalc = require("suncalc.js"); -pos = SunCalc.getPosition(Date.now(), 53.3, 10.1); -times = SunCalc.getTimes(Date.now(), 53.3, 10.1); -rise = times.sunrise; // Date object -rise_str = rise.getHours() + ':' + rise.getMinutes(); //hh:mm +Copyright (c) 2014, Vladimir Agafonkin +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are +permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of + conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list + of conditions and the following disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ -var exports={}; + +(function () { 'use strict'; // shortcuts for easier to read formulas @@ -26,6 +43,7 @@ var PI = Math.PI, // sun calculations are based on http://aa.quae.nl/en/reken/zonpositie.html formulas + // date/time constants and conversions var dayMs = 1000 * 60 * 60 * 24, @@ -33,7 +51,7 @@ var dayMs = 1000 * 60 * 60 * 24, J2000 = 2451545; function toJulian(date) { return date.valueOf() / dayMs - 0.5 + J1970; } -function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); } // PB: onece removed + 0.5; included it again 4 Jan 2021 +function fromJulian(j) { return new Date((j + 0.5 - J1970) * dayMs); } function toDays(date) { return toJulian(date) - J2000; } @@ -81,9 +99,13 @@ function sunCoords(d) { }; } + +var SunCalc = {}; + + // calculates sun position for a given date and latitude/longitude -exports.getPosition = function (date, lat, lng) { +SunCalc.getPosition = function (date, lat, lng) { var lw = rad * -lng, phi = rad * lat, @@ -93,19 +115,32 @@ exports.getPosition = function (date, lat, lng) { H = siderealTime(d, lw) - c.ra; return { - azimuth: Math.round((azimuth(H, phi, c.dec) / rad + 180) % 360), // PB: converted to deg - altitude: Math.round( altitude(H, phi, c.dec) / rad) // PB: converted to deg + azimuth: azimuth(H, phi, c.dec), + altitude: altitude(H, phi, c.dec) }; }; // sun times configuration (angle, morning name, evening name) -var times = [ - [-0.833, 'sunrise', 'sunset' ] +var times = SunCalc.times = [ + [-0.833, 'sunrise', 'sunset' ], + [ -0.3, 'sunriseEnd', 'sunsetStart' ], + [ -6, 'dawn', 'dusk' ], + [ -12, 'nauticalDawn', 'nauticalDusk'], + [ -18, 'nightEnd', 'night' ], + [ 6, 'goldenHourEnd', 'goldenHour' ] ]; +// adds a custom time to the times config + +SunCalc.addTime = function (angle, riseName, setName) { + times.push([angle, riseName, setName]); +}; + + // calculations for sun times + var J0 = 0.0009; function julianCycle(d, lw) { return Math.round(d - J0 - lw / (2 * PI)); } @@ -128,7 +163,7 @@ function getSetJ(h, lw, phi, dec, n, M, L) { // calculates sun times for a given date, latitude/longitude, and, optionally, // the observer height (in meters) relative to the horizon -exports.getTimes = function (date, lat, lng, height) { +SunCalc.getTimes = function (date, lat, lng, height) { height = height || 0; @@ -189,7 +224,7 @@ function moonCoords(d) { // geocentric ecliptic coordinates of the moon }; } -getMoonPosition = function (date, lat, lng) { +SunCalc.getMoonPosition = function (date, lat, lng) { var lw = rad * -lng, phi = rad * lat, @@ -216,7 +251,7 @@ getMoonPosition = function (date, lat, lng) { // based on http://idlastro.gsfc.nasa.gov/ftp/pro/astro/mphase.pro formulas and // Chapter 48 of "Astronomical Algorithms" 2nd edition by Jean Meeus (Willmann-Bell, Richmond) 1998. -getMoonIllumination = function (date) { +SunCalc.getMoonIllumination = function (date) { var d = toDays(date || new Date()), s = sunCoords(d), @@ -243,8 +278,8 @@ function hoursLater(date, h) { // calculations for moon rise/set times are based on http://www.stargazing.net/kepler/moonrise.html article -getMoonTimes = function (date, lat, lng, inUTC) { - var t = new Date(date); +SunCalc.getMoonTimes = function (date, lat, lng, inUTC) { + var t = typeof(date) === "object" ? date : new Date(date); if (inUTC) t.setUTCHours(0, 0, 0, 0); else t.setHours(0, 0, 0, 0); @@ -295,4 +330,12 @@ getMoonTimes = function (date, lat, lng, inUTC) { if (!rise && !set) result[ye > 0 ? 'alwaysUp' : 'alwaysDown'] = true; return result; -}; \ No newline at end of file +}; + + +// export as Node module / AMD module / browser variable +if (typeof exports === 'object' && typeof module !== 'undefined') module.exports = SunCalc; +else if (typeof define === 'function' && define.amd) define(SunCalc); +else window.SunCalc = SunCalc; + +}()); diff --git a/modules/widget_utils.js b/modules/widget_utils.js index 3440a01d2..154a95f68 100644 --- a/modules/widget_utils.js +++ b/modules/widget_utils.js @@ -29,6 +29,7 @@ exports.show = function() { /// Remove any intervals/handlers/etc that we might have added. Does NOT re-show widgets that were hidden exports.cleanup = function() { + delete exports.autohide; delete Bangle.appRect; if (exports.swipeHandler) { Bangle.removeListener("swipe", exports.swipeHandler); @@ -50,11 +51,19 @@ exports.cleanup = function() { /** Put widgets offscreen, and allow them to be swiped back onscreen with a downwards swipe. Use .show to undo. -Bangle.js 2 only at the moment. */ -exports.swipeOn = function() { +First parameter controls automatic hiding time, 0 equals not hiding at all. +Default value is 2000ms until hiding. +Bangle.js 2 only at the moment. On Bangle.js 1 widgets will be hidden permanently. + +Note: On Bangle.js 1 is is possible to draw widgets in an offscreen area of the LCD +and use Bangle.setLCDOffset. However we can't detect a downward swipe so how to +actually make this work needs some thought. +*/ +exports.swipeOn = function(autohide) { + if (process.env.HWVERSION!==2) return exports.hide(); exports.cleanup(); if (!global.WIDGETS) return; - + exports.autohide=autohide===undefined?2000:autohide; /* TODO: maybe when widgets are offscreen we don't even store them in an offscreen buffer? */ @@ -125,11 +134,13 @@ exports.swipeOn = function() { clearTimeout(exports.hideTimeout); delete exports.hideTimeout; } - if (ud>0 && offset<0) anim(4, function() { + let cb; + if (exports.autohide > 0) cb = function() { exports.hideTimeout = setTimeout(function() { anim(-4); - }, 2000); - }); + }, exports.autohide); + } + if (ud>0 && offset<0) anim(4, cb); if (ud<0 && offset>-24) anim(-4); };