diff --git a/apps/alarm/ChangeLog b/apps/alarm/ChangeLog index 398396c96..ab7db522c 100644 --- a/apps/alarm/ChangeLog +++ b/apps/alarm/ChangeLog @@ -41,3 +41,4 @@ 0.38: Display date in locale When switching 'repeat' from 'Workdays', 'Weekends' to 'Custom' preset Custom menu with previous selection Display alarm label in delete prompt +0.39: Dated event repeat option diff --git a/apps/alarm/README.md b/apps/alarm/README.md index 0298e0836..9da142dab 100644 --- a/apps/alarm/README.md +++ b/apps/alarm/README.md @@ -13,6 +13,7 @@ It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master - `Repeat` → Select when the alarm will fire. You can select a predefined option (_Once_, _Every Day_, _Workdays_ or _Weekends_ or you can configure the days freely) - `New Timer` → Configure a new timer (triggered based on amount of time elapsed in hours/minutes/seconds) - `New Event` → Configure a new event (triggered based on time and date) + - `Repeat` → Alarm can be be fired only once or repeated (every X number of _days_, _weeks_, _months_ or _years_) - `Advanced` - `Scheduler settings` → Open the [Scheduler](https://github.com/espruino/BangleApps/tree/master/apps/sched) settings page, see its [README](https://github.com/espruino/BangleApps/blob/master/apps/sched/README.md) for details - `Enable All` → Enable _all_ disabled alarms & timers diff --git a/apps/alarm/app.js b/apps/alarm/app.js index e97b61917..c0ae17f00 100644 --- a/apps/alarm/app.js +++ b/apps/alarm/app.js @@ -6,6 +6,8 @@ const firstDayOfWeek = (require("Storage").readJSON("setting.json", true) || {}) const WORKDAYS = 62 const WEEKEND = firstDayOfWeek ? 192 : 65; const EVERY_DAY = firstDayOfWeek ? 254 : 127; +const INTERVALS = ["day", "week", "month", "year"]; +const INTERVAL_LABELS = [/*LANG*/"Day", /*LANG*/"Week", /*LANG*/"Month", /*LANG*/"Year"]; const iconAlarmOn = "\0" + 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 = "\0" + (g.theme.dark @@ -44,8 +46,8 @@ function getLabel(e) { const dateStr = e.date && require("locale").date(new Date(e.date), 1); return (e.timer ? require("time_utils").formatDuration(e.timer) - : (dateStr ? `${dateStr} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeDOW(e)}` : "")) - ) + (e.msg ? " " + e.msg : ""); + : (dateStr ? `${dateStr}${e.rp?"*":""} ${require("time_utils").formatTime(e.t)}` : require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeRepeat(e)}` : "")) + ) + (e.msg ? ` ${e.msg}` : ""); } function showMainMenu() { @@ -152,8 +154,8 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) { onchange: v => alarm.on = v }, /*LANG*/"Repeat": { - value: decodeDOW(alarm), - onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, alarm.dow, (repeat, dow) => { + value: decodeRepeat(alarm), + onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, date || alarm.dow, (repeat, dow) => { alarm.rp = repeat; alarm.dow = dow; prepareAlarmForSave(alarm, alarmIndex, time, date, true); @@ -178,9 +180,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) { }; if (!keyboard) delete menu[/*LANG*/"Message"]; - if (alarm.date || withDate) { - delete menu[/*LANG*/"Repeat"]; - } else { + if (!alarm.date) { delete menu[/*LANG*/"Day"]; delete menu[/*LANG*/"Month"]; delete menu[/*LANG*/"Year"]; @@ -229,49 +229,77 @@ function saveAndReload() { alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow)); } -function decodeDOW(alarm) { +function decodeRepeat(alarm) { return alarm.rp - ? require("date_utils") - .dows(firstDayOfWeek, 2) - .map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_") - .join("") - .toLowerCase() + ? (alarm.date + ? `${alarm.rp.num}*${INTERVAL_LABELS[INTERVALS.indexOf(alarm.rp.interval)]}` + : require("date_utils") + .dows(firstDayOfWeek, 2) + .map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_") + .join("") + .toLowerCase()) : /*LANG*/"Once" } -function showEditRepeatMenu(repeat, dow, dowChangeCallback) { +function showEditRepeatMenu(repeat, day, dowChangeCallback) { var originalRepeat = repeat; - var originalDow = dow; - var isCustom = repeat && dow != WORKDAYS && dow != WEEKEND && dow != EVERY_DAY; + var dow; const menu = { "": { "title": /*LANG*/"Repeat Alarm" }, "< Back": () => dowChangeCallback(repeat, dow), - /*LANG*/"Once": { + /*LANG*/"Only Once": () => dowChangeCallback(false, EVERY_DAY) // The alarm will fire once. Internally it will be saved // as "fire every days" BUT the repeat flag is false so // we avoid messing up with the scheduler. - value: !repeat, - onchange: () => dowChangeCallback(false, EVERY_DAY) - }, - /*LANG*/"Workdays": { - value: repeat && dow == WORKDAYS, - onchange: () => dowChangeCallback(true, WORKDAYS) - }, - /*LANG*/"Weekends": { - value: repeat && dow == WEEKEND, - onchange: () => dowChangeCallback(true, WEEKEND) - }, - /*LANG*/"Every Day": { - value: repeat && dow == EVERY_DAY, - onchange: () => dowChangeCallback(true, EVERY_DAY) - }, - /*LANG*/"Custom": { - value: isCustom ? decodeDOW({ rp: true, dow: dow }) : false, - onchange: () => setTimeout(showCustomDaysMenu, 10, dow, dowChangeCallback, originalRepeat, originalDow) - } }; + let restOfMenu; + if (typeof day === "number") { + dow = day; + var originalDow = dow; + var isCustom = repeat && dow != WORKDAYS && dow != WEEKEND && dow != EVERY_DAY; + + restOfMenu = { + /*LANG*/"Workdays": { + value: repeat && dow == WORKDAYS, + onchange: () => dowChangeCallback(true, WORKDAYS) + }, + /*LANG*/"Weekends": { + value: repeat && dow == WEEKEND, + onchange: () => dowChangeCallback(true, WEEKEND) + }, + /*LANG*/"Every Day": { + value: repeat && dow == EVERY_DAY, + onchange: () => dowChangeCallback(true, EVERY_DAY) + }, + /*LANG*/"Custom": { + value: isCustom ? decodeRepeat({ rp: true, dow: dow }) : false, + onchange: () => setTimeout(showCustomDaysMenu, 10, dow, dowChangeCallback, originalRepeat, originalDow) + } + }; + } else { + var date = day; // eventually: detect day of date and configure a repeat e.g. 3rd Monday of Month + dow = EVERY_DAY; + repeat = repeat || {interval: "month", num: 1}; + + restOfMenu = { + /*LANG*/"Every": { + value: repeat.num, + min: 1, + onchange: v => repeat.num = v + }, + /*LANG*/"Interval": { + value: INTERVALS.indexOf(repeat.interval), + format: v => INTERVAL_LABELS[v], + min: 0, + max: INTERVALS.length - 1, + onchange: v => repeat.interval = INTERVALS[v] + } + }; + } + + Object.assign(menu, restOfMenu); E.showMenu(menu); } diff --git a/apps/alarm/metadata.json b/apps/alarm/metadata.json index 28d48daab..8522b07c0 100644 --- a/apps/alarm/metadata.json +++ b/apps/alarm/metadata.json @@ -2,7 +2,7 @@ "id": "alarm", "name": "Alarms & Timers", "shortName": "Alarms", - "version": "0.38", + "version": "0.39", "description": "Set alarms and timers on your Bangle", "icon": "app.png", "tags": "tool,alarm", diff --git a/apps/altimeter/ChangeLog b/apps/altimeter/ChangeLog index 29388520e..8d21cf797 100644 --- a/apps/altimeter/ChangeLog +++ b/apps/altimeter/ChangeLog @@ -1,2 +1,3 @@ 0.01: New App! 0.02: Actually upload correct code +0.03: Display sea-level pressure, too, and allow calibration diff --git a/apps/altimeter/app.js b/apps/altimeter/app.js index cac4e80fd..de664af39 100644 --- a/apps/altimeter/app.js +++ b/apps/altimeter/app.js @@ -1,4 +1,4 @@ -Bangle.setBarometerPower(true, "app"); +Bangle.setBarometerPower(true, "altimeter"); g.clear(1); Bangle.loadWidgets(); @@ -10,21 +10,62 @@ var MEDIANLENGTH = 20; var avr = [], median; var value = 0; +function getStandardPressure(altitude) { + const P0 = 1013.25; // standard pressure at sea level in hPa + const T0 = 288.15; // standard temperature at sea level in K + const g0 = 9.80665; // standard gravitational acceleration in m/s^2 + const R = 8.31432; // gas constant in J/(mol*K) + const M = 0.0289644; // molar mass of air in kg/mol + const L = -0.0065; // temperature lapse rate in K/m + + const temperature = T0 + L * altitude; // temperature at the given altitude + const pressure = P0 * Math.pow((temperature / T0), (-g0 * M) / (R * L)); // pressure at the given altitude + + return pressure; +} + +function convertToSeaLevelPressure(pressure, altitude) { + return 1013.25 * (pressure / getStandardPressure(altitude)); +} + Bangle.on('pressure', function(e) { while (avr.length>MEDIANLENGTH) avr.pop(); avr.unshift(e.altitude); median = avr.slice().sort(); - g.reset().clearRect(0,y-30,g.getWidth()-10,y+30); + g.reset().clearRect(0,y-30,g.getWidth()-10,R.h); if (median.length>10) { var mid = median.length>>1; value = E.sum(median.slice(mid-4,mid+5)) / 9; - g.setFont("Vector",50).setFontAlign(0,0).drawString((value-zero).toFixed(1), g.getWidth()/2, y); + t = value-zero; + if ((t > -100) && (t < 1000)) + t = t.toFixed(1); + else + t = t.toFixed(0); + g.setFont("Vector",50).setFontAlign(0,0).drawString(t, g.getWidth()/2, y); + sea = convertToSeaLevelPressure(e.pressure, value-zero); + t = sea.toFixed(1) + " " + e.temperature.toFixed(1); + if (0) { + print("alt raw:", value.toFixed(1)); + print("temperature:", e.temperature); + print("pressure:", e.pressure); + print("sea pressure:", sea); + print("std pressure:", getStandardPressure(value-zero)); + } + g.setFont("Vector",25).setFontAlign(-1,0).drawString(t, + 10, R.y+R.h - 35); } }); +print(g.getFonts()); g.reset(); -g.setFont("6x8").setFontAlign(0,0).drawString(/*LANG*/"ALTITUDE (m)", g.getWidth()/2, y-40); +g.setFont("Vector:15"); +g.setFontAlign(0,0); +g.drawString(/*LANG*/"ALTITUDE (m)", g.getWidth()/2, y-40); +g.drawString(/*LANG*/"SEA L (hPa) TEMP (C)", g.getWidth()/2, y+62); +g.flip(); g.setFont("6x8").setFontAlign(0,0,3).drawString(/*LANG*/"ZERO", g.getWidth()-5, g.getHeight()/2); -setWatch(function() { - zero = value; -}, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat:true}); +Bangle.setUI("updown", btn=> { + if (!btn) zero=value; + if (btn<0) zero-=5; + if (btn>0) zero+=5; +}); diff --git a/apps/altimeter/metadata.json b/apps/altimeter/metadata.json index 8bdbf3022..8bf3772ed 100644 --- a/apps/altimeter/metadata.json +++ b/apps/altimeter/metadata.json @@ -1,6 +1,6 @@ { "id": "altimeter", "name": "Altimeter", - "version":"0.02", + "version":"0.03", "description": "Simple altimeter that can display height changed using Bangle.js 2's built in pressure sensor.", "icon": "app.png", "tags": "tool,outdoors", diff --git a/apps/boot/ChangeLog b/apps/boot/ChangeLog index a63a54eaf..82e55fa91 100644 --- a/apps/boot/ChangeLog +++ b/apps/boot/ChangeLog @@ -65,3 +65,4 @@ Only add boot info comments if settings.bootDebug was set If settings.bootDebug is set, output timing for each section of .boot0 0.56: Settings.log = 0,1,2,3 for off,display, log, both +0.57: Handle the whitelist being disabled diff --git a/apps/boot/bootupdate.js b/apps/boot/bootupdate.js index d929b26a0..626171490 100644 --- a/apps/boot/bootupdate.js +++ b/apps/boot/bootupdate.js @@ -79,7 +79,7 @@ if (global.save) boot += `global.save = function() { throw new Error("You can't if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`; if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`; if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`; -if (s.whitelist) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`; +if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`; if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation // ================================================== FIXING OLDER FIRMWARES if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted. diff --git a/apps/boot/metadata.json b/apps/boot/metadata.json index 9f64b672b..c652f6136 100644 --- a/apps/boot/metadata.json +++ b/apps/boot/metadata.json @@ -1,7 +1,7 @@ { "id": "boot", "name": "Bootloader", - "version": "0.56", + "version": "0.57", "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/bwclklite/ChangeLog b/apps/bwclklite/ChangeLog new file mode 100644 index 000000000..c728997da --- /dev/null +++ b/apps/bwclklite/ChangeLog @@ -0,0 +1,36 @@ +0.01: New App. +0.02: Use build in function for steps and other improvements. +0.03: Adapt colors based on the theme of the user. +0.04: Steps can be hidden now such that the time is even larger. +0.05: Included icons for information. +0.06: Design and usability improvements. +0.07: Improved positioning. +0.08: Select the color of widgets correctly. Additional settings to hide colon. +0.09: Larger font size if colon is hidden to improve readability further. +0.10: HomeAssistant integration if HomeAssistant is installed. +0.11: Performance improvements. +0.12: Implements a 2D menu. +0.13: Clicks < 24px are for widgets, if fullscreen mode is disabled. +0.14: Adds humidity to weather data. +0.15: Added option for a dynamic mode to show widgets only if unlocked. +0.16: You can now show your agenda if your calendar is synced with Gadgetbridge. +0.17: Fix - Step count was no more shown in the menu. +0.18: Set timer for an agenda entry by simply clicking in the middle of the screen. Only one timer can be set. +0.19: Fix - Compatibility with "Digital clock widget" +0.20: Better handling of async data such as getPressure. +0.21: On the default menu the week of year can be shown. +0.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. +0.24: Update clock_info to avoid a redraw +0.25: Use Bangle.setUI({remove:...}) to allow loading the launcher without a full reset on fw2v16. + ClockInfo Fix: Use .get instead of .show as .show is not implemented for weather etc. +0.26: Use clkinfo.addInteractive instead of a custom implementation +0.27: Clean out some leftovers in the remove function after switching to +clkinfo.addInteractive that would cause ReferenceError. +0.28: Option to show (1) time only and (2) week of year. +0.29: use setItem of clockInfoMenu to change the active item +0.30: Use widget_utils +0.31: Use clock_info module as an app +0.32: Diverge from BW Clock. Change out the custom font for a standard bitmap one to speed up loading times. + Remove invertion of theme as this doesn'twork very well with fastloading. + Do an quick inital fillRect on theclock info area. diff --git a/apps/bwclklite/README.md b/apps/bwclklite/README.md new file mode 100644 index 000000000..1ad320894 --- /dev/null +++ b/apps/bwclklite/README.md @@ -0,0 +1,30 @@ +# BW Clock Lite +This is a fork of a very minimalistic clock. + +![](screenshot.png) + +## Features +The BW clock implements features that are exposed by other apps through the `clkinfo` module. +For example, if you install the Simple Timer app, this menu item will be shown if you first +touch the bottom of the screen and then swipe left/right to the Simple Timer menu. To select +sub-items simply swipe up/down. To run an action (e.g. add 5 min), simply select the clkinfo (border) and touch on the item again. See also the screenshot below: + +![](screenshot_3.png) + +Note: Check out the settings to change different themes. + +## Settings +- Screen: Normal (widgets shown), Dynamic (widgets shown if unlocked) or Full (widgets are hidden). +- Enable/disable lock icon in the settings. Useful if fullscreen mode is on. +- The colon (e.g. 7:35 = 735) can be hidden in the settings for an even larger time font to improve readability further. +- Your bangle uses the sys color settings so you can change the color too. + +## Thanks to +- Thanks to Gordon Williams not only for the great BangleJs, but specifically also for the implementation of `clkinfo` which simplified the BWClock a lot and moved complexety to the apps where it should be located. +- Icons created by Flaticon + +## Creator +[David Peer](https://github.com/peerdavid) + +## Contributors +thyttan diff --git a/apps/bwclklite/app-icon.js b/apps/bwclklite/app-icon.js new file mode 100644 index 000000000..1df0fa6a5 --- /dev/null +++ b/apps/bwclklite/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgIcah0EgEB/H8iFsAoOY4kMBYMDhmGgXkAoUGiWkAoQQBoAFCjgnCAoM4hgFDuEI+wpC8EKyg1C/0eAoMAsEAiQvBAAeAApQAB/4Ao+P4v/wn0P8Pgn/wnkH4Pjv/j/nn9PH//n/nj/IFF4F88AXBAoM88EcAoPHj//jlDAoOf/+Y+YFHjnnjAjBEIIjD+BHDO9IALA==")) diff --git a/apps/bwclklite/app.js b/apps/bwclklite/app.js new file mode 100644 index 000000000..1008eae9c --- /dev/null +++ b/apps/bwclklite/app.js @@ -0,0 +1,331 @@ +{ // must be inside our own scope here so that when we are unloaded everything disappears + +/************************************************ + * Includes + */ +const locale = require('locale'); +const storage = require('Storage'); +const clock_info = require("clock_info"); +const widget_utils = require("widget_utils"); + +/************************************************ + * Globals + */ +const SETTINGS_FILE = "bwclklite.setting.json"; +const W = g.getWidth(); +const H = g.getHeight(); + +/************************************************ + * Settings + */ +let settings = { + screen: "Normal", + showLock: true, + hideColon: false, + menuPosX: 0, + menuPosY: 0, +}; + +let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; +for (const key in saved_settings) { + settings[key] = saved_settings[key]; +} + +let isFullscreen = function() { + let s = settings.screen.toLowerCase(); + if(s == "dynamic"){ + return Bangle.isLocked(); + } else { + return s == "full"; + } +}; + +let getLineY = function(){ + return H/5*2 + (isFullscreen() ? 0 : 8); +}; + +/************************************************ + * Assets + */ +let imgLock = function() { + return { + width : 16, height : 16, bpp : 1, + transparent : 0, + buffer : E.toArrayBuffer(atob("A8AH4A5wDDAYGBgYP/w//D/8Pnw+fD58Pnw//D/8P/w=")) + }; +}; + + +/************************************************ + * Clock Info + */ +let clockInfoItems = clock_info.load(); + +// Add some custom clock-infos +let weekOfYear = function() { + let 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. + let 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); +}; + +clockInfoItems[0].items.unshift({ name : "weekofyear", + get : function() { return { text : "Week " + weekOfYear(), + img : null};}, + show : function() {}, + hide : function() {}, +}); + +// Empty for large time +clockInfoItems[0].items.unshift({ name : "nop", + get : function() { return { text : null, + img : null};}, + show : function() {}, + hide : function() {}, +}); + + + +let clockInfoMenu = clock_info.addInteractive(clockInfoItems, { + app: "bwclklite", + x : 0, + y: 135, + w: W, + h: H-135, + draw : (itm, info, options) => { + let hideClkInfo = info.text == null; + + g.setColor(g.theme.fg); + g.fillRect(options.x, options.y, options.x+options.w, options.y+options.h); + + g.setFontAlign(0,0); + g.setColor(g.theme.bg); + + if (options.focus){ + let y = hideClkInfo ? options.y+20 : options.y+2; + let h = hideClkInfo ? options.h-20 : options.h-2; + g.drawRect(options.x, y, options.x+options.w-2, y+h-1); // show if focused + g.drawRect(options.x+1, y+1, options.x+options.w-3, y+h-2); // show if focused + } + + // In case we hide the clkinfo, we show the time again as the time should + // be drawn larger. + if(hideClkInfo){ + drawTime(); + return; + } + + // Set text and font + let image = info.img; + let text = String(info.text); + if(text.split('\n').length > 1){ + g.setFont("6x8"); //g.setMiniFont(); + } else { + g.setFont("6x8:3"); //g.setSmallFont(); + } + + // Compute sizes + let strWidth = g.stringWidth(text); + let imgWidth = image == null ? 0 : 24; + let midx = options.x+options.w/2; + + // Draw + if (image) { + let scale = imgWidth / image.width; + g.drawImage(image, midx-parseInt(imgWidth*1.3/2)-parseInt(strWidth/2), options.y+6, {scale: scale}); + } + g.drawString(text, midx+parseInt(imgWidth*1.3/2), options.y+20); + + // In case we are in focus and the focus box changes (fullscreen yes/no) + // we draw the time again. Otherwise it could happen that a while line is + // not cleared correctly. + if(options.focus) drawTime(); + } +}); + + +/************************************************ + * Draw + */ +let draw = function() { + // Queue draw again + queueDraw(); + + // Draw clock + drawDate(); + drawTime(); + drawLock(); + drawWidgets(); +}; + + +let drawDate = function() { + // Draw background + let y = getLineY(); + g.reset().clearRect(0,0,W,y); + + // Draw date + y = parseInt(y/2)+4; + y += isFullscreen() ? 0 : 8; + let date = new Date(); + let dateStr = date.getDate(); + dateStr = ("0" + dateStr).substr(-2); + g.setFont("6x8:4"); //g.setMediumFont(); // Needed to compute the width correctly + let dateW = g.stringWidth(dateStr); + + g.setFont("6x8:3"); //g.setSmallFont(); + let dayStr = locale.dow(date, true); + let monthStr = locale.month(date, 1); + let dayW = Math.max(g.stringWidth(dayStr), g.stringWidth(monthStr)); + let fullDateW = dateW + 10 + dayW; + + g.setFontAlign(-1,0); + g.drawString(dayStr, W/2 - fullDateW/2 + 10 + dateW, y-12); + g.drawString(monthStr, W/2 - fullDateW/2 + 10 + dateW, y+11); + + g.setFont("6x8:4"); //g.setMediumFont(); + g.setColor(g.theme.fg); + g.drawString(dateStr, W/2 - fullDateW / 2, y+2); +}; + + +let drawTime = function() { + let hideClkInfo = clockInfoMenu.menuA == 0 && clockInfoMenu.menuB == 0; + + // Draw background + let y1 = getLineY(); + let y = y1; + let date = new Date(); + + let hours = String(date.getHours()); + let minutes = date.getMinutes(); + minutes = minutes < 10 ? String("0") + minutes : minutes; + let colon = settings.hideColon ? "" : ":"; + let timeStr = hours + colon + minutes; + + // Set y coordinates correctly + y += parseInt((H - y)/2) + 5; + + if (hideClkInfo){ + g.setFont("6x8:5"); //g.setLargeFont(); + } else { + y -= 15; + g.setFont("6x8:4"); //g.setMediumFont(); + } + + // Clear region and draw time + g.setColor(g.theme.fg); + g.fillRect(0,y1,W,y+20 + (hideClkInfo ? 1 : 0) + (isFullscreen() ? 3 : 0)); + + g.setColor(g.theme.bg); + g.setFontAlign(0,0); + g.drawString(timeStr, W/2, y); +}; + + +let drawLock = function() { + if(settings.showLock && Bangle.isLocked()){ + g.setColor(g.theme.fg); + g.drawImage(imgLock(), W-16, 2); + } +}; + + +let drawWidgets = function() { + if(isFullscreen()){ + widget_utils.hide(); + } else { + Bangle.drawWidgets(); + } +}; + + +/************************************************ + * Listener + */ +// timeout used to update every minute +let drawTimeout; + +// schedule a draw for the next minute +let queueDraw = function() { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + draw(); + }, 60000 - (Date.now() % 60000)); +}; + + +// Stop updates when LCD is off, restart when on +let lcdListenerBw = function(on) { + if (on) { + draw(); // draw immediately, queue redraw + } else { // stop draw timer + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + } +}; +Bangle.on('lcdPower', lcdListenerBw); + +let lockListenerBw = function(isLocked) { + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + + if(!isLocked && settings.screen.toLowerCase() == "dynamic"){ + // If we have to show the widgets again, we load it from our + // cache and not through Bangle.loadWidgets as its much faster! + widget_utils.show(); + } + + draw(); +}; +Bangle.on('lock', lockListenerBw); + +let charging = function(charging){ + // Jump to battery + clockInfoMenu.setItem(0, 2); + drawTime(); +}; +Bangle.on('charging', charging); + +let kill = function(){ + clockInfoMenu.remove(); + delete clockInfoMenu; +}; +E.on("kill", kill); + +/************************************************ + * Startup Clock + */ + +// Show launcher when middle button pressed +Bangle.setUI({ + mode : "clock", + remove : function() { + // Called to unload all of the clock app + Bangle.removeListener('lcdPower', lcdListenerBw); + Bangle.removeListener('lock', lockListenerBw); + Bangle.removeListener('charging', charging); + if (drawTimeout) clearTimeout(drawTimeout); + drawTimeout = undefined; + // save settings + kill(); + E.removeListener("kill", kill); + Bangle.removeListener('charging', charging); + widget_utils.show(); + } +}); + +// Load widgets and draw clock the first time +Bangle.loadWidgets(); + +// Draw first time +g.setColor(g.theme.fg).fillRect(0,135,W,H); // Otherwise this rect will wait for clock_info before updating +draw(); + +} // End of app scope diff --git a/apps/bwclklite/app.png b/apps/bwclklite/app.png new file mode 100644 index 000000000..5073f0ed0 Binary files /dev/null and b/apps/bwclklite/app.png differ diff --git a/apps/bwclklite/metadata.json b/apps/bwclklite/metadata.json new file mode 100644 index 000000000..bab852623 --- /dev/null +++ b/apps/bwclklite/metadata.json @@ -0,0 +1,43 @@ +{ + "id": "bwclklite", + "name": "BW Clock Lite", + "version": "0.32", + "description": "A very minimalistic clock. This version of BW Clock is quicker at the cost of the custom font.", + "readme": "README.md", + "icon": "app.png", + "screenshots": [ + { + "url": "screenshot.png" + }, + { + "url": "screenshot_2.png" + }, + { + "url": "screenshot_3.png" + } + ], + "type": "clock", + "tags": "clock,clkinfo", + "supports": [ + "BANGLEJS2" + ], + "dependencies": { + "clock_info": "module" + }, + "allow_emulator": true, + "storage": [ + { + "name": "bwclklite.app.js", + "url": "app.js" + }, + { + "name": "bwclklite.img", + "url": "app-icon.js", + "evaluate": true + }, + { + "name": "bwclklite.settings.js", + "url": "settings.js" + } + ] +} diff --git a/apps/bwclklite/screenshot.png b/apps/bwclklite/screenshot.png new file mode 100644 index 000000000..28983c9c4 Binary files /dev/null and b/apps/bwclklite/screenshot.png differ diff --git a/apps/bwclklite/screenshot_2.png b/apps/bwclklite/screenshot_2.png new file mode 100644 index 000000000..8d2f1717f Binary files /dev/null and b/apps/bwclklite/screenshot_2.png differ diff --git a/apps/bwclklite/screenshot_3.png b/apps/bwclklite/screenshot_3.png new file mode 100644 index 000000000..573675d28 Binary files /dev/null and b/apps/bwclklite/screenshot_3.png differ diff --git a/apps/bwclklite/settings.js b/apps/bwclklite/settings.js new file mode 100644 index 000000000..2d3916a3d --- /dev/null +++ b/apps/bwclklite/settings.js @@ -0,0 +1,50 @@ +(function(back) { + const SETTINGS_FILE = "bwclklite.setting.json"; + + // initialize with default settings... + const storage = require('Storage') + let settings = { + screen: "Normal", + showLock: true, + hideColon: false, + }; + let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; + for (const key in saved_settings) { + settings[key] = saved_settings[key] + } + + function save() { + storage.write(SETTINGS_FILE, settings) + } + + var screenOptions = ["Normal", "Dynamic", "Full"]; + E.showMenu({ + '': { 'title': 'BW Clock' }, + '< Back': back, + 'Screen': { + value: 0 | screenOptions.indexOf(settings.screen), + min: 0, max: 2, + format: v => screenOptions[v], + onchange: v => { + settings.screen = screenOptions[v]; + save(); + }, + }, + 'Show Lock': { + value: settings.showLock, + format: () => (settings.showLock ? 'Yes' : 'No'), + onchange: () => { + settings.showLock = !settings.showLock; + save(); + }, + }, + 'Hide Colon': { + value: settings.hideColon, + format: () => (settings.hideColon ? 'Yes' : 'No'), + onchange: () => { + settings.hideColon = !settings.hideColon; + save(); + }, + } + }); + }) diff --git a/apps/clockcal/ChangeLog b/apps/clockcal/ChangeLog index 4c8c13366..5657bf26d 100644 --- a/apps/clockcal/ChangeLog +++ b/apps/clockcal/ChangeLog @@ -5,3 +5,4 @@ 0.05: Improved colors (connected vs disconnected) 0.06: Tell clock widgets to hide. 0.07: Convert Yes/No On/Off in settings to checkboxes +0.08: Fixed typo in settings.js for DRAGDOWN to make option work \ No newline at end of file diff --git a/apps/clockcal/metadata.json b/apps/clockcal/metadata.json index 03f5c3df2..998115827 100644 --- a/apps/clockcal/metadata.json +++ b/apps/clockcal/metadata.json @@ -1,7 +1,7 @@ { "id": "clockcal", "name": "Clock & Calendar", - "version": "0.07", + "version": "0.08", "description": "Clock with Calendar", "readme":"README.md", "icon": "app.png", diff --git a/apps/clockcal/settings.js b/apps/clockcal/settings.js index 4d8be6fbd..a406f3cf7 100644 --- a/apps/clockcal/settings.js +++ b/apps/clockcal/settings.js @@ -93,7 +93,7 @@ value: actions.indexOf(settings.DRAGDOWN), format: v => actions[v], onchange: v => { - settings.DRGDOWN = actions[v]; + settings.DRAGDOWN = actions[v]; writeSettings(); } }, diff --git a/apps/draguboard/ChangeLog b/apps/draguboard/ChangeLog new file mode 100644 index 000000000..a228aab54 --- /dev/null +++ b/apps/draguboard/ChangeLog @@ -0,0 +1 @@ +0.01: New App based on dragboard, but with a U shaped drag area diff --git a/apps/draguboard/README.md b/apps/draguboard/README.md new file mode 100644 index 000000000..2386c7658 --- /dev/null +++ b/apps/draguboard/README.md @@ -0,0 +1,8 @@ +Swipe along the drag bars and release to select a letter, number or punctuation. + +Tap on left for backspace or right for space. + +Settings: +- ABC Color: color of the characters row +- Num Color: color of the digits and symbols row +- Highlight Color: color of the currently shown character diff --git a/apps/draguboard/app.png b/apps/draguboard/app.png new file mode 100644 index 000000000..ae7262b47 Binary files /dev/null and b/apps/draguboard/app.png differ diff --git a/apps/draguboard/lib.js b/apps/draguboard/lib.js new file mode 100644 index 000000000..258f8b02d --- /dev/null +++ b/apps/draguboard/lib.js @@ -0,0 +1,156 @@ +exports.input = function(options) { + options = options||{}; + var text = options.text; + if ("string"!=typeof text) text=""; + let settings = require('Storage').readJSON('draguboard.json',1)||{}; + + var R; + const paramToColor = (param) => g.toColor(`#${settings[param].toString(16).padStart(3,0)}`); + var BGCOLOR = g.theme.bg; + var HLCOLOR = settings.Highlight ? paramToColor("Highlight") : g.theme.fg; + var ABCCOLOR = settings.ABC ? paramToColor("ABC") : g.toColor(1,0,0);//'#FF0000'; + var NUMCOLOR = settings.Num ? paramToColor("Num") : g.toColor(0,1,0);//'#00FF00'; + var BIGFONT = '6x8:3'; + var SMALLFONT = '6x8:1'; + + var LEFT = "IJKLMNOPQ"; + var MIDDLE = "ABCDEFGH"; + var RIGHT = "RSTUVWXYZ"; + + var NUM = ' 1234567890!?,.-@'; + var rectHeight = 40; + var vLength = LEFT.length; + var MIDPADDING; + var NUMPADDING; + var showCharY; + var middleWidth; + var middleStart; + var topStart; + + function drawAbcRow() { + g.clear(); + try { // Draw widgets if they are present in the current app. + if (WIDGETS) Bangle.drawWidgets(); + } catch (_) {} + g.setColor(ABCCOLOR); + g.setFont('6x8:2x1'); + g.setFontAlign(-1, -1, 0); + g.drawString(RIGHT.split("").join("\n\n"), R.x2-28, topStart); + g.drawString(LEFT.split("").join("\n\n"), R.x+22, topStart); + g.setFont('6x8:1x2'); + var spaced = MIDDLE.split("").join(" "); + middleWidth = g.stringWidth(spaced); + middleStart = (R.x2-middleWidth)/2; + g.drawString(spaced, (R.x2-middleWidth)/2, (R.y2)/2); + g.fillRect(MIDPADDING, (R.y2)-26, (R.x2-MIDPADDING), (R.y2)); + // Draw left and right drag rectangles + g.fillRect(R.x, R.y, 12, R.y2); + g.fillRect(R.x2, R.y, R.x2-12, R.y2); + } + + function drawNumRow() { + g.setFont('6x8:1x2'); + g.setColor(NUMCOLOR); + NUMPADDING = (R.x2-g.stringWidth(NUM))/2; + g.setFontAlign(-1, -1, 0); + g.drawString(NUM, NUMPADDING, (R.y2)/4); + g.drawString("<-", NUMPADDING+10, showCharY+5); + g.drawString("->", R.x2-(NUMPADDING+20), showCharY+5); + + g.fillRect(NUMPADDING, (R.y2)-rectHeight*4/3, (R.x2)-NUMPADDING, (R.y2)-rectHeight*2/3); + } + + function updateTopString() { + g.setFont(SMALLFONT); + g.setColor(BGCOLOR); + g.fillRect(R.x,R.y,R.x2,R.y+9); + var rectLen = text.length<27? text.length*6:27*6; + g.setColor(0.7,0,0); + //draw cursor at end of text + g.fillRect(R.x+rectLen+5,R.y,R.x+rectLen+10,R.y+9); + g.setColor(HLCOLOR); + g.setFontAlign(-1, -1, 0); + g.drawString(text.length<=27? text : '<- '+text.substr(-24,24), R.x+5, R.y+1); + } + + function showChars(chars) { + "ram"; + + // clear large character + g.setColor(BGCOLOR); + g.fillRect(R.x+65,showCharY,R.x2-65,showCharY+28); + + // show new large character + g.setColor(HLCOLOR); + g.setFont(BIGFONT); + g.setFontAlign(-1, -1, 0); + g.drawString(chars, (R.x2 - g.stringWidth(chars))/2, showCharY+4); + } + + var charPos; + var char; + var prevChar; + + function moveCharPos(list, select, posPixels) { + charPos = Math.min(list.length-1, Math.max(0, Math.floor(posPixels))); + char = list.charAt(charPos); + + if (char != prevChar) showChars(char); + prevChar = char; + + if (select) { + text += char; + updateTopString(); + } + } + + return new Promise((resolve,reject) => { + // Interpret touch input + Bangle.setUI({ + mode: 'custom', + back: ()=>{ + Bangle.setUI(); + g.clearRect(Bangle.appRect); + resolve(text); + }, + drag: function(event) { + "ram"; + + // drag on middle bottom rectangle + if (event.x > MIDPADDING - 2 && event.x < (R.x2-MIDPADDING + 2) && event.y >= ( (R.y2) - 12 )) { + moveCharPos(MIDDLE, event.b == 0, (event.x-middleStart)/(middleWidth/MIDDLE.length)); + } + // drag on left or right rectangle + else if (event.y > R.y && (event.x < MIDPADDING-2 || event.x > (R.x2-MIDPADDING + 2))) { + moveCharPos(event.x ( (R.y2) - 52 ))) { + moveCharPos(NUM, event.b == 0, (event.x-NUMPADDING)/6); + } + // Make a space or backspace by tapping right or left on screen above green rectangle + else if (event.y > R.y && event.b == 0) { + if (event.x < (R.x2)/2) { + showChars('<-'); + text = text.slice(0, -1); + } else { + //show space sign + showChars('->'); + text += ' '; + } + prevChar = null; + updateTopString(); + } + } + }); + + R = Bangle.appRect; + MIDPADDING = R.x + 35; + showCharY = (R.y2)/3; + topStart = R.y+12; + + drawAbcRow(); + drawNumRow(); + updateTopString(); + }); +}; diff --git a/apps/draguboard/metadata.json b/apps/draguboard/metadata.json new file mode 100644 index 000000000..926e36807 --- /dev/null +++ b/apps/draguboard/metadata.json @@ -0,0 +1,15 @@ +{ "id": "draguboard", + "name": "DragUboard", + "version":"0.01", + "description": "A library for text input via swiping U-shaped keyboard.", + "icon": "app.png", + "type":"textinput", + "tags": "keyboard", + "supports" : ["BANGLEJS2"], + "screenshots": [{"url":"screenshot.png"}], + "readme": "README.md", + "storage": [ + {"name":"textinput","url":"lib.js"}, + {"name":"draguboard.settings.js","url":"settings.js"} + ] +} diff --git a/apps/draguboard/screenshot.png b/apps/draguboard/screenshot.png new file mode 100644 index 000000000..f2cb91717 Binary files /dev/null and b/apps/draguboard/screenshot.png differ diff --git a/apps/draguboard/settings.js b/apps/draguboard/settings.js new file mode 100644 index 000000000..c94ebee70 --- /dev/null +++ b/apps/draguboard/settings.js @@ -0,0 +1,44 @@ +(function(back) { + let settings = require('Storage').readJSON('draguboard.json',1)||{}; + const colors = { + 4095: /*LANG*/"White", + 4080: /*LANG*/"Yellow", + 3840: /*LANG*/"Red", + 3855: /*LANG*/"Magenta", + 255: /*LANG*/"Cyan", + 240: /*LANG*/"Green", + 15: /*LANG*/"Blue", + 0: /*LANG*/"Black", + '-1': /*LANG*/"Default" + }; + + const save = () => require('Storage').write('draguboard.json', settings); + function colorMenu(key) { + let menu = {'': {title: key}, '< Back': () => E.showMenu(appMenu)}; + Object.keys(colors).forEach(color => { + var label = colors[color]; + menu[label] = { + value: settings[key] == color, + onchange: () => { + if (color >= 0) { + settings[key] = color; + } else { + delete settings[key]; + } + save(); + setTimeout(E.showMenu, 10, appMenu); + } + }; + }); + return menu; + } + + const appMenu = { + '': {title: 'draguboard'}, '< Back': back, + /*LANG*/'ABC Color': () => E.showMenu(colorMenu("ABC")), + /*LANG*/'Num Color': () => E.showMenu(colorMenu("Num")), + /*LANG*/'Highlight Color': () => E.showMenu(colorMenu("Highlight")) + }; + + E.showMenu(appMenu); +}); \ No newline at end of file diff --git a/apps/fwupdate/ChangeLog b/apps/fwupdate/ChangeLog index ea0b48eb9..a9673f6de 100644 --- a/apps/fwupdate/ChangeLog +++ b/apps/fwupdate/ChangeLog @@ -6,3 +6,4 @@ Add CRC checks for common bootloaders that we know don't work 0.04: Include a precompiled bootloader for easy bootloader updates 0.05: Rename Bootloader->DFU and add explanation to avoid confusion with Bootloader app +0.06: Lower chunk size to 1024 (from 2048) to make firmware updates more reliable diff --git a/apps/fwupdate/custom.html b/apps/fwupdate/custom.html index 31eb4a256..a648030c1 100644 --- a/apps/fwupdate/custom.html +++ b/apps/fwupdate/custom.html @@ -98,6 +98,8 @@ function onInit(device) { if (crc==4056371285) version = "2v13"; if (crc==1038322422) version = "2v14"; if (crc==2560806221) version = "2v15"; + if (crc==2886730689) version = "2v16"; + if (crc==156320890) version = "2v17"; if (!ok) { version += `(⚠ update required)`; } @@ -317,7 +319,7 @@ function createJS_app(binary, startAddress, endAddress) { hexJS += `\x10if (E.CRC32(E.memoryArea(0xF7000,0x7000))==1207580954) { print("DFU 2v10.236 needs update"); load();}\n`; hexJS += '\x10var s = require("Storage");\n'; hexJS += '\x10s.erase(".firmware");\n'; - var CHUNKSIZE = 2048; + var CHUNKSIZE = 1024; for (var i=0;iCHUNKSIZE) l=CHUNKSIZE; diff --git a/apps/fwupdate/metadata.json b/apps/fwupdate/metadata.json index 372f6850c..e3294f316 100644 --- a/apps/fwupdate/metadata.json +++ b/apps/fwupdate/metadata.json @@ -1,7 +1,7 @@ { "id": "fwupdate", "name": "Firmware Update", - "version": "0.05", + "version": "0.06", "description": "Uploads new Espruino firmwares to Bangle.js 2", "icon": "app.png", "type": "RAM", diff --git a/apps/iconlaunch/ChangeLog b/apps/iconlaunch/ChangeLog index 6f0e8194c..8bad496bf 100644 --- a/apps/iconlaunch/ChangeLog +++ b/apps/iconlaunch/ChangeLog @@ -21,3 +21,4 @@ 0.15: Ensure that we hide widgets if in fullscreen mode (So that widgets are still hidden if launcher is fast-loaded) 0.16: Use firmware provided E.showScroller method +0.17: fix fullscreen with oneClickExit diff --git a/apps/iconlaunch/app.js b/apps/iconlaunch/app.js index 8d155c73e..9f8cedb0f 100644 --- a/apps/iconlaunch/app.js +++ b/apps/iconlaunch/app.js @@ -9,6 +9,7 @@ timeOut:"Off" }, s.readJSON("iconlaunch.json", true) || {}); + if (!settings.fullscreen) { Bangle.loadWidgets(); Bangle.drawWidgets(); @@ -103,15 +104,12 @@ }; const itemsN = Math.ceil(launchCache.apps.length / appsN); - let back = ()=>{}; - if (settings.oneClickExit) back = Bangle.showClock; - + let idWatch = null; let options = { h: itemSize, c: itemsN, draw: drawItem, select: selectItem, - back: back, remove: function() { if (timeout) clearTimeout(timeout); Bangle.removeListener("drag", updateTimeout); @@ -120,8 +118,26 @@ if (settings.fullscreen) { // for fast-load, if we hid widgets then we should show them again require("widget_utils").show(); } - } + if(idWatch) clearWatch(idWatch); + }, + btn:Bangle.showClock }; + + //work both the fullscreen and the oneClickExit + if( settings.fullscreen && settings.oneClickExit) + { + idWatch=setWatch(function(e) { + Bangle.showClock(); + }, BTN, {repeat:false, edge:'rising' }); + + } + else if( settings.oneClickExit ) + { + options.back=Bangle.showClock; + } + + + let scroller = E.showScroller(options); @@ -141,4 +157,4 @@ Bangle.on("touch", updateTimeout); updateTimeout(); -} +} \ No newline at end of file diff --git a/apps/iconlaunch/metadata.json b/apps/iconlaunch/metadata.json index 435a29b39..35a7907bd 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.16", + "version": "0.17", "icon": "app.png", "description": "A launcher inspired by smartphones, with an icon-only scrollable menu.", "tags": "tool,system,launcher", diff --git a/apps/messages_light/ChangeLog b/apps/messages_light/ChangeLog index 328e2a120..23d9ba053 100644 --- a/apps/messages_light/ChangeLog +++ b/apps/messages_light/ChangeLog @@ -4,4 +4,6 @@ settings now points to message settings implemented use of the "messageicons" library removed lib no longer used -1.3: icon changed \ No newline at end of file +1.3: icon changed +1.4: new management of events implemented; removed code no longer used (from now the music will be managed by the Messagesgui app) +1.5: Fix graphic bug; View via popup while there are other open apps \ No newline at end of file diff --git a/apps/messages_light/README.md b/apps/messages_light/README.md index 00fe39bd0..2bc162cb8 100644 --- a/apps/messages_light/README.md +++ b/apps/messages_light/README.md @@ -5,7 +5,17 @@ This app handles the display of messages and message notifications. It is a GUI replacement for the `messages` apps. +To work, you must install: +- Messages +- Messages UI +- Messages Light (obviously) + +The Messages UI is recalled for the management of the "Music" notification (up to implementing a dedicated app) + + ## Creator Rarder44 +Thanks to @halemmerich for having "reviewed" the code. I applied some of your changes. + diff --git a/apps/messages_light/messages_light.app.js b/apps/messages_light/messages_light.app.js index 5d5363d38..40f94dd0f 100644 --- a/apps/messages_light/messages_light.app.js +++ b/apps/messages_light/messages_light.app.js @@ -10,35 +10,39 @@ } */ + + + let LOG=function(){ //print.apply(null, arguments); } - - -let settings= (()=>{ - let tmp={}; - tmp.NewEventFileName="messages_light.NewEvent.json"; - - tmp.fontSmall = "6x8"; - tmp.fontMedium = g.getFonts().includes("Vector")?"Vector:16":"6x8:2"; - tmp.fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; - tmp.fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4"; +let settings= { + NewEventFileName:"messages_light.NewEvent.json", + fontSmall : "6x8", + fontMedium : "Vector:16", + fontBig : "Vector:20", + fontLarge : "Vector:30", + colHeadBg : g.theme.dark ? "#141":"#4f4", - tmp.colHeadBg = g.theme.dark ? "#141":"#4f4"; - tmp.colBg = g.theme.dark ? "#000":"#fff"; - tmp.colLock = g.theme.dark ? "#ff0000":"#ff0000"; + colBg : g.theme.dark ? "#000":"#fff", + colLock : g.theme.dark ? "#ff0000":"#ff0000", + + quiet:!!((require('Storage').readJSON('setting.json', 1) || {}).quiet), + timeOut:(require('Storage').readJSON("messages_light.settings.json", true) || {}).timeOut || "Off", +}; + + - tmp.quiet=((require('Storage').readJSON('setting.json', 1) || {}).quiet) - return tmp; -})(); let EventQueue=[]; //in posizione 0, c'è quello attualmente visualizzato let callInProgress=false; +let justOpened=true; + //TODO: RICORDARSI DI FARE IL DELETE @@ -46,68 +50,73 @@ var manageEvent = function(event) { event.new=true; - LOG("manageEvent"); - if( event.id=="call") - { - showCall(event); - return; + LOG(event); + + if( event.id=="call"){ + showCall(event); } - switch(event.t) - { - case "add": - EventQueue.unshift(event); + else if( event.id=="music"){ + //la musica non la gestisco più ( uso l'app standard o un altra app) + } + else{ - if(!callInProgress) - showMessage(event); - break; - - case "modify": - //cerco l'evento nella lista, se lo trovo, lo modifico, altrimenti lo pusho - let find=false; - EventQueue.forEach(element => { - if(element.id == event.id) - { - find=true; - Object.assign(element,event); - } - }); - if(!find) //se non l'ho trovato, lo aggiungo in fondo + //----------------- + //notification + //----------------- + if(event.t=="add"){ EventQueue.unshift(event); - - if(!callInProgress) - showMessage(event); - break; - - case "remove": - - //se non c'è niente nella queue e non c'è una chiamata in corso - if( EventQueue.length==0 && !callInProgress) - next(); - - //se l'id è uguale a quello attualmente visualizzato ( e non siamo in chiamata ) - if(!callInProgress && EventQueue[0] !== undefined && EventQueue[0].id == event.id) - next(); //passo al messaggio successivo ( per la rimozione ci penserà la next ) - - else{ - //altrimenti rimuovo tutti gli elementi con quell'id( creando un nuovo array ) - let newEventQueue=[]; + + if(!callInProgress) + showMessage(event); + } + else if(event.t=="modify"){ + //cerco l'evento nella lista, se lo trovo, lo modifico, altrimenti lo pusho + let find=false; EventQueue.forEach(element => { - if(element.id != event.id) - newEventQueue.push(element); + if(element.id == event.id) + { + find=true; + Object.assign(element,event); + } }); - EventQueue=newEventQueue; - } - - + if(!find) //se non l'ho trovato, lo aggiungo in fondo + EventQueue.unshift(event); + + if(!callInProgress) + showMessage(event); + } + else if(event.t=="remove"){ + //se non c'è niente nella queue e non c'è una chiamata in corso + if( EventQueue.length==0 && !callInProgress) + next(); + + //se l'id è uguale a quello attualmente visualizzato ( e non siamo in chiamata ) + if(!callInProgress && EventQueue[0] !== undefined && EventQueue[0].id == event.id) + next(); //passo al messaggio successivo ( per la rimozione ci penserà la next ) + + else{ + //altrimenti rimuovo tutti gli elementi con quell'id( creando un nuovo array ) + let newEventQueue=[]; + EventQueue.forEach(element => { + if(element.id != event.id) + newEventQueue.push(element); + }); + + //non sovrascrivo, cosi uso lo stesso oggetto in memoria e dovrei avere meno problemi di memory leak + EventQueue.length=0; + newEventQueue.forEach(element => { + EventQueue.push(element); + }); + } + } + //----------------- + //notification + //----------------- + - - break; - case "musicstate": - case "musicinfo": - - break; } + }; @@ -118,6 +127,10 @@ var manageEvent = function(event) { let showMessage = function(msg){ LOG("showMessage"); LOG(msg); + + updateTimeout(); + + g.setBgColor(settings.colBg); @@ -208,7 +221,7 @@ let showCall = function(msg) } callInProgress=true; - + updateTimeout(); //se è una chiamata ( o una nuova chiamata, diversa dalla precedente ) @@ -262,7 +275,6 @@ let showCall = function(msg) - let next=function(){ LOG("next"); @@ -274,9 +286,10 @@ let next=function(){ EventQueue.shift(); //passa al messaggio successivo, se presente - tolgo il primo callInProgress=false; + LOG(EventQueue.length); if( EventQueue.length == 0) { - LOG("no element in queue - closing") + LOG("no element in queue - closing"); setTimeout(_ => load()); return; } @@ -288,31 +301,12 @@ let next=function(){ - - - - - - - - -let showMapMessage=function(msg) { - - g.clearRect(Bangle.appRect); - PrintMessageStrings({body:"Not implemented!"}); - -} - - - - - -let CallBuzzTimer=null; +let CallBuzzTimer=undefined; let StopBuzzCall=function() { if (CallBuzzTimer){ clearInterval(CallBuzzTimer); - CallBuzzTimer=null; + CallBuzzTimer=undefined; } } let DrawTriangleUp=function() @@ -443,13 +437,16 @@ let PrintMessageStrings=function(msg) let doubleTapUnlock=function(data) { + updateTimeout(); if( data.double) //solo se in double { Bangle.setLocked(false); Bangle.setLCDPower(1); } } -let toushScroll=function(button, xy) { +let toushScroll=function(_, xy) { + updateTimeout(); + let height=176; //g.getHeight(); -> 176 B2 height/=2; @@ -464,6 +461,24 @@ let toushScroll=function(button, xy) { } + +let timeout; +const updateTimeout = function(){ +if (settings.timeOut!="Off"){ + removeTimeout(); + if( callInProgress) return; //c'è una chiamata in corso -> no timeout + if( music!=undefined && EventQueue.length==0 ) return; //ho aperto l'interfaccia della musica e non ho messaggi davanti -> no timeout + + + let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt + timeout = setTimeout(next,time*1000); //next or Bangle.showClock/load()??? + } +}; +const removeTimeout=function(){ + if (timeout) clearTimeout(timeout); +} + + let main = function(){ LOG("Main"); @@ -478,16 +493,18 @@ let main = function(){ Bangle.on('tap', doubleTapUnlock); Bangle.on('touch', toushScroll); - //quando apro quest'app, do per scontato che c'è un messaggio da leggere posto in un file particolare ( NewMessage.json ) + //quando apro quest'app, do per scontato che c'è un messaggio da leggere posto in un file particolare ( messages_light.NewEvent.json ) let eventToShow = require('Storage').readJSON(settings.NewEventFileName, true); require("Storage").erase(settings.NewEventFileName) if( eventToShow!==undefined) manageEvent(eventToShow); else { - LOG("file not found!"); - setTimeout(_ => load(), 0); + LOG("file event not found! -> ?? open debug text"); + setTimeout(_=>{ GB({"t":"notify","id":15754117198411,"src":"Hangouts","title":"A Name","body":"Debug notification \nmessage contents demo demo demo demo"}) },0); } + justOpened=false; + }; diff --git a/apps/messages_light/messages_light.boot.js b/apps/messages_light/messages_light.boot.js index 741d08b96..f28e50661 100644 --- a/apps/messages_light/messages_light.boot.js +++ b/apps/messages_light/messages_light.boot.js @@ -1,33 +1,13 @@ -/* -//OLD CODE -> backup purpose - -let messageBootManager=function(type,event){ - //se l'app non è aperta - if ("undefined"==typeof manageEvent) - { - if(event.t=="remove") return; //l'app non è aperta, non c'è nessun messaggio da rimuovere dalla queue -> non lancio l'app - - //la apro - require("Storage").writeJSON("messages_light.NewEvent.json",{"event":event,"type":type}); - load("messages_light.app.js"); - } - else - { - //altrimenti gli dico di gestire il messaggio - manageEvent(type,event); - } -} -Bangle.on("message", messageBootManager); -Bangle.on("call", messageBootManager);*/ - - - +//OLD Code //override require to filter require("message") -global.require_real=global.require; +/*global.require_real=global.require; global.require = (_require => file => { - if (file==="messages") file = "messagesProxy"; - //else if (file==="messages_REAL") file = "messages"; //backdoor to real message - + if (file==="messages") file = "messagesProxy"; return _require(file); -})(require); - +})(require);*/ + +//the file on the device is called "boot_messages_light.boot.js" +//it's NOT an error! +//it's for the boot order + +Bangle.on("message", (type, msg) => require("messages_light.listener.js").listener(type, msg)); diff --git a/apps/messages_light/messages_light.listener.js b/apps/messages_light/messages_light.listener.js new file mode 100644 index 000000000..2525a52bd --- /dev/null +++ b/apps/messages_light/messages_light.listener.js @@ -0,0 +1,122 @@ + +let overlayTimeout=undefined; +exports.listener = function(type, event) { + + + //salva gli eventi che arrivano su file + /* events=require("Storage").readJSON("events_log",true) || []; + events.push ( event) + require("Storage").writeJSON("events_log",events); + */ + //if (event.handled) return; // already handled/app open + if( type=="clearAll" || type=="music" || event.id=="music") return; //lo lascio gestire a qualcun altro + + //se arrivo qua gestisco io + //non mi preoccupo di salvare ( a meno di problemi a mantenere tanti messaggi in queue nella ram...) + event.handled=true; + + + if( Bangle.CLOCK || global.__FILE__ === undefined || global.__FILE__ === ".bootcde" || global.__FILE__.startsWith("messages_light.")) + { + //se non ci sono app aperte ( clock oppure c'è messages_light) + //continuo con la visualizzazione dell messaggio + + let callApp; + //se l'app non è aperta + if ("undefined"==typeof manageEvent) + { + if(event.t=="remove") return; //l'app non è aperta, non c'è nessun messaggio da rimuovere dalla queue -> non lancio l'app + + //chiamo la load dell'app + callApp=function(event){ + require("Storage").writeJSON("messages_light.NewEvent.json",event); + load("messages_light.app.js"); + } + } + else + { + //dico all'app di gestire l'evento + callApp=function(event){ + manageEvent(event); + } + } + + + callApp(event); + + + } + else{ + //TODO: BHOO!!! + //vibro e basta? + //faccio comparire un overlay veloce? + //uso l'overlay sempre? ( gestione di tutti gli eventi smadonnosa... ) + //salvo lo stato dell'app attuale( NON SO COME ), lancio la mia app e alla chiusura torno allo stato precedente? + + console.log(event); + let ovr=undefined; + let palette; + let timeout; + + if(event.id=="call" && event.t!="remove") + { + let count=3; + let idInter= setInterval(()=>{ + if(--count<=0) + clearInterval(idInter); + + Bangle.buzz(100,1); + },200); + + ovr = Graphics.createArrayBuffer(136,136,2,{msb:true}); + ovr.setColor(1).fillRect({x:0,y:0,w:135,h:135,r:8}); + ovr.setColor(2).setFont("Vector:30").setFontAlign(0,0).drawString("Call",68,20); + var lines=ovr.wrapString(event.title,136); + for(let i=0;i< lines.length;i++) + ovr.setColor(2).setFont("Vector:20").setFontAlign(0,0).drawString(lines[i],68,50+i*15); + + palette=[0,g.toColor("#141"),g.toColor("#fff"),g.toColor("#FFF")]; + timeout=4000; + } + else if(event.t=="add" || event.t=="modify") + { + Bangle.buzz(); + ovr = Graphics.createArrayBuffer(136,136,2,{msb:true}); + ovr.setColor(1).fillRect({x:0,y:0,w:135,h:135,r:8}); + ovr.setColor(2).setFont("Vector:20").setFontAlign(0,0).drawString(event.src,68,15); + ovr.setColor(2).setFont("Vector:15").setFontAlign(0,0).drawString(event.title,68,35); + var lines=ovr.wrapString(event.body,136); + for(let i=0;i< lines.length;i++) + ovr.setColor(2).setFont("Vector:15").setFontAlign(0,0).drawString(lines[i],68,60+i*15); + + + palette=[0,g.toColor("#09c"),g.toColor("#fff"),g.toColor("#FFF")]; + timeout=5000; + } + + + if(ovr===undefined) + return; + + + Bangle.setLCDPower(true); + + Bangle.setLCDOverlay({ + width:ovr.getWidth(), height:ovr.getHeight(), + bpp:2, transparent:0, + palette:new Uint16Array(palette), + buffer:ovr.buffer + },20,20); + + Bangle.setLCDPower(true); + + if(overlayTimeout) clearTimeout(overlayTimeout); + overlayTimeout=setTimeout(()=>{ + Bangle.setLCDOverlay(); + overlayTimeout=undefined; + },timeout); + } + +} + + diff --git a/apps/messages_light/messages_light.messagesProxy.js b/apps/messages_light/messages_light.messagesProxy.js deleted file mode 100644 index 723397057..000000000 --- a/apps/messages_light/messages_light.messagesProxy.js +++ /dev/null @@ -1,30 +0,0 @@ - -//gestisco il messaggio a modo mio -exports.pushMessage = function(event) { - - //TODO: now i can't handle the music, so i call the real message app - if( event.id=="music") return require_real("messages").pushMessage(event); - - //se l'app non è aperta - if ("undefined"==typeof manageEvent) - { - if(event.t=="remove") return; //l'app non è aperta, non c'è nessun messaggio da rimuovere dalla queue -> non lancio l'app - - //la apro - require_real("Storage").writeJSON("messages_light.NewEvent.json",event); - load("messages_light.app.js"); - } - else - { - //altrimenti gli dico di gestire il messaggio - manageEvent(event); - } -} - - -//Call original message library -exports.clearAll = function() { return require_real("messages").clearAll()} -exports.getMessages = function() { return require_real("messages").getMessages()} -exports.status = function() { return require_real("messages").status()} -exports.buzz = function() { return require_real("messages").buzz(msgSrc)} -exports.stopBuzz = function() { return require_real("messages").stopBuzz()} \ No newline at end of file diff --git a/apps/messages_light/messages_light.settings.js b/apps/messages_light/messages_light.settings.js index b7197c70a..cd813d928 100644 --- a/apps/messages_light/messages_light.settings.js +++ b/apps/messages_light/messages_light.settings.js @@ -1 +1,27 @@ -eval(require("Storage").read("messages.settings.js")); +(function(back) { + const SETTINGS_FILE_NAME="messages_light.settings.json"; + let settings = function() { + let settings = require('Storage').readJSON(SETTINGS_FILE_NAME, true) || {}; + return settings; + } + function updateSetting(setting, value) { + let settings = require('Storage').readJSON(SETTINGS_FILE_NAME, true) || {}; + settings[setting] = value; + require('Storage').writeJSON(SETTINGS_FILE_NAME, settings); + } + const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"]; + var mainmenu = { + "" : { "title" : /*LANG*/"Messages Light" }, + "< Back" : back, + /*LANG*/'Time Out': { + value: timeOutChoices.indexOf(settings.timeOut), + min: 0, max: timeOutChoices.length-1, + format: v => timeOutChoices[v], + onchange: m => { + updateSetting("timeOut", timeOutChoices[m]); + } + }, + }; + E.showMenu(mainmenu); + }); + \ No newline at end of file diff --git a/apps/messages_light/metadata.json b/apps/messages_light/metadata.json index 3515a75c2..eeab7d9f4 100644 --- a/apps/messages_light/metadata.json +++ b/apps/messages_light/metadata.json @@ -1,7 +1,7 @@ { "id": "messages_light", "name": "Messages Light", - "version": "1.3", + "version": "1.5", "description": "A light implementation of messages App (display notifications from iOS and Gadgetbridge/Android)", "icon": "app.png", "type": "app", @@ -13,8 +13,8 @@ {"name":"messages_light.app.js","url":"messages_light.app.js"}, {"name":"messages_light.settings.js","url":"messages_light.settings.js"}, {"name":"messages_light.img","url":"app-icon.js","evaluate":true}, - {"name":"messagesProxy","url":"messages_light.messagesProxy.js"}, - {"name":"messages_light.boot.js","url":"messages_light.boot.js"} + {"name":"boot_messages_light.boot.js","url":"messages_light.boot.js"}, + {"name":"messages_light.listener.js","url":"messages_light.listener.js"} ], "data": [{"name":"messages_light.settings.json"},{"name":"messages_light.NewMessage.json"}], "screenshots": [{"url":"screenshot-notify.png"} ,{"url":"screenshot-long-text1.png"},{"url":"screenshot-long-text2.png"}, {"url":"screenshot-call.png"} ] diff --git a/apps/messagesoverlay/ChangeLog b/apps/messagesoverlay/ChangeLog index da98bfbce..0a2cf27b0 100644 --- a/apps/messagesoverlay/ChangeLog +++ b/apps/messagesoverlay/ChangeLog @@ -1 +1,3 @@ -0.01: Initial fork from messages_light \ No newline at end of file +0.01: Initial fork from messages_light +0.02: Fix touch/drag/swipe handlers not being restored correctly if a message is removed +0.03: Scroll six lines per swipe, leaving the previous top/bottom row visible. diff --git a/apps/messagesoverlay/app-icon.png b/apps/messagesoverlay/app-icon.png deleted file mode 100644 index c9b4b62ac..000000000 Binary files a/apps/messagesoverlay/app-icon.png and /dev/null differ diff --git a/apps/messagesoverlay/lib.js b/apps/messagesoverlay/lib.js index cc6b63176..5587fce19 100644 --- a/apps/messagesoverlay/lib.js +++ b/apps/messagesoverlay/lib.js @@ -279,6 +279,8 @@ let drawTriangleDown = function(ovr) { ovr.fillPoly([ovr.getWidth()-9, ovr.getHeight()-6, ovr.getWidth()-14, ovr.getHeight()-16, ovr.getWidth()-4, ovr.getHeight()-16]); }; +let linesScroll = 6; + let scrollUp = function(ovr) { msg = eventQueue[0]; LOG("up", msg); @@ -289,7 +291,7 @@ let scrollUp = function(ovr) { if (!msg.CanscrollUp) return; - msg.FirstLine = msg.FirstLine > 0 ? msg.FirstLine - 1 : 0; + msg.FirstLine = msg.FirstLine > 0 ? msg.FirstLine - linesScroll : 0; drawMessage(ovr, msg); }; @@ -304,7 +306,7 @@ let scrollDown = function(ovr) { if (!msg.CanscrollDown) return; - msg.FirstLine = msg.FirstLine + 1; + msg.FirstLine = msg.FirstLine + linesScroll; drawMessage(ovr, msg); }; @@ -389,19 +391,17 @@ let getTouchHandler = function(ovr){ }; }; -let touchHandler; -let swipeHandler; - let restoreHandler = function(event){ - if (backup[event]){ - Bangle["#on" + event]=backup[event]; - backup[event] = undefined; - } + LOG("Restore", event, backup[event]); + Bangle.removeAllListeners(event); + Bangle["#on" + event]=backup[event]; + backup[event] = undefined; }; let backupHandler = function(event){ - if (eventQueue.length > 1 && ovr) return; // do not backup, overlay is already up + if (backupDone) return; // do not backup, overlay is already up backup[event] = Bangle["#on" + event]; + LOG("Backed up", backup[event]); Bangle.removeAllListeners(event); }; @@ -414,21 +414,16 @@ let cleanup = function(){ restoreHandler("swipe"); restoreHandler("drag"); - if (touchHandler) { - Bangle.removeListener("touch", touchHandler); - touchHandler = undefined; - } - if (swipeHandler) { - Bangle.removeListener("swipe", swipeHandler); - swipeHandler = undefined; - } Bangle.setLCDOverlay(); + backupDone = false; ovr = undefined; quiet = undefined; }; let backup = {}; +let backupDone = false; + let main = function(ovr, event) { LOG("Main", event, settings); @@ -441,13 +436,11 @@ let main = function(ovr, event) { backupHandler("touch"); backupHandler("swipe"); backupHandler("drag"); - - if (touchHandler) Bangle.removeListener("touch",touchHandler); - if (swipeHandler) Bangle.removeListener("swipe",swipeHandler); - touchHandler = getTouchHandler(ovr); - swipeHandler = getSwipeHandler(ovr); - Bangle.on('touch', touchHandler); - Bangle.on('swipe', swipeHandler); + if (!backupDone){ + Bangle.on('touch', getTouchHandler(ovr)); + Bangle.on('swipe', getSwipeHandler(ovr)); + } + backupDone=true; if (event !== undefined){ drawBorder(ovr); @@ -492,4 +485,4 @@ exports.clearAll = function() { return require_real("messages").clearAll();}; exports.getMessages = function() { return require_real("messages").getMessages();}; exports.status = function() { return require_real("messages").status();}; exports.buzz = function() { return require_real("messages").buzz(msgSrc);}; -exports.stopBuzz = function() { return require_real("messages").stopBuzz();}; \ No newline at end of file +exports.stopBuzz = function() { return require_real("messages").stopBuzz();}; diff --git a/apps/messagesoverlay/metadata.json b/apps/messagesoverlay/metadata.json index 9efe95d26..6a3f953d8 100644 --- a/apps/messagesoverlay/metadata.json +++ b/apps/messagesoverlay/metadata.json @@ -1,18 +1,35 @@ { "id": "messagesoverlay", "name": "Messages Overlay", - "version": "0.01", + "version": "0.03", "description": "An overlay based implementation of a messages UI (display notifications from iOS and Gadgetbridge/Android)", "icon": "app.png", "type": "bootloader", "tags": "tool,system", - "supports": ["BANGLEJS2"], - "dependencies" : { "messageicons":"module","messages":"app" }, + "supports": [ + "BANGLEJS2" + ], + "dependencies": { + "messageicons": "module", + "messages": "app" + }, "readme": "README.md", "storage": [ - {"name":"messagesoverlay.settings.js","url":"settings.js"}, - {"name":"messagesoverlay","url":"lib.js"}, - {"name":"messagesoverlay.boot.js","url":"boot.js"} + { + "name": "messagesoverlay", + "url": "lib.js" + }, + { + "name": "messagesoverlay.boot.js", + "url": "boot.js" + } ], - "screenshots": [{"url":"screen_call.png"} ,{"url":"screen_message.png"} ] + "screenshots": [ + { + "url": "screen_call.png" + }, + { + "url": "screen_message.png" + } + ] } diff --git a/apps/messagesoverlay/settings.js b/apps/messagesoverlay/settings.js deleted file mode 100644 index b7197c70a..000000000 --- a/apps/messagesoverlay/settings.js +++ /dev/null @@ -1 +0,0 @@ -eval(require("Storage").read("messages.settings.js")); diff --git a/apps/multidice/ChangeLog b/apps/multidice/ChangeLog new file mode 100644 index 000000000..cb0cce2aa --- /dev/null +++ b/apps/multidice/ChangeLog @@ -0,0 +1,32 @@ +0.90: got most of the features done, lacking some polish and real-hardware testing +1.00: overhauled the whole app, made some margins larger to be easier to tap on +1.01: fixed bug that caused rolled dice on the right of screen to be writ off-screen +1.02: added vibration when dice is rolled +1.03: vibration caused the accelerometer to never stop +1.04: decreased vibration strength +1.05: toggled the acceleration handler to prevent infinite buzz loop +1.06: increased vibration again +1.07: IDK how to use promises properly +1.08: still trying to fix the lack of vibrations +1.09: hopefully now it's fixed? +1.10: not having web bluetooth to debug is a PAIN +1.11: decreased vibration time, decreased accel requirement +1.12: issue with app calling roll function too many times at startup +1.13: added a delay after the buzzer stops to prevent multi-rolling +1.14: made the delay needlessly long to see if it even does anything +1.15: moved accel & vibration commands to the accelHandler function +1.16: enabled button usage & temporarily disabled acceleration +1.17: made changes to when accelHandler gets overwritten, temporarily disabled button usage +1.18: decided to keep around the button even while testing, disabled all safety round the accelHandler self-triggering +1.19: added longer delay before resetting accelHandler +1.20: removed all traces of accel b/c I've given up +1.21: added a drawWidgets command to see if I have the padding right +1.22: ok the buzzing *might* work now +1.23: forgot to resolve the promise +1.24: fixed dumb errors +1.25: god I hope this works +1.26: trying to add timeout after it's done buzzing... again +1.27: OH GOD IT FINALLY WORKS +1.28: increased vibration strength, added some comments, & some QOL +1.29: changed image +1.30: changed image, again diff --git a/apps/multidice/README.md b/apps/multidice/README.md new file mode 100644 index 000000000..72a2d8af5 --- /dev/null +++ b/apps/multidice/README.md @@ -0,0 +1,20 @@ +# multiple dice roller + +roll anywhere from 1-8 dice at the same time. + +## Usage + +![startup.png](startup.png) +On the menu screen: tap on the dice to change what variant is selected, & shake/or press BTN to roll the dice +![single_rolled.png](single_rolled.png) +On the dice screen: tap anywhere on the screen to go back to the menu, or shake/or press BTN to roll the dice + +## Features + +roll anywhere from 1-8 dice (d4, d6, d8, d10, d12, d20, & d percentile). You can select multiple different dice at the same time +![many_selected.png](many_selected.png) +![many_rolled.png](many_rolled.png) + +## Controls + +App uses touchscreen to cycle through different dice, and accelerometer/BTN to roll them diff --git a/apps/multidice/app-icon.js b/apps/multidice/app-icon.js new file mode 100644 index 000000000..88feb83cd --- /dev/null +++ b/apps/multidice/app-icon.js @@ -0,0 +1 @@ +atob("MDABAAAAA8AAAAAAB+AAf//+DDAAwAACGBgAwAADMAwAwAADYAYAgAAAwYMAgAABg+GAgAADBiDAgDwGBiBggH4MAmAwgMMYA8AYgIEwAAAMgIFg8AeGgIHBkAyDgMNBGAjDgH5BGAjDgDxh8A+DgAAwYAMGgAAYAYAMgAAMA+AYwAAGBiAwwAADBiBgwAADgmDAf//+w8Gwf///4AM8wAADMAYEzwADGA3En4ABDBvkmYABBjIkmYABI+IkH4ABPAPkjwABHAHEgDwBAHAEAH4BAPgEgGYBAIgEgGYBAIgEgH4BAPgEgDwBAHAEgADxHAHEgAH5PgPkgAGZIgIkgAGZIgIkgAH5PgPkwADzHAHEwAADAAAEcAAPwAAcP//8///w") diff --git a/apps/multidice/app.js b/apps/multidice/app.js new file mode 100644 index 000000000..53f67e21e --- /dev/null +++ b/apps/multidice/app.js @@ -0,0 +1,185 @@ +var menu = true; // default to have the selection menu open +const DICE_ARRAY = [0, 4, 6, 8, 10, 12, 20, 100]; // 0 means nothing selected +const SELECTION_ARRAY = [6, 0, 0, 0, 0, 0, 0, 0]; // default to selecting a single d20 + +// function to draw the selection menu +function drawMenu() { + + stringArr = new Array ("", "", "", "", "", "", "", ""); + for (i = 0; i < 8; i++) { + + if (SELECTION_ARRAY [i] != 0) { + + stringArr [i] = "" + DICE_ARRAY [SELECTION_ARRAY [i]]; + } else { + + stringArr [i] = " . "; // more clearly defines where the user can tap + } + } + + g.clear(); + g.setFont ("Vector", 40); + + // " ".slice(-3) left-pads all numbers with spaces + g.drawString ((" " + stringArr [0]).slice (-3), 5, 10); + g.drawString ((" " + stringArr [1]).slice (-3), 5, 50); + g.drawString ((" " + stringArr [2]).slice (-3), 5, 90); + g.drawString ((" " + stringArr [3]).slice (-3), 5, 130); + g.drawString ((" " + stringArr [4]).slice (-3), 96, 10); + g.drawString ((" " + stringArr [5]).slice (-3), 96, 50); + g.drawString ((" " + stringArr [6]).slice (-3), 96, 90); + g.drawString ((" " + stringArr [7]).slice (-3), 96, 130); +} + +// function to change what dice is selected in the menu +function touchHandler (button, xy) { + + if (! menu) { // if menu isn't open, open it & return + + menu = true; + drawMenu(); + return; + } + + if (xy.x <= 87) { // left + + if (xy.y <= 43) { // first + + selection = 0; + } else if (xy.y <= 87) { // second + + selection = 1; + } else if (xy.y <= 131) { // third + + selection = 2; + } else { // fourth + + selection = 3; + } + } else { // right + + if (xy.y <= 43) { // first + + selection = 4; + } else if (xy.y <= 87) { // second + + selection = 5; + } else if (xy.y <= 131) { // third + + selection = 6; + } else { // fourth + + selection = 7; + } + } + + if (SELECTION_ARRAY [selection] == SELECTION_ARRAY.length - 1) { // if last dice is selected, go back to first + + SELECTION_ARRAY [selection] = 0; + } else { + + SELECTION_ARRAY [selection] += 1; + } + + drawMenu(); +} + +function accelHandler (xyz) { + + if (xyz.diff >= 0.3) { + + menu = false; + mutex (rollDice).catch (() => { + + return; // not necessary, but prevents spamming the logs + }); + } +} + +// returns a resolved promise if no other mutex call is active, all further ones return a rejected one +let lock = false; +function mutex (functionRef) { + + if (lock) { + + return Promise.reject (new Error ("mutex is busy")); + } + + lock = true; + return new Promise ((resolve, reject) => { + + functionRef().then ((result) => { + + lock = false; + resolve (result); + }).catch ((error) => { + + lock = false; + reject (error); + }); + }); +} + +// function to roll all selected dice, and display them +function rollDice() { + + resultsArr = new Uint8Array (8); + for (i = 0; i < 8; i++) { + + if (SELECTION_ARRAY [i] != 0) { + + resultsArr [i] = random (DICE_ARRAY [SELECTION_ARRAY [i]]); + } + } + + g.clear(); + g.setFont ("Vector", 40); + + for (i = 0; i < 4; i++) { + + if (SELECTION_ARRAY [i] != 0) { + + g.drawString ((" " + resultsArr [i]).slice (-3), 5, 10 + 40 * i); + } + } + + for (i = 4; i < 8; i++) { + + if (SELECTION_ARRAY [i] != 0) { + + g.drawString ((" " + resultsArr [i]).slice (-3), 96, 10 + 40 * (i - 4)); + } + } + + return vibrate(); +} + +// triggers the vibration, then pauses before returning +function vibrate() { + + return new Promise ((resolve, reject) => { + + return Bangle.buzz (50, 1).then ((value) => { + + setTimeout (() => { + + resolve (value); + }, 200); + }); + }); +} + +// returns a integer [1, max] +function random (max) { + + return Math.round (Math.random() * (max - 1) + 1); +} + +drawMenu(); +Bangle.on ('touch', touchHandler); +Bangle.on ('accel', accelHandler); +setWatch (function() { + + menu = false; + mutex (rollDice); +}, BTN, {repeat: true, edge: "falling", debounce: 10}); diff --git a/apps/multidice/app.png b/apps/multidice/app.png new file mode 100644 index 000000000..75ee0514a Binary files /dev/null and b/apps/multidice/app.png differ diff --git a/apps/multidice/many_rolled.png b/apps/multidice/many_rolled.png new file mode 100644 index 000000000..4cf34e9c7 Binary files /dev/null and b/apps/multidice/many_rolled.png differ diff --git a/apps/multidice/many_selected.png b/apps/multidice/many_selected.png new file mode 100644 index 000000000..137e2c363 Binary files /dev/null and b/apps/multidice/many_selected.png differ diff --git a/apps/multidice/metadata.json b/apps/multidice/metadata.json new file mode 100644 index 000000000..304c789e4 --- /dev/null +++ b/apps/multidice/metadata.json @@ -0,0 +1,15 @@ +{ "id": "multidice", + "name": "multiple dice roller", + "shortName":"multidice", + "version":"1.30", + "description": "roll anywhere from 1-8 dice at the same time", + "icon": "app.png", + "tags": "tool,game", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "allow_emulator": true, + "storage": [ + {"name":"multidice.app.js","url":"app.js"}, + {"name":"multidice.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/multidice/single_rolled.png b/apps/multidice/single_rolled.png new file mode 100644 index 000000000..3ce67ab74 Binary files /dev/null and b/apps/multidice/single_rolled.png differ diff --git a/apps/multidice/startup.png b/apps/multidice/startup.png new file mode 100644 index 000000000..587281e5c Binary files /dev/null and b/apps/multidice/startup.png differ diff --git a/apps/notanalog/ChangeLog b/apps/notanalog/ChangeLog index 094125f52..e011c4ae1 100644 --- a/apps/notanalog/ChangeLog +++ b/apps/notanalog/ChangeLog @@ -4,3 +4,4 @@ 0.04: Use alarm for timer instead of own alarm implementation. 0.05: Use internal step counter if no widget is available. 0.06: Use widget_utils. +0.07: Respect system setting for 12h or 24h time diff --git a/apps/notanalog/metadata.json b/apps/notanalog/metadata.json index 319d396a9..851e95ec8 100644 --- a/apps/notanalog/metadata.json +++ b/apps/notanalog/metadata.json @@ -3,7 +3,7 @@ "name": "Not Analog", "shortName":"Not Analog", "icon": "notanalog.png", - "version":"0.06", + "version":"0.07", "readme": "README.md", "supports": ["BANGLEJS2"], "description": "An analog watch face for people that can not read analog watch faces.", diff --git a/apps/notanalog/notanalog.app.js b/apps/notanalog/notanalog.app.js index 29fb1730f..b37c34721 100644 --- a/apps/notanalog/notanalog.app.js +++ b/apps/notanalog/notanalog.app.js @@ -13,6 +13,9 @@ let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings; for (const key in saved_settings) { settings[key] = saved_settings[key] } +const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})[ + "12hour" +]; /* * Set some important constants such as width, height and center @@ -199,6 +202,9 @@ function drawTime(){ // Hour var h = state.currentDate.getHours(); + if (is12Hour && h > 12) { + h = h - 12; + } var h1 = parseInt(h / 10); var h2 = h < 10 ? h : h - h1*10; drawTextCleared(h1, cx, posY+8); diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index 7f788c139..a256b459c 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -16,3 +16,4 @@ 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) +0.16: Draw waypoints, too. diff --git a/apps/openstmap/README.md b/apps/openstmap/README.md index f19b13bd1..bf247c7b7 100644 --- a/apps/openstmap/README.md +++ b/apps/openstmap/README.md @@ -17,20 +17,20 @@ To add a 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. +quality, but uploads faster and takes less space). Bangle.js 2 is limited to 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). +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), and +displays waypoints in the watch (if dependencies exist). * 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 +* 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 diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js index 89e2d2ddb..a5130d23e 100644 --- a/apps/openstmap/app.js +++ b/apps/openstmap/app.js @@ -10,6 +10,7 @@ var settings = require("Storage").readJSON("openstmap.json",1)||{}; function redraw() { g.setClipRect(R.x,R.y,R.x2,R.y2); m.draw(); + drawPOI(); drawMarker(); // if track drawing is enabled... if (settings.drawTrack) { @@ -25,6 +26,26 @@ function redraw() { g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); } +// Draw the POIs +function drawPOI() { + try { + var waypoints = require("waypoints").load(); + } catch (ex) { + // Waypoints module not available. + return; + } + g.setFont("Vector", 18); + waypoints.forEach((wp, idx) => { + var p = m.latLonToXY(wp.lat, wp.lon); + var sz = 2; + g.setColor(0,0,0); + g.fillRect(p.x-sz, p.y-sz, p.x+sz, p.y+sz); + g.setColor(0,0,0); + g.drawString(wp.name, p.x, p.y); + print(wp.name); + }) +} + // Draw the marker for where we are function drawMarker() { if (!fix.fix) return; diff --git a/apps/openstmap/metadata.json b/apps/openstmap/metadata.json index 819dc4122..4419cd411 100644 --- a/apps/openstmap/metadata.json +++ b/apps/openstmap/metadata.json @@ -2,7 +2,7 @@ "id": "openstmap", "name": "OpenStreetMap", "shortName": "OpenStMap", - "version": "0.15", + "version": "0.16", "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", diff --git a/apps/qrcode/metadata.json b/apps/qrcode/metadata.json index 24af7b813..89c859a0c 100644 --- a/apps/qrcode/metadata.json +++ b/apps/qrcode/metadata.json @@ -1,6 +1,7 @@ { "id": "qrcode", "name": "Custom QR Code", + "shortName": "QR Code", "version": "0.06", "description": "Use this to upload a customised QR code to Bangle.js", "icon": "app.png", diff --git a/apps/quicklaunch/metadata.json b/apps/quicklaunch/metadata.json index a0ff9fa9d..e38d25d09 100644 --- a/apps/quicklaunch/metadata.json +++ b/apps/quicklaunch/metadata.json @@ -6,11 +6,12 @@ "description": "Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice. Configurations can be accessed through Settings->Apps.", "type": "bootloader", "tags": "tools, system", + "readme": "README.md", "supports": ["BANGLEJS2"], "storage": [ - {"name":"quicklaunch.settings.js","url":"settings.js"}, - {"name":"quicklaunch.boot.js","url":"boot.js"}, - {"name":"quicklaunch.app.js","url":"app.js"} + {"name": "quicklaunch.settings.js", "url": "settings.js"}, + {"name": "quicklaunch.boot.js", "url": "boot.js"}, + {"name": "quicklaunch.app.js", "url": "app.js"} ], - "data": [{"name":"quicklaunch.json"}] + "data": [{"name": "quicklaunch.json"}] } diff --git a/apps/runplus/ChangeLog b/apps/runplus/ChangeLog index 0e19b5559..d920a3eca 100644 --- a/apps/runplus/ChangeLog +++ b/apps/runplus/ChangeLog @@ -17,3 +17,6 @@ Keep run state between runs (allowing you to exit and restart the app) 0.16: Don't clear zone 2b indicator segment when updating HRM reading. Write to correct settings file, fixing settings not working. +0.17: Fix typo in variable name preventing starting a run. +0.18: Tweak HRM min/max defaults. Extend min/max intervals in settings. Fix + another typo. diff --git a/apps/runplus/app.js b/apps/runplus/app.js index 9d7010e6c..7cb5d4381 100644 --- a/apps/runplus/app.js +++ b/apps/runplus/app.js @@ -44,8 +44,8 @@ let settings = Object.assign({ }, }, HRM: { - min: 65, - max: 170, + min: 55, + max: 185, }, }, require("Storage").readJSON("runplus.json", 1) || {}); let statIDs = [settings.B1,settings.B2,settings.B3,settings.B4,settings.B5,settings.B6].filter(s=>s!==""); @@ -124,7 +124,7 @@ lc.push({ type:"h", filly:1, c:[ // Now calculate the layout let layout = new Layout( { type:"v", c: lc -},{lazy:true, btns:[{ label:"---", cb: ()=>{if (karvonnenActive) {stopKarvonnenUI();run();} onStartStop();}, id:"button"}]}); +},{lazy:true, btns:[{ label:"---", cb: (()=>{if (karvonenActive) {stopKarvonenUI();run();} onStartStop();}), id:"button"}]}); delete lc; setStatus(exs.state.active); layout.render(); diff --git a/apps/runplus/metadata.json b/apps/runplus/metadata.json index 017a52ac3..c605c438d 100644 --- a/apps/runplus/metadata.json +++ b/apps/runplus/metadata.json @@ -1,7 +1,7 @@ { "id": "runplus", "name": "Run+", - "version": "0.16", + "version": "0.18", "description": "Displays distance, time, steps, cadence, pace and more for runners. Based on the Run app, but extended with additional screen for heart rate interval training.", "icon": "app.png", "tags": "run,running,fitness,outdoors,gps,karvonen,karvonnen", diff --git a/apps/runplus/settings.js b/apps/runplus/settings.js index cd72022be..539391a27 100644 --- a/apps/runplus/settings.js +++ b/apps/runplus/settings.js @@ -32,8 +32,8 @@ }, }, HRM: { - min: 65, - max: 165, + min: 55, + max: 185, }, }, storage.readJSON(SETTINGS_FILE, 1) || {}); function saveSettings() { @@ -145,7 +145,7 @@ }, } hrmMenu[/*LANG*/"max"] = { - min: 120, max: 190, + min: 101, max: 220, value: settings.HRM.max, format: v => v, onchange: v => { diff --git a/apps/sched/ChangeLog b/apps/sched/ChangeLog index 634250d48..92b04fb32 100644 --- a/apps/sched/ChangeLog +++ b/apps/sched/ChangeLog @@ -22,3 +22,4 @@ 0.19: Update clock_info to refresh periodically on active alarms/timers 0.20: Alarm dismiss and snooze events 0.21: Fix crash in clock_info +0.22: Dated event repeat option diff --git a/apps/sched/README.md b/apps/sched/README.md index c874b5577..2fb201cee 100644 --- a/apps/sched/README.md +++ b/apps/sched/README.md @@ -45,7 +45,9 @@ Alarms are stored in an array in `sched.json`, and take the form: // eg (new Date()).toISOString().substr(0,10) msg : "Eat food", // message to display. last : 0, // last day of the month we alarmed on - so we don't alarm twice in one day! (No change from 0 on timers) - rp : true, // repeat the alarm every day? + rp : true, // repeat the alarm every day? If date is given, pass an object instead of a boolean, + // e.g. repeat every 2 months: { interval: "month", num: 2 }. + // Supported intervals: day, week, month, year vibrate : "...", // OPTIONAL pattern of '.', '-' and ' ' to use for when buzzing out this alarm (defaults to '..' if not set) hidden : false, // OPTIONAL if false, the widget should not show an icon for this alarm as : false, // auto snooze diff --git a/apps/sched/metadata.json b/apps/sched/metadata.json index 42c3aaa12..1a4a64994 100644 --- a/apps/sched/metadata.json +++ b/apps/sched/metadata.json @@ -1,7 +1,7 @@ { "id": "sched", "name": "Scheduler", - "version": "0.21", + "version": "0.22", "description": "Scheduling library for alarms and timers", "icon": "app.png", "type": "scheduler", diff --git a/apps/sched/sched.js b/apps/sched/sched.js index f2f2644f9..ea4c43443 100644 --- a/apps/sched/sched.js +++ b/apps/sched/sched.js @@ -42,7 +42,9 @@ function showAlarm(alarm) { if (del) { alarms.splice(alarmIndex, 1); } else { - if (!alarm.timer) { + if (alarm.date && alarm.rp) { + setNextRepeatDate(alarm); + } else if (!alarm.timer) { alarm.last = new Date().getDate(); } if (alarm.ot !== undefined) { @@ -78,6 +80,36 @@ function showAlarm(alarm) { }); } + function setNextRepeatDate(alarm) { + let date = new Date(alarm.date); + let rp = alarm.rp; + switch(rp.interval) { + case true: + date.setDate(date.getDate() + 1); + break; + case "day": + date.setDate(date.getDate() + rp.num); + break; + case "week": + date.setDate(date.getDate() + (rp.num * 7)); + break; + case "month": + if (!alarm.od) alarm.od = date.getDate(); + date = new Date(date.getFullYear(), date.getMonth() + rp.num, alarm.od); + if (date.getDate() != alarm.od) date.setDate(0); + break; + case "year": + if (!alarm.od) alarm.od = date.getDate(); + date = new Date(date.getFullYear() + rp.num, date.getMonth(), alarm.od); + if (date.getDate() != alarm.od) date.setDate(0); + break; + default: + console.log(`sched: unknown repeat '${JSON.stringify(rp)}'`); + break; + } + alarm.date = date.toLocalISOString().slice(0,10); + } + if ((require("Storage").readJSON("setting.json", 1) || {}).quiet > 1) return; diff --git a/apps/setting/ChangeLog b/apps/setting/ChangeLog index 8f14d0419..971162691 100644 --- a/apps/setting/ChangeLog +++ b/apps/setting/ChangeLog @@ -64,3 +64,4 @@ of 'Select Clock' 0.57: Settings.log = 0,1,2,3 for off,display,log,both 0.58: On/Off settings items now use checkboxes +0.59: Preserve BLE whitelist even when disabled diff --git a/apps/setting/metadata.json b/apps/setting/metadata.json index ab029f1e5..e62479604 100644 --- a/apps/setting/metadata.json +++ b/apps/setting/metadata.json @@ -1,7 +1,7 @@ { "id": "setting", "name": "Settings", - "version": "0.58", + "version": "0.59", "description": "A menu for setting up Bangle.js", "icon": "settings.png", "tags": "tool,system", diff --git a/apps/setting/settings.js b/apps/setting/settings.js index db3e476b7..5d2a5f7c6 100644 --- a/apps/setting/settings.js +++ b/apps/setting/settings.js @@ -191,7 +191,14 @@ function showBLEMenu() { onchange: () => setTimeout(showPasskeyMenu) // graphical_menu redraws after the call }, /*LANG*/'Whitelist': { - value: settings.whitelist?(settings.whitelist.length+/*LANG*/" devs"):/*LANG*/"off", + value: + ( + settings.whitelist_disabled ? /*LANG*/"off" : /*LANG*/"on" + ) + ( + settings.whitelist + ? " (" + settings.whitelist.length + ")" + : "" + ), onchange: () => setTimeout(showWhitelistMenu) // graphical_menu redraws after the call } }); @@ -341,12 +348,21 @@ function showPasskeyMenu() { function showWhitelistMenu() { var menu = { "< Back" : ()=>showBLEMenu(), - /*LANG*/"Disable" : () => { - settings.whitelist = undefined; + }; + if (settings.whitelist_disabled) { + menu[/*LANG*/"Enable"] = () => { + delete settings.whitelist_disabled; updateSettings(); showBLEMenu(); - } - }; + }; + } else { + menu[/*LANG*/"Disable"] = () => { + settings.whitelist_disabled = true; + updateSettings(); + showBLEMenu(); + }; + } + if (settings.whitelist) settings.whitelist.forEach(function(d){ menu[d.substr(0,17)] = function() { E.showPrompt(/*LANG*/'Remove\n'+d).then((v) => { @@ -366,6 +382,7 @@ function showWhitelistMenu() { NRF.removeAllListeners('connect'); NRF.on('connect', function(addr) { if (!settings.whitelist) settings.whitelist=[]; + delete settings.whitelist_disabled; settings.whitelist.push(addr); updateSettings(); NRF.removeAllListeners('connect'); diff --git a/apps/weather/ChangeLog b/apps/weather/ChangeLog index 0010a58fd..50c033600 100644 --- a/apps/weather/ChangeLog +++ b/apps/weather/ChangeLog @@ -21,3 +21,4 @@ 0.22: Automatic translation of strings, some left untranslated. 0.23: Update clock_info to avoid a redraw 0.24: Redraw clock_info on update and provide color field for condition +0.25: Added monochrome parameter to drawIcon in lib diff --git a/apps/weather/clkinfo.js b/apps/weather/clkinfo.js index ef3b7d139..4e526b977 100644 --- a/apps/weather/clkinfo.js +++ b/apps/weather/clkinfo.js @@ -22,7 +22,7 @@ function weatherIcon(code) { var ovr = Graphics.createArrayBuffer(24,24,1,{msb:true}); - weatherLib.drawIcon({code:code},12,12,12,ovr); + weatherLib.drawIcon({code:code},12,12,12,ovr,true); var img = ovr.asImage(); img.transparent = 0; return img; diff --git a/apps/weather/lib.js b/apps/weather/lib.js index 14ca77ec6..af6aa4d7e 100644 --- a/apps/weather/lib.js +++ b/apps/weather/lib.js @@ -155,14 +155,11 @@ exports.getColor = function(code) { * @param y Top * @param r Icon Size * @param ovr Graphics instance (or undefined for g) + * @param monochrome If true, produce a monochromatic icon */ -exports.drawIcon = function(cond, x, y, r, ovr) { +exports.drawIcon = function(cond, x, y, r, ovr, monochrome) { var palette; - var monochrome=1; - if(!ovr) { - ovr = g; - monochrome=0; - } + if(!ovr) ovr = g; palette = getPalette(monochrome, ovr); diff --git a/apps/weather/metadata.json b/apps/weather/metadata.json index bcb2fe109..55c2973b0 100644 --- a/apps/weather/metadata.json +++ b/apps/weather/metadata.json @@ -1,7 +1,7 @@ { "id": "weather", "name": "Weather", - "version": "0.24", + "version": "0.25", "description": "Show Gadgetbridge weather report", "icon": "icon.png", "screenshots": [{"url":"screenshot.png"}], diff --git a/apps/weatherClock/ChangeLog b/apps/weatherClock/ChangeLog index a6a12c297..f31e15729 100644 --- a/apps/weatherClock/ChangeLog +++ b/apps/weatherClock/ChangeLog @@ -1,5 +1,6 @@ 0.01: New App! 0.02: Minor layout format tweak so it uses less memory and draws ok on Bangle.js 1 (#1012) 0.03: Minor layout extra spaces. -0.04: Layout now compatible with Bangle.js 2 -0.05: Use weather condition code for icon selection +0.04: Layout now compatible with Bangle.js 2. +0.05: Use weather condition code for icon selection. +0.06: WeatherClock icons now reflect weather conditions better. Add settings menu to hide elements and to use weather icons of Weather app. Images placed into functions for performance. \ No newline at end of file diff --git a/apps/weatherClock/README.md b/apps/weatherClock/README.md index f1f146440..a7f44f7f7 100644 --- a/apps/weatherClock/README.md +++ b/apps/weatherClock/README.md @@ -1,12 +1,14 @@ # Weather Clock -A clock which displays the current weather conditions. Temperature, wind speed, and an icon indicating the weather conditions are displayed. +A clock which displays the current weather conditions. Time, day of week, date, temperature, wind speed, and an icon indicating the weather condition are displayed. + +As of Weather Clock v0.06 the date, day of week, temperature, weather icon and/or wind speed can be hidden in Settings. The icons can be changed to those of the Weather app. Standard widgets are displayed. ## Requirements -**This clock requires Gadgetbridge and the weather app in order to get weather data!** +**This clock requires Gadgetbridge and the Weather app in order to get weather data!** See the [Bangle.js Gadgetbridge documentation](https://www.espruino.com/Gadgetbridge) for instructions on setting up Gadgetbridge and weather. diff --git a/apps/weatherClock/app.js b/apps/weatherClock/app.js index 91d0ab36f..4896a9f49 100644 --- a/apps/weatherClock/app.js +++ b/apps/weatherClock/app.js @@ -1,22 +1,36 @@ const Layout = require("Layout"); -const storage = require('Storage'); +const storage = require("Storage"); const locale = require("locale"); +const SETTINGS_FILE = "weatherClock.json"; +let s; +const w = require("weather"); -// weather icons from https://icons8.com/icon/set/weather/color -var sunIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AbhvQC6vd7ouVC4IwUCwIwUFwQwQCYgAHDZQXc9wACC6QWDDAgXN7wXF9oXPCwowDC5guGGAYXMCw4wCC5RGJJAZGTJBiNISIylQVJrLCC5owGF65fXR7AwBC5jvhC7JIILxapDFxAXOGAy9KC4owGBAQXODAgHDC54AHC8T0FAAQSOGg4qPGA4WUGAIuVC7AA/AH4AEA=")); - -var partSunIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AY6AWVhvdC6vd7owUFwIABFiYAFGR4Xa93u9oXTCwIYDC6HeC4fuC56MBC4ySOIwpIQXYQXHmYABRpwXECwQYKF5HjC4kwL5gQCAYYwO7wqFAAowK7wWKJBgXLJBPd6YX/AAoVMAAM/Cw0DC5yRHCx5JGFyAwGCyIwFC/4XyR4inXa64wRFwowQCw4A/AH4AkA")); - -var cloudIcon = require("heatshrink").decompress(atob("mEwwhC/AH4A/AH4AtgczmYWWDCgWDmcwIKAuEGBoSGGCAWKC7BIKIxYX6CpgABn4tUSJIWPJIwuQGAwWRGAoX/C+SPEU67XXGCIuFGCAWHAH4A/AH4A/ADg=")); - -var snowIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AhxGAC9YUBC4QZRhAVBAIWIC6QAEI6IYEI5cIBgwWOC64NCKohHPNox3RBgqnQEo7XPHpKONR5AXYAH4ASLa4XWXILiBC6r5LDBgWWDBRrKC5hsCEacIHawvMCIwvQC5QvQFAROEfZ5ADLJ4YGCywvVI7CPGC9IA/AH4AF")); - -var rainIcon = require("heatshrink").decompress(atob("mEwwhC/AH4AFgczmYWWDCgWDmcwIKAuEGBoSGGCAWKC7BIKIxYX6CpgABn4tUSJIWPJIwuQGAwWRGAoX/C+SPEU67XXGCIuFGCAWHAGeIBJEIwAVJhGIC5AJBC5QMJEJQMEC44JBC6QSCC54FHLxgNBBgYSEDgKpPMhQXneSwuUAH4A/AA4=")); - -var stormIcon = require("heatshrink").decompress(atob("mEwwhC/AFEzmcwCyoYUgYXDmYuVGAY0OFwocHC6pNLCxYXYJBQXuCxhhJRpgYKCyBKFFyIXFCyJIFC/4XaO66nU3eza6k7C4IWFGBwXBCwwwO3ewC5AZMC6RaCIxZiI3e7AYYwRCQIIBC4QwPIQIpDC5owDhYREIxgAEFIouNC4orDFyBGBGAcLC6BaFhYWRLSRIFISQXcCyqhRAH4Az")); - +// Weather icons from https://icons8.com/icon/set/weather/color +function getSun() { + return require("heatshrink").decompress(atob("mEwwhC/AH4AbhvQC6vd7ouVC4IwUCwIwUFwQwQCYgAHDZQXc9wACC6QWDDAgXN7wXF9oXPCwowDC5guGGAYXMCw4wCC5RGJJAZGTJBiNISIylQVJrLCC5owGF65fXR7AwBC5jvhC7JIILxapDFxAXOGAy9KC4owGBAQXODAgHDC54AHC8T0FAAQSOGg4qPGA4WUGAIuVC7AA/AH4AEA=")); +} +function getPartSun() { + return require("heatshrink").decompress(atob("mEwwhC/AH4AY6AWVhvdC6vd7owUFwIABFiYAFGR4Xa93u9oXTCwIYDC6HeC4fuC56MBC4ySOIwpIQXYQXHmYABRpwXECwQYKF5HjC4kwL5gQCAYYwO7wqFAAowK7wWKJBgXLJBPd6YX/AAoVMAAM/Cw0DC5yRHCx5JGFyAwGCyIwFC/4XyR4inXa64wRFwowQCw4A/AH4AkA")); +} +function getCloud() { + return require("heatshrink").decompress(atob("mEwwhC/AH4A/AH4AtgczmYWWDCgWDmcwIKAuEGBoSGGCAWKC7BIKIxYX6CpgABn4tUSJIWPJIwuQGAwWRGAoX/C+SPEU67XXGCIuFGCAWHAH4A/AH4A/ADg=")); +} +function getSnow() { + return require("heatshrink").decompress(atob("mEwwhC/AH4AhxGAC9YUBC4QZRhAVBAIWIC6QAEI6IYEI5cIBgwWOC64NCKohHPNox3RBgqnQEo7XPHpKONR5AXYAH4ASLa4XWXILiBC6r5LDBgWWDBRrKC5hsCEacIHawvMCIwvQC5QvQFAROEfZ5ADLJ4YGCywvVI7CPGC9IA/AH4AF")); +} +function getRain() { + return require("heatshrink").decompress(atob("mEwwhC/AH4AFgczmYWWDCgWDmcwIKAuEGBoSGGCAWKC7BIKIxYX6CpgABn4tUSJIWPJIwuQGAwWRGAoX/C+SPEU67XXGCIuFGCAWHAGeIBJEIwAVJhGIC5AJBC5QMJEJQMEC44JBC6QSCC54FHLxgNBBgYSEDgKpPMhQXneSwuUAH4A/AA4=")); +} +function getStorm() { + return require("heatshrink").decompress(atob("mEwwhC/AFEzmcwCyoYUgYXDmYuVGAY0OFwocHC6pNLCxYXYJBQXuCxhhJRpgYKCyBKFFyIXFCyJIFC/4XaO66nU3eza6k7C4IWFGBwXBCwwwO3ewC5AZMC6RaCIxZiI3e7AYYwRCQIIBC4QwPIQIpDC5owDhYREIxgAEFIouNC4orDFyBGBGAcLC6BaFhYWRLSRIFISQXcCyqhRAH4Az")); +} // err icon - https://icons8.com/icons/set/error -var errIcon = require("heatshrink").decompress(atob("mEwwkBiIA/AH4AZUAIWUiAXBWqgXXdIYuVGCgXBgICCIyYXCJCQTDC6QrEMCQSEJCQRFC6ApGJCCiDDQSpQFAYXEJBqNGJCA/EC4ZIOEwgXFJBgNEAhKlNAgxIKBgoXEJBjsLC5TsIeRycMBhRrMMBKzQEozjOBxAgHGww+IA6wfSH4hnIC47OMSJqlRIJAXCACIXaGoQARPwwuTAH4A/ABw")); +function getErr() { + return require("heatshrink").decompress(atob("mEwwkBiIA/AH4AZUAIWUiAXBWqgXXdIYuVGCgXBgICCIyYXCJCQTDC6QrEMCQSEJCQRFC6ApGJCCiDDQSpQFAYXEJBqNGJCA/EC4ZIOEwgXFJBgNEAhKlNAgxIKBgoXEJBjsLC5TsIeRycMBhRrMMBKzQEozjOBxAgHGww+IA6wfSH4hnIC47OMSJqlRIJAXCACIXaGoQARPwwuTAH4A/ABw")); +} +function getDummy() { + return require("heatshrink").decompress(atob("gMBwMAwA")); +} /** Choose weather icon to display based on condition. @@ -25,32 +39,30 @@ sent from gadget bridge. */ function chooseIcon(condition) { condition = condition.toLowerCase(); - if (condition.includes("thunderstorm")) return stormIcon; + if (condition.includes("thunderstorm")|| + condition.includes("squalls")|| + condition.includes("tornado")) return getStorm; if (condition.includes("freezing")||condition.includes("snow")|| condition.includes("sleet")) { - return snowIcon; + return getSnow; } if (condition.includes("drizzle")|| - condition.includes("shower")) { - return rainIcon; + condition.includes("shower")|| + condition.includes("rain")) return getRain; + if (condition.includes("clear")) return getSun; + if (condition.includes("clouds")) return getCloud; + if (condition.includes("few clouds")|| + condition.includes("scattered clouds")|| + condition.includes("mist")|| + condition.includes("smoke")|| + condition.includes("haze")|| + condition.includes("sand")|| + condition.includes("dust")|| + condition.includes("fog")|| + condition.includes("ash")) { + return getPartSun; } - if (condition.includes("rain")) return rainIcon; - if (condition.includes("clear")) return sunIcon; - if (condition.includes("few clouds")) return partSunIcon; - if (condition.includes("scattered clouds")) return cloudIcon; - if (condition.includes("clouds")) return cloudIcon; - if (condition.includes("mist") || - condition.includes("smoke") || - condition.includes("haze") || - condition.includes("sand") || - condition.includes("dust") || - condition.includes("fog") || - condition.includes("ash") || - condition.includes("squalls") || - condition.includes("tornado")) { - return cloudIcon; - } - return cloudIcon; + return getCloud; } /* @@ -60,55 +72,29 @@ function chooseIcon(condition) { function chooseIconByCode(code) { const codeGroup = Math.round(code / 100); switch (codeGroup) { - case 2: return stormIcon; - case 3: return rainIcon; - case 5: return rainIcon; - case 6: return snowIcon; - case 7: return cloudIcon; + case 2: return getStorm; + case 3: return getRain; + case 5: + switch (code) { + case 511: return getSnow; + default: return getRain; + } + case 6: return getSnow; + case 7: return getPartSun; case 8: switch (code) { - case 800: return sunIcon; - case 801: return partSunIcon; - default: return cloudIcon; + case 800: return getSun; + case 804: return getCloud; + default: return getPartSun; } - default: return cloudIcon; + default: return getCloud; } } -/** -Get weather stored in json file by weather app. -*/ -function getWeather() { - let jsonWeather = storage.readJSON('weather.json'); - return jsonWeather; -} - -var clockLayout = new Layout( { - type:"v", c: [ - {type:"txt", font:"35%", halign: 0, fillx:1, pad: 8, label:"00:00", id:"time" }, - {type: "h", fillx: 1, c: [ - {type:"txt", font:"10%", label:"THU", id:"dow" }, - {type:"txt", font:"10%", label:"01/01/1970", id:"date" } - ] - }, - {type: "h", valign : 1, fillx:1, c: [ - {type: "img", filly: 1, id: "weatherIcon", src: sunIcon}, - {type: "v", fillx:1, c: [ - {type: "h", c: [ - {type: "txt", font: "10%", id: "temp", label: "000 °C"}, - ]}, - {type: "h", c: [ - {type: "txt", font: "10%", id: "wind", label: "00 km/h"}, - ]} - ] - }, - ]}] -}); - -// timeout used to update every minute +// Timeout used to update every minute var drawTimeout; -// schedule a draw for the next minute +// Schedule a draw for the next minute function queueDraw() { if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { @@ -119,37 +105,85 @@ function queueDraw() { function draw() { var date = new Date(); - clockLayout.time.label = locale.time(date, 1); - clockLayout.date.label = locale.date(date, 1).toUpperCase(); - clockLayout.dow.label = locale.dow(date, 1).toUpperCase() + " "; - var weatherJson = getWeather(); - if(weatherJson && weatherJson.weather){ - var currentWeather = weatherJson.weather; - const temp = locale.temp(currentWeather.temp-273.15).match(/^(\D*\d*)(.*)$/); - clockLayout.temp.label = temp[1] + " " + temp[2]; - const code = currentWeather.code || -1; + cLayout.time.label = locale.time(date, 1); + cLayout.dow.label = s.day ? locale.dow(date, 1).toUpperCase() + " " : ""; + cLayout.date.label = s.date ? locale.date(date, 1).toUpperCase() : ""; + let curr = w.get(); // Get weather from weather app. + if(curr){ + const temp = locale.temp(curr.temp-273.15).match(/^(\D*\d*)(.*)$/); + cLayout.temp.label = temp[1] + " " + temp[2]; + const code = curr.code || -1; if (code > 0) { - clockLayout.weatherIcon.src = chooseIconByCode(code); + let showIconC = s.src ? wDrawIcon(curr.code) : chooseIconByCode(curr.code); + cLayout.wIcon.src = s.icon ? showIconC : getDummy; } else { - clockLayout.weatherIcon.src = chooseIcon(currentWeather.txt); + let showIconT = s.src ? wDrawIcon(curr.txt) : chooseIcon(curr.txt); + cLayout.wIcon.src = s.icon ? showIconT : getDummy; } - const wind = locale.speed(currentWeather.wind).match(/^(\D*\d*)(.*)$/); - clockLayout.wind.label = wind[1] + " " + wind[2] + " " + (currentWeather.wrose||'').toUpperCase(); + const wind = locale.speed(curr.wind).match(/^(\D*\d*)(.*)$/); + cLayout.wind.label = wind[1] + " " + wind[2] + " " + (curr.wrose||"").toUpperCase(); } else{ - clockLayout.temp.label = "Err"; - clockLayout.wind.label = "No Data"; - clockLayout.weatherIcon.src = errIcon; + cLayout.temp.label = "Err"; + cLayout.wind.label = "No Data"; + cLayout.wIcon.src = s.icon ? getErr : getDummy; } - clockLayout.clear(); - clockLayout.render(); - // queue draw in one minute + cLayout.clear(); + cLayout.render(); + // Queue draw in one minute queueDraw(); } +// Load settings from file +s = storage.readJSON(SETTINGS_FILE,1)||{}; +s.src = s.src === undefined ? false : s.src; +s.icon = s.icon === undefined ? true : s.icon; +s.day = s.day === undefined ? true : s.day; +s.date = s.date === undefined ? true : s.date; +s.wind = s.wind === undefined ? true : s.wind; + +function wDrawIcon(code) { + var ovr = Graphics.createArrayBuffer(50,50,8,{msb:true}); + if (typeof code == "number") w.drawIcon({code:code},24,24,24,ovr); + if (typeof code == "string") w.drawIcon({txt:code},24,24,24,ovr); + var img = ovr.asImage(); + img.transparent = 0; + return img; +} + +let srcIcons = s.src ? wDrawIcon(800) : getSun; +let srcWeather = s.icon ? srcIcons : getDummy; +let fontTemp = s.wind ? "10%" : "20%"; +let fontWind = s.wind ? "10%" : "0%"; +let labelDay = s.day ? "THU" : ""; +let labelDate = s.date ? "01/01/1970" : ""; +var cLayout = new Layout( { + type:"v", c: [ + {type:"txt", font:"35%", halign: 0, fillx:1, pad: 8, label:"00:00", id:"time" }, + {type: "h", fillx: 1, c: [ + {type: "h", c: [ + {type:"txt", font:"10%", label:labelDay, id:"dow" }, + {type:"txt", font:"10%", label:labelDate, id:"date" } + ]}, + ] + }, + {type: "h", valign : 1, fillx:1, c: [ + {type: "img", filly: 1, pad: 8, id: "wIcon", src: srcWeather}, + {type: "v", fillx:1, c: [ + {type: "h", c: [ + {type: "txt", font: fontTemp, id: "temp", label: "000 °C"}, + ]}, + {type: "h", c: [ + {type: "txt", font: fontWind, id: "wind", label: "00 km/h"}, + ]} + ] + }, + ]}] +}); + g.clear(); Bangle.setUI("clock"); // Show launcher when middle button pressed Bangle.loadWidgets(); Bangle.drawWidgets(); -clockLayout.render(); +cLayout.render(); draw(); diff --git a/apps/weatherClock/metadata.json b/apps/weatherClock/metadata.json index cf8bd899e..270591c74 100644 --- a/apps/weatherClock/metadata.json +++ b/apps/weatherClock/metadata.json @@ -1,9 +1,11 @@ { "id": "weatherClock", "name": "Weather Clock", - "version": "0.05", + "shortName": "Weather Clock", + "version": "0.06", "description": "A clock which displays current weather conditions (requires Gadgetbridge and Weather apps).", "icon": "app.png", + "dependencies": {"weather":"app"}, "screenshots": [{"url":"screens/screen1.png"}], "type": "clock", "tags": "clock, weather", @@ -12,6 +14,8 @@ "readme": "README.md", "storage": [ {"name":"weatherClock.app.js","url":"app.js"}, - {"name":"weatherClock.img","url":"app-icon.js","evaluate":true} - ] + {"name":"weatherClock.img","url":"app-icon.js","evaluate":true}, + {"name":"weatherClock.settings.js","url":"settings.js"} + ], + "data": [{"name":"weatherClock.json"}] } diff --git a/apps/weatherClock/settings.js b/apps/weatherClock/settings.js new file mode 100644 index 000000000..0aa7330c1 --- /dev/null +++ b/apps/weatherClock/settings.js @@ -0,0 +1,58 @@ +(function(back) { + const SETTINGS_FILE = "weatherClock.json"; + + // Load settings file + const storage = require('Storage'); + let settings = storage.readJSON(SETTINGS_FILE, 1) || {}; + let s = {}; + s.date = (settings.date === undefined ? true : settings.date); + s.day = (settings.day === undefined ? true : settings.day); + s.icon = (settings.icon === undefined ? true : settings.icon); + s.wind = (settings.wind === undefined ? true : settings.wind); + s.src = (settings.src === undefined ? false : settings.src); + + function save() { + settings = s + storage.write(SETTINGS_FILE, settings) + } + + E.showMenu({ + '': { 'title': 'Weather Clock' }, + '< Back': back, + 'Show date': { + value: !!s.date, + onchange: v => { + s.date = v; + save(); + }, + }, + 'Show day Of Week': { + value: !!s.day, + onchange: v => { + s.day = v; + save(); + }, + }, + 'Show weather Icon': { + value: !!s.icon, + onchange: v => { + s.icon = v; + save(); + }, + }, + 'Show wind Speed': { + value: !!s.wind, + onchange: v => { + s.wind = v; + save(); + }, + }, + 'Use weather app icons': { + value: !!s.src, + onchange: v => { + s.src = v; + save(); + }, + } + }); +}); diff --git a/apps/widChargingStatus/widget.ts b/apps/widChargingStatus/widget.ts index 02f3cd8d4..a161d5408 100644 --- a/apps/widChargingStatus/widget.ts +++ b/apps/widChargingStatus/widget.ts @@ -6,11 +6,11 @@ ); const iconWidth = 18; - function draw(this: { x: number; y: number }) { + function draw(this: { x?: number; y?: number }) { g.reset(); if (Bangle.isCharging()) { g.setColor('#FD0'); - g.drawImage(icon, this.x + 1, this.y + 1, { + g.drawImage(icon, this.x! + 1, this.y! + 1, { scale: 0.6875, }); } diff --git a/apps/widalarmeta/ChangeLog b/apps/widalarmeta/ChangeLog index 4bcf6ec69..2b74766c8 100644 --- a/apps/widalarmeta/ChangeLog +++ b/apps/widalarmeta/ChangeLog @@ -8,3 +8,4 @@ 0.05: Convert Yes/No On/Off in settings to checkboxes 0.06: Remember next alarm to reduce calculation amount Redraw only every hour when no alarm in next 24h +0.07: Fix when no alarms are present diff --git a/apps/widalarmeta/metadata.json b/apps/widalarmeta/metadata.json index a3d2e8adb..6b3d8978b 100644 --- a/apps/widalarmeta/metadata.json +++ b/apps/widalarmeta/metadata.json @@ -2,7 +2,7 @@ "id": "widalarmeta", "name": "Alarm & Timer ETA", "shortName": "Alarm ETA", - "version": "0.06", + "version": "0.07", "description": "A widget that displays the time to the next Alarm or Timer in hours and minutes, maximum 24h (configurable).", "icon": "widget.png", "type": "widget", diff --git a/apps/widalarmeta/widget.js b/apps/widalarmeta/widget.js index 750ae5d98..0104eb3b1 100644 --- a/apps/widalarmeta/widget.js +++ b/apps/widalarmeta/widget.js @@ -9,13 +9,15 @@ function getNextAlarm(date) { const alarms = (require("Storage").readJSON("sched.json",1) || []).filter(alarm => alarm.on && alarm.hidden !== true); WIDGETS["widalarmeta"].numActiveAlarms = alarms.length; - const times = alarms.map(alarm => require("sched").getTimeToAlarm(alarm, date) || Number.POSITIVE_INFINITY); - const eta = times.length > 0 ? Math.min.apply(null, times) : 0; - if (eta !== Number.POSITIVE_INFINITY) { - const idx = times.indexOf(eta); - const alarm = alarms[idx]; - delete alarm.msg; delete alarm.id; delete alarm.data; // free some memory - return alarm; + if (alarms.length > 0) { + const times = alarms.map(alarm => require("sched").getTimeToAlarm(alarm, date) || Number.POSITIVE_INFINITY); + const eta = Math.min.apply(null, times); + if (eta !== Number.POSITIVE_INFINITY) { + const idx = times.indexOf(eta); + const alarm = alarms[idx]; + delete alarm.msg; delete alarm.id; delete alarm.data; // free some memory + return alarm; + } } } // getNextAlarm diff --git a/apps/widbtstates/ChangeLog b/apps/widbtstates/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/widbtstates/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/widbtstates/metadata.json b/apps/widbtstates/metadata.json new file mode 100644 index 000000000..ada530e1b --- /dev/null +++ b/apps/widbtstates/metadata.json @@ -0,0 +1,14 @@ +{ + "id": "widbtstates", + "name": "Bluetooth States", + "version": "0.01", + "description": "If active, shows a white bluetooth icon, if connected, a blue one (nothing if sleeping)", + "icon": "widget.png", + "type": "widget", + "tags": "widget,bluetooth,clkinfo", + "provides_widgets" : ["bluetooth"], + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name":"widbtstates.wid.js","url":"widget.js"} + ] +} diff --git a/apps/widbtstates/widget.js b/apps/widbtstates/widget.js new file mode 100644 index 000000000..1d32e1bc4 --- /dev/null +++ b/apps/widbtstates/widget.js @@ -0,0 +1,53 @@ +"use strict"; +(function () { + "ram"; + var _a; + var state = (function () { + var status = NRF.getSecurityStatus(); + if (status.connected) + return 2; + if (status.advertising) + return 1; + return 0; + })(); + var width = function () { return state > 0 ? 15 : 0; }; + var update = function (newState) { + state = newState; + WIDGETS["bluetooth"].width = width(); + setTimeout(Bangle.drawWidgets, 50); + }; + var colours = (_a = {}, + _a[1] = { + false: "#fff", + true: "#fff", + }, + _a[2] = { + false: "#0ff", + true: "#00f", + }, + _a); + WIDGETS["bluetooth"] = { + area: "tl", + sortorder: -1, + draw: function () { + if (state == 0) + return; + g.reset(); + g.setColor(colours[state]["".concat(g.theme.dark)]); + g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), this.x + 2, this.y + 2); + }, + width: width(), + }; + NRF.on("connect", update.bind(null, 2)); + NRF.on("disconnect", update.bind(null, 1)); + var origWake = NRF.wake; + var origSleep = NRF.sleep; + NRF.wake = function () { + update(1); + return origWake.apply(this, arguments); + }; + NRF.sleep = function () { + update(0); + return origSleep.apply(this, arguments); + }; +})(); diff --git a/apps/widbtstates/widget.png b/apps/widbtstates/widget.png new file mode 100644 index 000000000..1a884a62c Binary files /dev/null and b/apps/widbtstates/widget.png differ diff --git a/apps/widbtstates/widget.ts b/apps/widbtstates/widget.ts new file mode 100644 index 000000000..8f02c1b8c --- /dev/null +++ b/apps/widbtstates/widget.ts @@ -0,0 +1,77 @@ +(() => { + "ram"; + + const enum State { + Asleep, + Active, + Connected + } + + let state: State = (() => { + const status = NRF.getSecurityStatus(); + + if (status.connected) return State.Connected; + if (status.advertising) return State.Active; + return State.Asleep; + })(); + + const width = () => state > State.Asleep ? 15 : 0; + + const update = (newState: State) => { + state = newState; + WIDGETS["bluetooth"]!.width = width(); + setTimeout(Bangle.drawWidgets, 50); // no need for .bind() + }; + + type DarkTheme = `${boolean}`; + const colours: { + [key in State.Active | State.Connected]: { + [key in DarkTheme]: ColorResolvable + } + } = { + [State.Active]: { + false: "#fff", + true: "#fff", + }, + [State.Connected]: { + false: "#0ff", + true: "#00f", + }, + }; + + WIDGETS["bluetooth"] = { + area: "tl", + sortorder: -1, + draw: function() { + if (state == State.Asleep) + return; + + g.reset(); + + g.setColor(colours[state][`${g.theme.dark}`]); + + g.drawImage( + atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="), + this.x! + 2, + this.y! + 2 + ); + }, + width: width(), + }; + + NRF.on("connect", update.bind(null, State.Connected)); + NRF.on("disconnect", update.bind(null, State.Active)); + + const origWake = NRF.wake; + const origSleep = NRF.sleep; + + NRF.wake = function() { + update(State.Active); + return origWake.apply(this, arguments); + }; + + NRF.sleep = function() { + update(State.Asleep); + return origSleep.apply(this, arguments); + }; +})(); diff --git a/apps/widlockunlock/ChangeLog b/apps/widlockunlock/ChangeLog index b4d1ae593..b5efcaa86 100644 --- a/apps/widlockunlock/ChangeLog +++ b/apps/widlockunlock/ChangeLog @@ -1 +1,2 @@ 0.01: First commit +0.02: Add tap-to-lock functionality diff --git a/apps/widlockunlock/metadata.json b/apps/widlockunlock/metadata.json index d701279b9..cc4fa76cd 100644 --- a/apps/widlockunlock/metadata.json +++ b/apps/widlockunlock/metadata.json @@ -1,8 +1,8 @@ { "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", + "version": "0.02", + "description": "On devices with always-on display (Bangle.js 2) this displays lock icon whenever the display is locked, or an unlock icon otherwise. Tap to lock the lcd", "icon": "widget.png", "type": "widget", "tags": "widget,lock", diff --git a/apps/widlockunlock/widget.js b/apps/widlockunlock/widget.js index 0716a9edf..cfbbc87a3 100644 --- a/apps/widlockunlock/widget.js +++ b/apps/widlockunlock/widget.js @@ -1,5 +1,28 @@ -Bangle.on("lockunlock", function() { - Bangle.drawWidgets(); +Bangle.on("lock", () => Bangle.drawWidgets()); + +Bangle.on('touch', (_btn, xy) => { + const oversize = 5; + + const w = WIDGETS.lockunlock; + + const x = xy.x; + const y = xy.y; + + if(w.x - oversize <= x && x < w.x + 14 + oversize + && w.y - oversize <= y && y < w.y + 24 + oversize) + { + Bangle.setLocked(true); + + const backlightTimeout = Bangle.getOptions().backlightTimeout; // ms + + // seems to be a race/if we don't give the firmware enough time, + // it won't timeout the backlight and we'll restore it in our setTimeout below + Bangle.setOptions({ backlightTimeout: 100 }); + + setTimeout(() => { + Bangle.setOptions({ backlightTimeout }); + }, 300); + } }); 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/bin/lib/apploader.js b/bin/lib/apploader.js index 329ece7b7..c1aaa5389 100644 --- a/bin/lib/apploader.js +++ b/bin/lib/apploader.js @@ -26,8 +26,10 @@ var device = { id : DEVICEID, appsInstalled : [] }; // call with {DEVICEID:"BANGLEJS/BANGLEJS2"} exports.init = function(options) { - if (options.DEVICEID) + if (options.DEVICEID) { DEVICEID = options.DEVICEID; + device.id = options.DEVICEID; + } // Load app metadata var dirs = require("fs").readdirSync(APPSDIR, {withFileTypes: true}); dirs.forEach(dir => { diff --git a/core b/core index 0d02ff376..2bb1e55d3 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 0d02ff3763783d166ff84906af038420736aabfc +Subproject commit 2bb1e55d32d640312fe08cf54de1cd9c498cd31e diff --git a/typescript/types/main.d.ts b/typescript/types/main.d.ts index a48ce7565..944d55326 100644 --- a/typescript/types/main.d.ts +++ b/typescript/types/main.d.ts @@ -83,7 +83,10 @@ type WidgetArea = "tl" | "tr" | "bl" | "br"; type Widget = { area: WidgetArea; width: number; - draw: (this: { x: number; y: number }) => void; + sortorder?: number; + draw: (this: Widget, w: Widget) => void; + x?: number; + y?: number; }; declare const WIDGETS: { [key: string]: Widget }; @@ -180,6 +183,23 @@ type NRFFilters = { manufacturerData?: object; }; +type NRFSecurityStatus = { + advertising: boolean, +} & ( + { + connected: true, + encrypted: boolean, + mitm_protected: boolean, + bonded: boolean, + connected_addr?: string, + } | { + connected: false, + encrypted: false, + mitm_protected: false, + bonded: false, + } +); + type ImageObject = { width: number; height: number; @@ -718,7 +738,7 @@ declare class NRF { * @returns {any} An object * @url http://www.espruino.com/Reference#l_NRF_getSecurityStatus */ - static getSecurityStatus(): any; + static getSecurityStatus(): NRFSecurityStatus; /** * @returns {any} An object @@ -1895,6 +1915,7 @@ declare class NRF { * encrypted // Communication on this link is encrypted. * mitm_protected // The encrypted communication is also protected against man-in-the-middle attacks. * bonded // The peer is bonded with us + * advertising // Are we currently advertising? * connected_addr // If connected=true, the MAC address of the currently connected device * } * ``` @@ -1903,7 +1924,7 @@ declare class NRF { * @returns {any} An object * @url http://www.espruino.com/Reference#l_NRF_getSecurityStatus */ - static getSecurityStatus(): any; + static getSecurityStatus(): NRFSecurityStatus; /** * @@ -4550,7 +4571,7 @@ declare class BluetoothRemoteGATTServer { * @returns {any} An object * @url http://www.espruino.com/Reference#l_BluetoothRemoteGATTServer_getSecurityStatus */ - getSecurityStatus(): any; + getSecurityStatus(): NRFSecurityStatus; /** * See `NRF.connect` for usage examples.