diff --git a/apps/90sclk/metadata.json b/apps/90sclk/metadata.json index bfbb6b080..b4b320a1b 100644 --- a/apps/90sclk/metadata.json +++ b/apps/90sclk/metadata.json @@ -14,5 +14,6 @@ {"name":"90sclk.app.js","url":"app.js"}, {"name":"90sclk.img","url":"app-icon.js","evaluate":true}, {"name":"90sclk.settings.js","url":"settings.js"} - ] + ], + "data": [{"name":"90sclk.setting.json"}] } diff --git a/apps/activepedom/metadata.json b/apps/activepedom/metadata.json index 81bafb573..fa1044d85 100644 --- a/apps/activepedom/metadata.json +++ b/apps/activepedom/metadata.json @@ -14,5 +14,6 @@ {"name":"activepedom.settings.js","url":"settings.js"}, {"name":"activepedom.img","url":"app-icon.js","evaluate":true}, {"name":"activepedom.app.js","url":"app.js"} - ] + ], + "data":[{"name":"activepedom.settings.json"}] } diff --git a/apps/agenda/ChangeLog b/apps/agenda/ChangeLog index 9e7151e1e..836810638 100644 --- a/apps/agenda/ChangeLog +++ b/apps/agenda/ChangeLog @@ -13,3 +13,4 @@ Added dynamic, short and range fields to clkinfo 0.12: Added color field and updating clkinfo periodically (running events) 0.13: Show day of the week in date +0.14: Fixed "Today" and "Yesterday" wrongly displayed for allDay events on some time zones diff --git a/apps/agenda/agenda.js b/apps/agenda/agenda.js index 6d2b783fd..814525a2e 100644 --- a/apps/agenda/agenda.js +++ b/apps/agenda/agenda.js @@ -38,13 +38,12 @@ function formatDay(date) { if (!settings.useToday) { return formattedDate; } - const dateformatted = date.toISOString().split('T')[0]; // yyyy-mm-dd - const today = new Date(Date.now()).toISOString().split('T')[0]; // yyyy-mm-dd - if (dateformatted == today) { + const today = new Date(Date.now()); + if (date.getDay() == today.getDay() && date.getMonth() == today.getMonth()) return /*LANG*/"Today "; - } else { - const tomorrow = new Date(Date.now() + 86400 * 1000).toISOString().split('T')[0]; // yyyy-mm-dd - if (dateformatted == tomorrow) { + else { + const tomorrow = new Date(Date.now() + 86400 * 1000); + if (date.getDay() == tomorrow.getDay() && date.getMonth() == tomorrow.getMonth()) { return /*LANG*/"Tomorrow "; } return formattedDate; diff --git a/apps/agenda/metadata.json b/apps/agenda/metadata.json index 737568cb5..2d5864145 100644 --- a/apps/agenda/metadata.json +++ b/apps/agenda/metadata.json @@ -1,7 +1,7 @@ { "id": "agenda", "name": "Agenda", - "version": "0.13", + "version": "0.14", "description": "Simple agenda", "icon": "agenda.png", "screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}], diff --git a/apps/alyxclock/ChangeLog b/apps/alyxclock/ChangeLog new file mode 100644 index 000000000..9db0e26c5 --- /dev/null +++ b/apps/alyxclock/ChangeLog @@ -0,0 +1 @@ +0.01: first release diff --git a/apps/alyxclock/README.md b/apps/alyxclock/README.md new file mode 100644 index 000000000..7765a878f --- /dev/null +++ b/apps/alyxclock/README.md @@ -0,0 +1,4 @@ +# Half-Life Alyx Style clock + +![](screenshot_alyxclock.png) + diff --git a/apps/alyxclock/alyxclock.app.js b/apps/alyxclock/alyxclock.app.js new file mode 100644 index 000000000..57b7d7f48 --- /dev/null +++ b/apps/alyxclock/alyxclock.app.js @@ -0,0 +1,174 @@ +const icoH = [ + [0,1,1,0,0,1,1,0], + [1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1], + [1,1,1,1,1,1,1,1], + [0,1,1,1,1,1,1,0], + [0,0,1,1,1,1,0,0], + [0,0,0,1,1,0,0,0], + [0,0,0,0,0,0,0,0], +] + +const icoR = [ + [0,0,0,0,1,1,1,1,0,0,0,0], + [0,0,1,1,0,0,0,0,1,1,0,0], + [0,1,1,1,1,0,0,1,1,0,1,0], + [0,1,1,0,0,0,0,0,0,0,1,0], + [1,1,1,1,1,1,1,1,0,0,0,1], + [1,1,0,0,1,0,0,0,0,0,0,1], + [1,1,1,1,1,1,1,0,1,1,0,1], + [1,1,1,1,1,1,0,0,0,0,1,1], + [0,1,1,1,1,1,1,1,1,1,1,0], + [0,1,1,1,1,1,1,1,1,1,1,0], + [0,0,1,1,1,1,1,1,1,1,0,0], + [0,0,0,0,1,1,1,1,0,0,0,0], +] + +let idTimeout = null; + +function icon (icon, x, y, size, gap) { + const color = g.getColor(); + for (let r=0; rm.id=="nav")) + event.t = "modify"; } else { event.t="remove"; } @@ -229,6 +231,7 @@ //send the request var req = {t: "http", url:url, id:options.id}; if (options.xpath) req.xpath = options.xpath; + if (options.return) req.return = options.return; // for xpath if (options.method) req.method = options.method; if (options.body) req.body = options.body; if (options.headers) req.headers = options.headers; diff --git a/apps/android/metadata.json b/apps/android/metadata.json index 30890f12a..8489570f7 100644 --- a/apps/android/metadata.json +++ b/apps/android/metadata.json @@ -2,7 +2,7 @@ "id": "android", "name": "Android Integration", "shortName": "Android", - "version": "0.27", + "version": "0.29", "description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.", "icon": "app.png", "tags": "tool,system,messages,notifications,gadgetbridge", diff --git a/apps/boxclk/ChangeLog b/apps/boxclk/ChangeLog index ba46af04e..cc73fbc08 100644 --- a/apps/boxclk/ChangeLog +++ b/apps/boxclk/ChangeLog @@ -1,2 +1,5 @@ 0.01: New App! 0.02: New config options such as step, meridian, short/long formats, custom prefix/suffix +0.03: Allows showing the month in short or long format by setting `"shortMonth"` to true or false +0.04: Improves touchscreen drag handling for background apps such as Pattern Launcher +0.05: Fixes step count not resetting after a new day starts diff --git a/apps/boxclk/README.md b/apps/boxclk/README.md index 1dc8ef98f..c72d932a4 100644 --- a/apps/boxclk/README.md +++ b/apps/boxclk/README.md @@ -18,7 +18,7 @@ Each box can be customized extensively via a simple JSON configuration. You can ## Config File Structure -Here's what an example configuration might look like: +Here's an example of what a configuration might contain: ``` { @@ -37,8 +37,9 @@ Here's what an example configuration might look like: "boxPos": { "x": 0.5, "y": 0.5 }, "prefix": "", // Adds a string to the beginning of the main string "suffix": "", // Adds a string to the end of the main string - "disableSuffix": true, // Only used to remove the DayOfMonth suffix - "short": false // Gets long format value of time, meridian, date, or DoW + "disableSuffix": true, // Use to remove DayOfMonth suffix only + "short": false, // Use long format of time, meridian, date, or DoW + "shortMonth": false // Use long format of month within date }, "bg": { // Can also be removed for no background @@ -51,9 +52,9 @@ __Breakdown of Parameters:__ * **Box Name:** The name of your text box. Box Clock includes functional support for "time", "date", "meridian" (AM/PM), "dow" (Day of Week), "batt" (Battery), and "step" (Step count). You can add additional custom boxes with unique titles. -* **string:** The text string to be displayed inside the box. +* **string:** The text string to be displayed inside the box. This is only required for custom Box Names. -* **font:** The font name given to g.setFont() +* **font:** The font name given to g.setFont(). * **fontSize:** The size of the font. @@ -75,17 +76,34 @@ __Breakdown of Parameters:__ * **suffix:** Adds a string to the end of the main string. For example, you can set "suffix": "%" to display "80%" for the battery percentage. -* **disableSuffix:** Applies only to the "date" box. Set to true to disable the DayOfMonth suffix. This is used to remove the "st","nd","rd", or "th" from the DayOfMonth number +* **disableSuffix:** Applies only to the "date" box. Set to true to disable the DayOfMonth suffix. This is used to remove the "st","nd","rd", or "th" from the DayOfMonth number. -* **short:** Set to false to get the long format value of time, meridian, date, or DayOfWeek. Short formats are used by default, +* **short:** Set to false to get the long format value of time, meridian, date, or DayOfWeek. Short formats are used by default if not specified. + +* **shortMonth:** Applies only to the "date" box. Set to false to get the long format value of the month. Short format is used by default if not specified. * **bg:** This specifies a custom background image, with the img property defining the name of the image file on the Bangle.js storage. ## Multiple Configurations -The app includes a settings menu that allows you to switch between different configurations. The selected configuration is stored in the default JSON file alongside the other configuration data using the selectedConfig property. +__Settings Menu:__ -If the selectedConfig property is not present or is set to 0, the app will use the default configuration. To create additional configurations, create separate JSON files with the naming convention boxclk-N.json, where N is the configuration number. The settings menu will list all available configurations. +The app includes a settings menu that allows you to switch between different configurations. The selected configuration is stored as a number in the default `boxclk.json` file using the selectedConfig property. + +If the selectedConfig property is not present or is set to 0, the app will use the default configuration. To create additional configurations, create separate JSON files with the naming convention `boxclk-N.json`, where `N` is the configuration number. The settings menu will list all available configurations. + +## Example Configs: + +To easily try out other configs, download and place the JSON configs and/or background images from below onto your Bangle.js storage. Then go to the Box Clock settings menu to select the new config number. You can also modify them to suit your personal preferences. + +__Space Theme:__ + +- **Config:** [boxclk-1.json](https://github.com/espruino/BangleApps/tree/master/apps/boxclk/boxclk-1.json) +- **Background:** [boxclk.space.img](https://github.com/espruino/BangleApps/tree/master/apps/boxclk/boxclk.space.img) ([Original Source](https://www.pixilart.com/art/fallin-from-outer-space-sr2e0c1a705749a)) + +__System Color Theme:__ + +- **Config:** [boxclk-2.json](https://github.com/espruino/BangleApps/tree/master/apps/boxclk/boxclk-2.json) ## Compatibility diff --git a/apps/boxclk/app.js b/apps/boxclk/app.js index 41636e1ef..12c69e789 100644 --- a/apps/boxclk/app.js +++ b/apps/boxclk/app.js @@ -4,6 +4,7 @@ * 1. Module dependencies and initial configurations * --------------------------------------------------------------- */ + let storage = require("Storage"); let locale = require("locale"); let widgets = require("widget_utils"); @@ -30,6 +31,7 @@ * 2. Graphical and visual configurations * --------------------------------------------------------------- */ + let w = g.getWidth(); let h = g.getHeight(); let totalWidth, totalHeight; @@ -40,6 +42,7 @@ * 3. Touchscreen Handlers * --------------------------------------------------------------- */ + let touchHandler; let dragHandler; let movementDistance = 0; @@ -49,6 +52,7 @@ * 4. Font loading function * --------------------------------------------------------------- */ + let loadCustomFont = function() { Graphics.prototype.setFontBrunoAce = function() { // Actual height 23 (24 - 2) @@ -66,6 +70,7 @@ * 5. Initial settings of boxes and their positions * --------------------------------------------------------------- */ + for (let key in boxesConfig) { if (key === 'bg' && boxesConfig[key].img) { bgImage = storage.read(boxesConfig[key].img); @@ -167,14 +172,15 @@ * 7. String forming helper functions * --------------------------------------------------------------- */ + let isBool = function(val, defaultVal) { return typeof val !== 'undefined' ? Boolean(val) : defaultVal; }; - let getDate = function(short, disableSuffix) { + let getDate = function(short, shortMonth, disableSuffix) { const date = new Date(); const dayOfMonth = date.getDate(); - const month = short ? locale.month(date, 0) : locale.month(date, 1); + const month = shortMonth ? locale.month(date, 1) : locale.month(date, 0); const year = date.getFullYear(); let suffix; if ([1, 21, 31].includes(dayOfMonth)) { @@ -211,6 +217,7 @@ * 8. Main draw function * --------------------------------------------------------------- */ + let draw = (function() { let updatePerMinute = true; // variable to track the state of time display @@ -228,7 +235,12 @@ boxes.meridian.string = modString(boxes.meridian, locale.meridian(date, isBool(boxes.meridian.short, true))); } if (boxes.date) { - boxes.date.string = modString(boxes.date, getDate(isBool(boxes.date.short, true), isBool(boxes.date.disableSuffix, false))); + boxes.date.string = ( + modString(boxes.date, + getDate(isBool(boxes.date.short, true), + isBool(boxes.date.shortMonth, true), + isBool(boxes.date.disableSuffix, false) + ))); } if (boxes.dow) { boxes.dow.string = modString(boxes.dow, getDayOfWeek(date, isBool(boxes.dow.short, true))); @@ -237,7 +249,7 @@ boxes.batt.string = modString(boxes.batt, E.getBattery()); } if (boxes.step) { - boxes.step.string = modString(boxes.step, Bangle.getStepCount()); + boxes.step.string = modString(boxes.step, Bangle.getHealthStatus("day").steps); } boxKeys.forEach((boxKey) => { let boxItem = boxes[boxKey]; @@ -267,6 +279,7 @@ * 9. Helper function for touch event * --------------------------------------------------------------- */ + let touchInText = function(e, boxItem, boxKey) { calcBoxSize(boxItem); const pos = calcBoxPos(boxKey); @@ -291,6 +304,7 @@ * 10. Setup function to configure event handlers * --------------------------------------------------------------- */ + let setup = function() { // ------------------------------------ // Define the touchHandler function @@ -338,6 +352,8 @@ // Define the dragHandler function // ------------------------------------ dragHandler = function(e) { + // Check if any box is being dragged + if (!Object.values(isDragging).some(Boolean)) return; // Calculate the movement distance movementDistance += Math.abs(e.dx) + Math.abs(e.dy); // Check if the movement distance exceeds a threshold @@ -391,8 +407,9 @@ * 11. Main execution part * --------------------------------------------------------------- */ + Bangle.loadWidgets(); widgets.swipeOn(); modSetColor(); setup(); -} +} \ No newline at end of file diff --git a/apps/boxclk/boxclk-1.json b/apps/boxclk/boxclk-1.json new file mode 100644 index 000000000..99e225f04 --- /dev/null +++ b/apps/boxclk/boxclk-1.json @@ -0,0 +1,88 @@ +{ + "time": { + "font": "6x8", + "fontSize": 3, + "outline": 2, + "color": "#0ff", + "outlineColor": "#00f", + "border": "#0f0", + "xPadding": -1, + "yPadding": -2.5, + "xOffset": 2, + "yOffset": 0, + "boxPos": { + "x": "0.33", + "y": "0.29" + } + }, + "meridian": { + "font": "6x8", + "fontSize": 2, + "outline": 1, + "color": "#FF9900", + "outlineColor": "fg", + "border": "#0ff", + "xPadding": -0.5, + "yPadding": -1.5, + "xOffset": 2, + "yOffset": 1, + "boxPos": { + "x": "0.34", + "y": "0.46" + }, + "short": false + }, + "dow": { + "font": "6x8", + "fontSize": 2, + "outline": 1, + "color": "#000", + "outlineColor": "#fff", + "border": "#0f0", + "xPadding": -0.5, + "yPadding": -0.5, + "xOffset": 1, + "yOffset": 1, + "boxPos": { + "x": "0.5", + "y": "0.82" + } + }, + "step": { + "font": "6x8", + "fontSize": 2, + "outline": 1, + "color": "#000", + "outlineColor": "#fff", + "border": "#0f0", + "xPadding": -0.5, + "yPadding": 0.5, + "xOffset": 1, + "yOffset": 1, + "boxPos": { + "x": "0.5", + "y": "0.71" + }, + "prefix": "Steps: " + }, + "batt": { + "font": "4x6", + "fontSize": 2, + "outline": 1, + "color": "#0ff", + "outlineColor": "#00f", + "border": "#0f0", + "xPadding": -0.5, + "yPadding": -0.5, + "xOffset": 1, + "yOffset": 1, + "boxPos": { + "x": "0.87", + "y": "0.87" + }, + "suffix": "%" + }, + "bg": { + "img": "boxclk.space.img" + } +} diff --git a/apps/boxclk/boxclk-2.json b/apps/boxclk/boxclk-2.json new file mode 100644 index 000000000..64b842f1c --- /dev/null +++ b/apps/boxclk/boxclk-2.json @@ -0,0 +1,87 @@ +{ + "time": { + "font": "6x8", + "fontSize": 5, + "outline": 3, + "color": "bgH", + "outlineColor": "fg", + "border": "#f0f", + "xPadding": -2, + "yPadding": -4.5, + "xOffset": 3, + "yOffset": 0, + "boxPos": { + "x": "0.5", + "y": "0.33" + } + }, + "dow": { + "font": "6x8", + "fontSize": 3, + "outline": 1, + "color": "#5ccd73", + "outlineColor": "fg", + "border": "#f0f", + "xPadding": -1, + "yPadding": 0.5, + "xOffset": 2, + "yOffset": 0, + "boxPos": { + "x": "0.5", + "y": "0.57" + }, + "short": false + }, + "date": { + "font": "6x8", + "fontSize": 2, + "outline": 1, + "color": "#5ccd73", + "outlineColor": "fg", + "border": "#f0f", + "xPadding": -0.5, + "yPadding": 0.5, + "xOffset": 1, + "yOffset": 0, + "boxPos": { + "x": "0.5", + "y": "0.75" + }, + "shortMonth": false, + "disableSuffix": true + }, + "step": { + "font": "4x6", + "fontSize": 3, + "outline": 2, + "color": "bgH", + "outlineColor": "fg", + "border": "#f0f", + "xPadding": -1, + "yPadding": 0.5, + "xOffset": 2, + "yOffset": 1, + "boxPos": { + "x": "0.5", + "y": "0.92" + }, + "prefix": "Steps: " + }, + "batt": { + "font": "4x6", + "fontSize": 3, + "outline": 2, + "color": "bgH", + "outlineColor": "fg", + "border": "#f0f", + "xPadding": -1, + "yPadding": -1, + "xOffset": 2, + "yOffset": 2, + "boxPos": { + "x": "0.85", + "y": "0.08" + }, + "suffix": "%" + } +} diff --git a/apps/boxclk/boxclk.space.img b/apps/boxclk/boxclk.space.img new file mode 100644 index 000000000..1708b5c24 Binary files /dev/null and b/apps/boxclk/boxclk.space.img differ diff --git a/apps/boxclk/metadata.json b/apps/boxclk/metadata.json index 9b759def7..dd81ac436 100644 --- a/apps/boxclk/metadata.json +++ b/apps/boxclk/metadata.json @@ -1,12 +1,13 @@ { "id": "boxclk", "name": "Box Clock", - "version": "0.02", + "version": "0.05", "description": "A customizable clock with configurable text boxes that can be positioned to show your favorite background", "icon": "app.png", "screenshots": [ {"url":"screenshot.png"}, - {"url":"screenshot-1.png"} + {"url":"screenshot-1.png"}, + {"url":"screenshot-2.png"} ], "type": "clock", "tags": "clock", diff --git a/apps/boxclk/screenshot-1.png b/apps/boxclk/screenshot-1.png index 18798bb30..c6e22d262 100644 Binary files a/apps/boxclk/screenshot-1.png and b/apps/boxclk/screenshot-1.png differ diff --git a/apps/boxclk/screenshot-2.png b/apps/boxclk/screenshot-2.png new file mode 100644 index 000000000..b7a73d66a Binary files /dev/null and b/apps/boxclk/screenshot-2.png differ diff --git a/apps/btadv/app.js b/apps/btadv/app.js index 67899370e..670691fb9 100644 --- a/apps/btadv/app.js +++ b/apps/btadv/app.js @@ -1,411 +1,413 @@ -var __assign = Object.assign; -var Layout = require("Layout"); -Bangle.loadWidgets(); -Bangle.drawWidgets(); -var HRM_MIN_CONFIDENCE = 75; -var services = ["0x180d", "0x181a", "0x1819"]; -var acc; -var bar; -var gps; -var hrm; -var hrmAny; -var mag; -var btnsShown = false; -var prevBtnsShown = undefined; -var hrmAnyClear; -var settings = { - bar: false, - gps: false, - hrm: false, - mag: false, -}; -var idToName = { - acc: "Acceleration", - bar: "Barometer", - gps: "GPS", - hrm: "HRM", - mag: "Magnetometer", -}; -var infoFont = "6x8:2"; -var colour = { - on: "#0f0", - off: "#fff", -}; -var makeToggle = function (id) { return function () { - settings[id] = !settings[id]; - var entry = btnLayout[id]; - var col = settings[id] ? colour.on : colour.off; - entry.btnBorder = entry.col = col; - btnLayout.update(); - btnLayout.render(); - enableSensors(); -}; }; -var btnStyle = { - font: "Vector:14", - fillx: 1, - filly: 1, - col: g.theme.fg, - bgCol: g.theme.bg, - btnBorder: "#fff", -}; -var btnLayout = new Layout({ - type: "v", - c: [ - { - type: "h", - c: [ - __assign({ type: "btn", label: idToName.bar, id: "bar", cb: makeToggle('bar') }, btnStyle), - __assign({ type: "btn", label: idToName.gps, id: "gps", cb: makeToggle('gps') }, btnStyle), - ] - }, - { - type: "h", - c: [ - __assign({ type: "btn", label: idToName.hrm, id: "hrm", cb: makeToggle('hrm') }, btnStyle), - __assign({ type: "btn", label: idToName.mag, id: "mag", cb: makeToggle('mag') }, btnStyle), - ] - }, - { - type: "h", - c: [ - __assign(__assign({ type: "btn", label: idToName.acc, id: "acc", cb: function () { } }, btnStyle), { col: colour.on, btnBorder: colour.on }), - __assign({ type: "btn", label: "Back", cb: function () { - setBtnsShown(false); - } }, btnStyle), - ] - } - ] -}, { - lazy: true, - back: function () { - setBtnsShown(false); - }, -}); -var setBtnsShown = function (b) { - btnsShown = b; - hook(!btnsShown); - setIntervals(); - redraw(); -}; -var drawInfo = function (force) { - var _a = Bangle.appRect, y = _a.y, x = _a.x, w = _a.w; - var mid = x + w / 2; - var drawn = false; - if (!force && !bar && !gps && !hrm && !mag) - return; - g.reset() - .clearRect(Bangle.appRect) - .setFont(infoFont) - .setFontAlign(0, -1); - if (bar) { - g.drawString("".concat(bar.altitude.toFixed(1), "m"), mid, y); - y += g.getFontHeight(); - g.drawString("".concat(bar.pressure.toFixed(1), " hPa"), mid, y); - y += g.getFontHeight(); - g.drawString("".concat(bar.temperature.toFixed(1), "C"), mid, y); - y += g.getFontHeight(); - drawn = true; - } - if (gps) { - g.drawString("".concat(gps.lat.toFixed(4), " lat, ").concat(gps.lon.toFixed(4), " lon"), mid, y); - y += g.getFontHeight(); - g.drawString("".concat(gps.alt, "m (").concat(gps.satellites, " sat)"), mid, y); - y += g.getFontHeight(); - drawn = true; - } - if (hrm) { - g.drawString("".concat(hrm.bpm, " BPM (").concat(hrm.confidence, "%)"), mid, y); - y += g.getFontHeight(); - drawn = true; - } - else if (hrmAny) { - g.drawString("~".concat(hrmAny.bpm, " BPM (").concat(hrmAny.confidence, "%)"), mid, y); - y += g.getFontHeight(); - drawn = true; - if (!settings.hrm && !hrmAnyClear) { - hrmAnyClear = setTimeout(function () { - hrmAny = undefined; - hrmAnyClear = undefined; - }, 10000); - } - } - if (mag) { - g.drawString("".concat(mag.x, " ").concat(mag.y, " ").concat(mag.z), mid, y); - y += g.getFontHeight(); - g.drawString("heading: ".concat(mag.heading.toFixed(1)), mid, y); - y += g.getFontHeight(); - drawn = true; - } - if (!drawn) { - if (!force || Object.values(settings).every(function (x) { return !x; })) { - g.drawString("swipe to enable", mid, y); - } - else { - g.drawString("events pending", mid, y); - } - y += g.getFontHeight(); - } -}; -var onTap = function () { - setBtnsShown(true); -}; -var redraw = function () { - if (btnsShown) { - if (!prevBtnsShown) { - prevBtnsShown = btnsShown; - Bangle.removeListener("swipe", onTap); - btnLayout.setUI(); - btnLayout.forgetLazyState(); - g.clearRect(Bangle.appRect); - } - btnLayout.render(); - } - else { - if (prevBtnsShown) { - prevBtnsShown = btnsShown; - Bangle.setUI(); - Bangle.on("swipe", onTap); - drawInfo(true); - } - else { - drawInfo(); - } - } -}; -var encodeHrm = function (hrm) { - return [0, hrm.bpm]; -}; -encodeHrm.maxLen = 2; -var encodePressure = function (data) { - return toByteArray(Math.round(data.pressure * 10), 4, false); -}; -encodePressure.maxLen = 4; -var encodeElevation = function (data) { - return toByteArray(Math.round(data.altitude * 100), 3, true); -}; -encodeElevation.maxLen = 3; -var encodeTemp = function (data) { - return toByteArray(Math.round(data.temperature * 10), 2, true); -}; -encodeTemp.maxLen = 2; -var encodeGps = function (data) { - var speed = toByteArray(Math.round(1000 * data.speed / 36), 2, false); - var lat = toByteArray(Math.round(data.lat * 10000000), 4, true); - var lon = toByteArray(Math.round(data.lon * 10000000), 4, true); - var elevation = toByteArray(Math.round(data.alt * 100), 3, true); - var heading = toByteArray(Math.round(data.course * 100), 2, false); - return [ - 157, - 2, - speed[0], speed[1], - lat[0], lat[1], lat[2], lat[3], - lon[0], lon[1], lon[2], lon[3], - elevation[0], elevation[1], elevation[2], - heading[0], heading[1] - ]; -}; -encodeGps.maxLen = 17; -var encodeGpsHeadingOnly = function (data) { - var heading = toByteArray(Math.round(data.heading * 100), 2, false); - return [ - 16, - 16, - heading[0], heading[1] - ]; -}; -encodeGpsHeadingOnly.maxLen = 17; -var encodeMag = function (data) { - var x = toByteArray(data.x, 2, true); - var y = toByteArray(data.y, 2, true); - var z = toByteArray(data.z, 2, true); - return [x[0], x[1], y[0], y[1], z[0], z[1]]; -}; -encodeMag.maxLen = 6; -var toByteArray = function (value, numberOfBytes, isSigned) { - var byteArray = new Array(numberOfBytes); - if (isSigned && (value < 0)) { - value += 1 << (numberOfBytes * 8); - } - for (var index = 0; index < numberOfBytes; index++) { - byteArray[index] = (value >> (index * 8)) & 0xff; - } - return byteArray; -}; -var enableSensors = function () { - Bangle.setBarometerPower(settings.bar, "btadv"); - if (!settings.bar) - bar = undefined; - Bangle.setGPSPower(settings.gps, "btadv"); - if (!settings.gps) - gps = undefined; - Bangle.setHRMPower(settings.hrm, "btadv"); - if (!settings.hrm) - hrm = hrmAny = undefined; - Bangle.setCompassPower(settings.mag, "btadv"); - if (!settings.mag) - mag = undefined; -}; -var haveServiceData = function (serv) { - switch (serv) { - case "0x180d": return !!hrm; - case "0x181a": return !!(bar || mag); - case "0x1819": return !!(gps && gps.lat && gps.lon || mag); - } -}; -var serviceToAdvert = function (serv, initial) { - var _a, _b, _c; - if (initial === void 0) { initial = false; } - switch (serv) { - case "0x180d": - if (hrm || initial) { - var o = { - maxLen: encodeHrm.maxLen, - readable: true, - notify: true, - }; - if (hrm) { - o.value = encodeHrm(hrm); - hrm = undefined; - } - return _a = {}, _a["0x2a37"] = o, _a; - } - return {}; - case "0x1819": - if (gps || initial) { - var o = { - maxLen: encodeGps.maxLen, - readable: true, - notify: true, - }; - if (gps) { - o.value = encodeGps(gps); - gps = undefined; - } - return _b = {}, _b["0x2a67"] = o, _b; - } - else if (mag) { - var o = { - maxLen: encodeGpsHeadingOnly.maxLen, - readable: true, - notify: true, - value: encodeGpsHeadingOnly(mag), - }; - return _c = {}, _c["0x2a67"] = o, _c; - } - return {}; - case "0x181a": { - var o = {}; - if (bar || initial) { - o["0x2a6c"] = { - maxLen: encodeElevation.maxLen, - readable: true, - notify: true, - }; - o["0x2A1F"] = { - maxLen: encodeTemp.maxLen, - readable: true, - notify: true, - }; - o["0x2a6d"] = { - maxLen: encodePressure.maxLen, - readable: true, - notify: true, - }; - if (bar) { - o["0x2a6c"].value = encodeElevation(bar); - o["0x2A1F"].value = encodeTemp(bar); - o["0x2a6d"].value = encodePressure(bar); - bar = undefined; - } - } - if (mag || initial) { - o["0x2aa1"] = { - maxLen: encodeMag.maxLen, - readable: true, - notify: true, - }; - if (mag) { - o["0x2aa1"].value = encodeMag(mag); - } - } - return o; - } - } -}; -var getBleAdvert = function (map, all) { - if (all === void 0) { all = false; } - var advert = {}; - for (var _i = 0, services_1 = services; _i < services_1.length; _i++) { - var serv = services_1[_i]; - if (all || haveServiceData(serv)) { - advert[serv] = map(serv); - } - } - mag = undefined; - return advert; -}; -var updateServices = function () { - var newAdvert = getBleAdvert(serviceToAdvert); - NRF.updateServices(newAdvert); -}; -var onAccel = function (newAcc) { return acc = newAcc; }; -var onPressure = function (newBar) { return bar = newBar; }; -var onGPS = function (newGps) { return gps = newGps; }; -var onHRM = function (newHrm) { - if (newHrm.confidence >= HRM_MIN_CONFIDENCE) - hrm = newHrm; - hrmAny = newHrm; -}; -var onMag = function (newMag) { return mag = newMag; }; -var hook = function (enable) { - if (enable) { - Bangle.on("accel", onAccel); - Bangle.on("pressure", onPressure); - Bangle.on("GPS", onGPS); - Bangle.on("HRM", onHRM); - Bangle.on("mag", onMag); - } - else { - Bangle.removeListener("accel", onAccel); - Bangle.removeListener("pressure", onPressure); - Bangle.removeListener("GPS", onGPS); - Bangle.removeListener("HRM", onHRM); - Bangle.removeListener("mag", onMag); - } -}; -var setIntervals = function (locked, connected) { - if (locked === void 0) { locked = Bangle.isLocked(); } - if (connected === void 0) { connected = NRF.getSecurityStatus().connected; } - changeInterval(redrawInterval, locked ? 15000 : 5000); - if (connected) { - var interval = btnsShown ? 5000 : 1000; - if (bleInterval) { - changeInterval(bleInterval, interval); - } - else { - bleInterval = setInterval(updateServices, interval); - } - } - else if (bleInterval) { - clearInterval(bleInterval); - bleInterval = undefined; - } -}; -var redrawInterval = setInterval(redraw, 1000); -Bangle.on("lock", function (locked) { return setIntervals(locked); }); -var bleInterval; -NRF.on("connect", function () { return setIntervals(undefined, true); }); -NRF.on("disconnect", function () { return setIntervals(undefined, false); }); -setIntervals(); -setBtnsShown(true); -enableSensors(); { - var ad = getBleAdvert(function (serv) { return serviceToAdvert(serv, true); }, true); - var adServices = Object - .keys(ad) - .map(function (k) { return k.replace("0x", ""); }); - NRF.setServices(ad, { - advertise: adServices, - uart: false, + var __assign = Object.assign; + var Layout_1 = require("Layout"); + Bangle.loadWidgets(); + Bangle.drawWidgets(); + var HRM_MIN_CONFIDENCE_1 = 75; + var services_1 = ["0x180d", "0x181a", "0x1819"]; + var acc_1; + var bar_1; + var gps_1; + var hrm_1; + var hrmAny_1; + var mag_1; + var btnsShown_1 = false; + var prevBtnsShown_1 = undefined; + var hrmAnyClear_1; + var settings_1 = { + bar: false, + gps: false, + hrm: false, + mag: false, + }; + var idToName = { + acc: "Acceleration", + bar: "Barometer", + gps: "GPS", + hrm: "HRM", + mag: "Magnetometer", + }; + var infoFont_1 = "6x8:2"; + var colour_1 = { + on: "#0f0", + off: "#fff", + }; + var makeToggle = function (id) { return function () { + settings_1[id] = !settings_1[id]; + var entry = btnLayout_1[id]; + var col = settings_1[id] ? colour_1.on : colour_1.off; + entry.btnBorder = entry.col = col; + btnLayout_1.update(); + btnLayout_1.render(); + enableSensors_1(); + }; }; + var btnStyle = { + font: "Vector:14", + fillx: 1, + filly: 1, + col: g.theme.fg, + bgCol: g.theme.bg, + btnBorder: "#fff", + }; + var btnLayout_1 = new Layout_1({ + type: "v", + c: [ + { + type: "h", + c: [ + __assign({ type: "btn", label: idToName.bar, id: "bar", cb: makeToggle('bar') }, btnStyle), + __assign({ type: "btn", label: idToName.gps, id: "gps", cb: makeToggle('gps') }, btnStyle), + ] + }, + { + type: "h", + c: [ + __assign({ type: "btn", label: idToName.hrm, id: "hrm", cb: makeToggle('hrm') }, btnStyle), + __assign({ type: "btn", label: idToName.mag, id: "mag", cb: makeToggle('mag') }, btnStyle), + ] + }, + { + type: "h", + c: [ + __assign(__assign({ type: "btn", label: idToName.acc, id: "acc", cb: function () { } }, btnStyle), { col: colour_1.on, btnBorder: colour_1.on }), + __assign({ type: "btn", label: "Back", cb: function () { + setBtnsShown_1(false); + } }, btnStyle), + ] + } + ] + }, { + lazy: true, + back: function () { + setBtnsShown_1(false); + }, }); + var setBtnsShown_1 = function (b) { + btnsShown_1 = b; + hook_1(!btnsShown_1); + setIntervals_1(); + redraw_1(); + }; + var drawInfo_1 = function (force) { + var _a = Bangle.appRect, y = _a.y, x = _a.x, w = _a.w; + var mid = x + w / 2; + var drawn = false; + if (!force && !bar_1 && !gps_1 && !hrm_1 && !mag_1) + return; + g.reset() + .clearRect(Bangle.appRect) + .setFont(infoFont_1) + .setFontAlign(0, -1); + if (bar_1) { + g.drawString("".concat(bar_1.altitude.toFixed(1), "m"), mid, y); + y += g.getFontHeight(); + g.drawString("".concat(bar_1.pressure.toFixed(1), " hPa"), mid, y); + y += g.getFontHeight(); + g.drawString("".concat(bar_1.temperature.toFixed(1), "C"), mid, y); + y += g.getFontHeight(); + drawn = true; + } + if (gps_1) { + g.drawString("".concat(gps_1.lat.toFixed(4), " lat, ").concat(gps_1.lon.toFixed(4), " lon"), mid, y); + y += g.getFontHeight(); + g.drawString("".concat(gps_1.alt, "m (").concat(gps_1.satellites, " sat)"), mid, y); + y += g.getFontHeight(); + drawn = true; + } + if (hrm_1) { + g.drawString("".concat(hrm_1.bpm, " BPM (").concat(hrm_1.confidence, "%)"), mid, y); + y += g.getFontHeight(); + drawn = true; + } + else if (hrmAny_1) { + g.drawString("~".concat(hrmAny_1.bpm, " BPM (").concat(hrmAny_1.confidence, "%)"), mid, y); + y += g.getFontHeight(); + drawn = true; + if (!settings_1.hrm && !hrmAnyClear_1) { + hrmAnyClear_1 = setTimeout(function () { + hrmAny_1 = undefined; + hrmAnyClear_1 = undefined; + }, 10000); + } + } + if (mag_1) { + g.drawString("".concat(mag_1.x, " ").concat(mag_1.y, " ").concat(mag_1.z), mid, y); + y += g.getFontHeight(); + g.drawString("heading: ".concat(mag_1.heading.toFixed(1)), mid, y); + y += g.getFontHeight(); + drawn = true; + } + if (!drawn) { + if (!force || Object.values(settings_1).every(function (x) { return !x; })) { + g.drawString("swipe to enable", mid, y); + } + else { + g.drawString("events pending", mid, y); + } + y += g.getFontHeight(); + } + }; + var onTap_1 = function () { + setBtnsShown_1(true); + }; + var redraw_1 = function () { + if (btnsShown_1) { + if (!prevBtnsShown_1) { + prevBtnsShown_1 = btnsShown_1; + Bangle.removeListener("swipe", onTap_1); + btnLayout_1.setUI(); + btnLayout_1.forgetLazyState(); + g.clearRect(Bangle.appRect); + } + btnLayout_1.render(); + } + else { + if (prevBtnsShown_1) { + prevBtnsShown_1 = btnsShown_1; + Bangle.setUI(); + Bangle.on("swipe", onTap_1); + drawInfo_1(true); + } + else { + drawInfo_1(); + } + } + }; + var encodeHrm_1 = function (hrm) { + return [0, hrm.bpm]; + }; + encodeHrm_1.maxLen = 2; + var encodePressure_1 = function (data) { + return toByteArray_1(Math.round(data.pressure * 10), 4, false); + }; + encodePressure_1.maxLen = 4; + var encodeElevation_1 = function (data) { + return toByteArray_1(Math.round(data.altitude * 100), 3, true); + }; + encodeElevation_1.maxLen = 3; + var encodeTemp_1 = function (data) { + return toByteArray_1(Math.round(data.temperature * 10), 2, true); + }; + encodeTemp_1.maxLen = 2; + var encodeGps_1 = function (data) { + var speed = toByteArray_1(Math.round(1000 * data.speed / 36), 2, false); + var lat = toByteArray_1(Math.round(data.lat * 10000000), 4, true); + var lon = toByteArray_1(Math.round(data.lon * 10000000), 4, true); + var elevation = toByteArray_1(Math.round(data.alt * 100), 3, true); + var heading = toByteArray_1(Math.round(data.course * 100), 2, false); + return [ + 157, + 2, + speed[0], speed[1], + lat[0], lat[1], lat[2], lat[3], + lon[0], lon[1], lon[2], lon[3], + elevation[0], elevation[1], elevation[2], + heading[0], heading[1] + ]; + }; + encodeGps_1.maxLen = 17; + var encodeGpsHeadingOnly_1 = function (data) { + var heading = toByteArray_1(Math.round(data.heading * 100), 2, false); + return [ + 16, + 16, + heading[0], heading[1] + ]; + }; + encodeGpsHeadingOnly_1.maxLen = 17; + var encodeMag_1 = function (data) { + var x = toByteArray_1(data.x, 2, true); + var y = toByteArray_1(data.y, 2, true); + var z = toByteArray_1(data.z, 2, true); + return [x[0], x[1], y[0], y[1], z[0], z[1]]; + }; + encodeMag_1.maxLen = 6; + var toByteArray_1 = function (value, numberOfBytes, isSigned) { + var byteArray = new Array(numberOfBytes); + if (isSigned && (value < 0)) { + value += 1 << (numberOfBytes * 8); + } + for (var index = 0; index < numberOfBytes; index++) { + byteArray[index] = (value >> (index * 8)) & 0xff; + } + return byteArray; + }; + var enableSensors_1 = function () { + Bangle.setBarometerPower(settings_1.bar, "btadv"); + if (!settings_1.bar) + bar_1 = undefined; + Bangle.setGPSPower(settings_1.gps, "btadv"); + if (!settings_1.gps) + gps_1 = undefined; + Bangle.setHRMPower(settings_1.hrm, "btadv"); + if (!settings_1.hrm) + hrm_1 = hrmAny_1 = undefined; + Bangle.setCompassPower(settings_1.mag, "btadv"); + if (!settings_1.mag) + mag_1 = undefined; + }; + var haveServiceData_1 = function (serv) { + switch (serv) { + case "0x180d": return !!hrm_1; + case "0x181a": return !!(bar_1 || mag_1); + case "0x1819": return !!(gps_1 && gps_1.lat && gps_1.lon || mag_1); + } + }; + var serviceToAdvert_1 = function (serv, initial) { + var _a, _b, _c; + if (initial === void 0) { initial = false; } + switch (serv) { + case "0x180d": + if (hrm_1 || initial) { + var o = { + maxLen: encodeHrm_1.maxLen, + readable: true, + notify: true, + }; + if (hrm_1) { + o.value = encodeHrm_1(hrm_1); + hrm_1 = undefined; + } + return _a = {}, _a["0x2a37"] = o, _a; + } + return {}; + case "0x1819": + if (gps_1 || initial) { + var o = { + maxLen: encodeGps_1.maxLen, + readable: true, + notify: true, + }; + if (gps_1) { + o.value = encodeGps_1(gps_1); + gps_1 = undefined; + } + return _b = {}, _b["0x2a67"] = o, _b; + } + else if (mag_1) { + var o = { + maxLen: encodeGpsHeadingOnly_1.maxLen, + readable: true, + notify: true, + value: encodeGpsHeadingOnly_1(mag_1), + }; + return _c = {}, _c["0x2a67"] = o, _c; + } + return {}; + case "0x181a": { + var o = {}; + if (bar_1 || initial) { + o["0x2a6c"] = { + maxLen: encodeElevation_1.maxLen, + readable: true, + notify: true, + }; + o["0x2A1F"] = { + maxLen: encodeTemp_1.maxLen, + readable: true, + notify: true, + }; + o["0x2a6d"] = { + maxLen: encodePressure_1.maxLen, + readable: true, + notify: true, + }; + if (bar_1) { + o["0x2a6c"].value = encodeElevation_1(bar_1); + o["0x2A1F"].value = encodeTemp_1(bar_1); + o["0x2a6d"].value = encodePressure_1(bar_1); + bar_1 = undefined; + } + } + if (mag_1 || initial) { + o["0x2aa1"] = { + maxLen: encodeMag_1.maxLen, + readable: true, + notify: true, + }; + if (mag_1) { + o["0x2aa1"].value = encodeMag_1(mag_1); + } + } + return o; + } + } + }; + var getBleAdvert_1 = function (map, all) { + if (all === void 0) { all = false; } + var advert = {}; + for (var _i = 0, services_2 = services_1; _i < services_2.length; _i++) { + var serv = services_2[_i]; + if (all || haveServiceData_1(serv)) { + advert[serv] = map(serv); + } + } + mag_1 = undefined; + return advert; + }; + var updateServices_1 = function () { + var newAdvert = getBleAdvert_1(serviceToAdvert_1); + NRF.updateServices(newAdvert); + }; + var onAccel_1 = function (newAcc) { return acc_1 = newAcc; }; + var onPressure_1 = function (newBar) { return bar_1 = newBar; }; + var onGPS_1 = function (newGps) { return gps_1 = newGps; }; + var onHRM_1 = function (newHrm) { + if (newHrm.confidence >= HRM_MIN_CONFIDENCE_1) + hrm_1 = newHrm; + hrmAny_1 = newHrm; + }; + var onMag_1 = function (newMag) { return mag_1 = newMag; }; + var hook_1 = function (enable) { + if (enable) { + Bangle.on("accel", onAccel_1); + Bangle.on("pressure", onPressure_1); + Bangle.on("GPS", onGPS_1); + Bangle.on("HRM", onHRM_1); + Bangle.on("mag", onMag_1); + } + else { + Bangle.removeListener("accel", onAccel_1); + Bangle.removeListener("pressure", onPressure_1); + Bangle.removeListener("GPS", onGPS_1); + Bangle.removeListener("HRM", onHRM_1); + Bangle.removeListener("mag", onMag_1); + } + }; + var setIntervals_1 = function (locked, connected) { + if (locked === void 0) { locked = Bangle.isLocked(); } + if (connected === void 0) { connected = NRF.getSecurityStatus().connected; } + changeInterval(redrawInterval_1, locked ? 15000 : 5000); + if (connected) { + var interval = btnsShown_1 ? 5000 : 1000; + if (bleInterval_1) { + changeInterval(bleInterval_1, interval); + } + else { + bleInterval_1 = setInterval(updateServices_1, interval); + } + } + else if (bleInterval_1) { + clearInterval(bleInterval_1); + bleInterval_1 = undefined; + } + }; + var redrawInterval_1 = setInterval(redraw_1, 1000); + Bangle.on("lock", function (locked) { return setIntervals_1(locked); }); + var bleInterval_1; + NRF.on("connect", function () { return setIntervals_1(undefined, true); }); + NRF.on("disconnect", function () { return setIntervals_1(undefined, false); }); + setIntervals_1(); + setBtnsShown_1(true); + enableSensors_1(); + { + var ad = getBleAdvert_1(function (serv) { return serviceToAdvert_1(serv, true); }, true); + var adServices = Object + .keys(ad) + .map(function (k) { return k.replace("0x", ""); }); + NRF.setServices(ad, { + advertise: adServices, + uart: false, + }); + } } diff --git a/apps/btadv/app.ts b/apps/btadv/app.ts index 85fd3a5d3..5e4930865 100644 --- a/apps/btadv/app.ts +++ b/apps/btadv/app.ts @@ -1,4 +1,5 @@ -// ts helpers: +{ +// @ts-ignore helper const __assign = Object.assign; const Layout = require("Layout"); @@ -713,3 +714,4 @@ enableSensors(); }, ); } +} diff --git a/apps/bthrm/metadata.json b/apps/bthrm/metadata.json index fea274ff3..6c31759c9 100644 --- a/apps/bthrm/metadata.json +++ b/apps/bthrm/metadata.json @@ -18,5 +18,6 @@ {"name":"bthrm.settings.js","url":"settings.js"}, {"name":"bthrm","url":"lib.js"}, {"name":"bthrm.default.json","url":"default.json"} - ] + ], + "data": [{"name":"bthrm.json"}] } diff --git a/apps/bwclk/metadata.json b/apps/bwclk/metadata.json index fba759bf0..d4091c2fe 100644 --- a/apps/bwclk/metadata.json +++ b/apps/bwclk/metadata.json @@ -15,5 +15,6 @@ {"name":"bwclk.app.js","url":"app.js"}, {"name":"bwclk.img","url":"app-icon.js","evaluate":true}, {"name":"bwclk.settings.js","url":"settings.js"} - ] + ], + "data":[{"name":"bwclk.setting.json"}] } diff --git a/apps/bwclklite/metadata.json b/apps/bwclklite/metadata.json index b073f985e..f8dffdca9 100644 --- a/apps/bwclklite/metadata.json +++ b/apps/bwclklite/metadata.json @@ -39,5 +39,10 @@ "name": "bwclklite.settings.js", "url": "settings.js" } + ], + "data": [ + { + "name": "bwclklite.setting.json" + } ] } diff --git a/apps/cassioWatch/metadata.json b/apps/cassioWatch/metadata.json index 4b9985c82..fb7dfd401 100644 --- a/apps/cassioWatch/metadata.json +++ b/apps/cassioWatch/metadata.json @@ -12,7 +12,10 @@ "readme": "README.md", "storage": [ { "name": "cassioWatch.app.js", "url": "app.js" }, - {"name":"cassioWatch.settings.js","url":"settings.js"}, + { "name": "cassioWatch.settings.js","url": "settings.js" }, { "name": "cassioWatch.img", "url": "icon.js", "evaluate": true } + ], + "data": [ + { "name": "cassioWatch.settings.json" } ] } diff --git a/apps/chargent/ChangeLog b/apps/chargent/ChangeLog index d7172c8d2..d7081ecfb 100644 --- a/apps/chargent/ChangeLog +++ b/apps/chargent/ChangeLog @@ -2,3 +2,4 @@ 0.02: Support BangleJS2 0.03: Added threshold 0.04: Added notification +0.05: Fixed boot diff --git a/apps/chargent/boot.js b/apps/chargent/boot.js index 666fd3a04..c8fd4f930 100644 --- a/apps/chargent/boot.js +++ b/apps/chargent/boot.js @@ -2,7 +2,7 @@ const pin = process.env.HWVERSION === 2 ? D3 : D30; var id; - Bangle.on('charging', (charging) => { + function gent(charging) { if (charging) { if (!id) { var max = 0; @@ -37,5 +37,8 @@ require('notify').hide({id: 'chargent'}); } } - }); + } + + Bangle.on('charging', gent); + if (Bangle.isCharging()) gent(true); })(); diff --git a/apps/chargent/metadata.json b/apps/chargent/metadata.json index dda0369a6..d43493ada 100644 --- a/apps/chargent/metadata.json +++ b/apps/chargent/metadata.json @@ -1,6 +1,6 @@ { "id": "chargent", "name": "Charge Gently", - "version": "0.04", + "version": "0.05", "description": "When charging, reminds you to disconnect the watch to prolong battery life.", "icon": "icon.png", "type": "bootloader", diff --git a/apps/chargerot/metadata.json b/apps/chargerot/metadata.json index 99c97070e..1b13403d7 100644 --- a/apps/chargerot/metadata.json +++ b/apps/chargerot/metadata.json @@ -11,5 +11,6 @@ "storage": [ {"name":"chargerot.boot.js","url":"boot.js"}, {"name":"chargerot.settings.js","url":"settings.js"} - ] + ], + "data":[{"name":"chargerot.settings.json"}] } diff --git a/apps/chess/app-icon.js b/apps/chess/app-icon.js new file mode 100644 index 000000000..2aa34d5ae --- /dev/null +++ b/apps/chess/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AEnPAEAv/wAAcF6XWAAYdFBQgLLF/4v/F/4vTFKoLGF/4v/F/4v/F4QpWBQov/F7UslgKBAYIABEgIDDF/4v/F4es1gvTdKoLBFYYAHF/4vTmQvuvQwJFxAvbAAOIxIAFFxIvzFKYLG6CMNF8GsF92BdpwvfqwvtRwgvQAC+IxIAJF5QAYF93OwEyRwqSPACwsJGEov/AH4A/AH4AwA=")) diff --git a/apps/chess/app.js b/apps/chess/app.js new file mode 100644 index 000000000..bb3ab147d --- /dev/null +++ b/apps/chess/app.js @@ -0,0 +1,283 @@ +// Using p4wn chess engine: https://p4wn.sourceforge.net/ | https://github.com/douglasbagnall/p4wn +const engine = require("chessengine"); + +Bangle.loadWidgets(); // load before first appRect call + +const FIELD_WIDTH = Bangle.appRect.w/8; +const FIELD_HEIGHT = Bangle.appRect.h/8; +const SETTINGS_FILE = "chess.json"; +const DEFAULT_TIMEOUT = Bangle.getOptions().lockTimeout; +const ICON_SIZE=45; +const ICON_BISHOP = require("heatshrink").decompress(atob("lstwMB/4Ac/wFE4IED/kPAofgn4FDGon8j4QEBQgQE4EHBQcACwfAgF/BQYWD8EAHAX+NgI4C+AQEwAQDDYIhDDYMDCAQKBGQQsHHogKDCAJODCAI3CHoQKCHoIQDHoIQCFgoQBFgfgIQYmBEIQECKgIrCBYQKDC4OBg/8iCvEAC+AA=")); +const ICON_PAWN = require("heatshrink").decompress(atob("lstwMB/4At/AFEGon4h4FDwE/AgX8CAngCAkAv4bDgYbECAf4gAhD4AhD/kAg4mDCAkACAYbBEIYQBG4gbDEII9DFhXAgEfBQYWDEwJUC/wKBGQXwCAgEBE4RCBCAYmBCAQmCCAQmBCAbdCCAIbCQ4gAYwA=")); +const ICON_KING = require("heatshrink").decompress(atob("lstwMB/4Ac/wFE+4KEh4FD+F/AofvCwgKE+IKEg4bEj4FDwADC/k8g+HAoJhCC4PwAoQXBNod//AECgYfBAoUP/gQE8AQEBQcfCAaLBCAZmBEIZuBBQgyDJAIWCPgXAEAQWDBQRUCPgQnBHgJqBLwYhDOwRvDGQc/EIaSDCwLedwAA==")); +const ICON_QUEEN = require("heatshrink").decompress(atob("lstwMB/4Ac/l/AgXn4PzAgP+j0Ph4FB8FwuE///PgeDwPn/k8n0+j0f4Hz+Px8F+g/Px+fgf4vgACn/jAAf/x8Pj0en/8vAsB+P/+PBwcHj//w0MjEwJgMwsHBw5CBwMEhBDBPoR6B/gFCDYPgAoRZBAgUH//4AoQbB4AbDCAYbBCAZ1CAgJ7CwAKDGQQmBCAYmBEIQmC+AQEDYQQBDYQQCFgo3CXQIsFBYIEDACmAA=")); +const ICON_ROOK = require("heatshrink").decompress(atob("lstwMB/4Ax/0HgPAAoPwnEOg4FBwBFBn///gEBI4XgAoMPAoJWCv4QDDYXwBQf/4AKD/wmDCARuDGQImCEIQbCGQMDCAQKBj4EB/AFBBQQsgDYQQCNQQhCOog3CCAQ3BEIRvCAoSRCE4IxCKgQmCKgYAZwA=")); +const ICON_KNIGHT = require("heatshrink").decompress(atob("lstwMB/4Ann1/AgX48IKD4UPAgX+gEHAoXwgALDJQMfDYQFBEQWAgBSCBQQcC4AFBn///hnCBQPgAgMDGIQnDGIIQDAgQQBEwQQCGIIQCEwMECAQxBsAQBEwMPCAQmBAIJDB4EPDoM/CAIoBKgP4BQQQB/AzCKgJlIPgQ+COwJlCHoJlDJwJlDS4aBDDYQsCADOA")); + +const settings = Object.assign({ + state: engine.P4_INITIAL_BOARD, + computer_level: 0, // default to "stupid" which is the fastest +}, require("Storage").readJSON(SETTINGS_FILE,1) || {}); + +var ovr = Graphics.createArrayBuffer(Bangle.appRect.w,Bangle.appRect.h,2,{msb:true}); +const curfield = [4*FIELD_WIDTH, 6*FIELD_HEIGHT]; // e2 +const startfield = Array(2); +let piece_sel = 0; +let showmenu = false; + +const writeSettings = () => { + settings.state = engine.p4_state2fen(state); + require('Storage').writeJSON(SETTINGS_FILE, settings); +}; + +const generateBgImage = () => { + var buf = Graphics.createArrayBuffer(Bangle.appRect.w,Bangle.appRect.h,1,{msb:true}); + for(let idxrow=0; idxrow<8; idxrow++) { + for(let idxcol=0; idxcol<8; idxcol++) { + const bgCol = idxrow % 2 != idxcol % 2 ? 0 : 1; + const x = idxcol*FIELD_WIDTH; + const y = idxrow*FIELD_HEIGHT; + buf.setColor(bgCol).fillRect({x:x, y:y, w:FIELD_WIDTH, h:FIELD_HEIGHT}); + } + } + return {width:buf.getWidth(), height:buf.getHeight(), + buffer:buf.buffer + }; +}; + +const idx2Pos = (idxcol, idxrow) => { + "ram" + return 2*(1+8+1) + (7-idxrow)*(1+8+1) + idxcol + 1; +}; + +const drawPiece = (buf, x, y, piece) => { + let icon; + + switch(piece & ~0x1) { + case engine.P4_PAWN: + icon = ICON_PAWN; + break; + case engine.P4_BISHOP: + icon = ICON_BISHOP; + break; + case engine.P4_KING: + icon = ICON_KING; + break; + case engine.P4_QUEEN: + icon = ICON_QUEEN; + break; + case engine.P4_ROOK: + icon = ICON_ROOK; + break; + case engine.P4_KNIGHT: + icon = ICON_KNIGHT; + break; + } + + if (icon) { + const scale = FIELD_HEIGHT/ICON_SIZE; + buf.drawImage(icon, x+(FIELD_WIDTH-(ICON_SIZE*scale))/2, y, {scale: scale}); + } + return buf; +}; + +const drawBoard = () => { + //console.log("Free: " + process.memory().free); + + g.setBgColor("#555").setColor("#aaa").drawImage(bgImage, Bangle.appRect.x, Bangle.appRect.y); + for(let idxrow=0; idxrow<8; idxrow++) { + for(let idxcol=0; idxcol<8; idxcol++) { + const x = idxcol*FIELD_WIDTH+Bangle.appRect.x; + const y = idxrow*FIELD_HEIGHT+Bangle.appRect.y; + + const pos = idx2Pos(idxcol, idxrow); + const field = state.board[pos]; + + if (field) { + const fgCol = field & 0x1 ? "#000" : "#fff"; + drawPiece(g.setBgColor(fgCol), x, y, field); + } + } + } +}; + +const roundX = (x) => { + return Math.round(x/FIELD_WIDTH)*FIELD_WIDTH; +}; + +const roundY = (y) => { + return Math.round(y/FIELD_HEIGHT)*FIELD_HEIGHT; +}; + +const drawSelectedField = () => { + ovr.clear(); + if (!showmenu) { + if (startfield[0] !== undefined && startfield[1] !== undefined) { + // remove piece from startfield + const x = startfield[0]; + const y = startfield[1]; + ovr.setColor(2).fillRect({x:x, y:y, w:FIELD_WIDTH, h:FIELD_HEIGHT}); + } + + const x = roundX(curfield[0]); + const y = roundY(curfield[1]); + ovr.setColor(piece_sel ? 1 : 2) + .drawRect({x:x+1, y:y, w:FIELD_WIDTH-2, h:FIELD_HEIGHT}) + .drawRect({x:x+2, y:y+1, w:FIELD_WIDTH-4, h:FIELD_HEIGHT-2}) + .drawRect({x:x+3, y:y+2, w:FIELD_WIDTH-6, h:FIELD_HEIGHT-4}); + if (piece_sel) { + drawPiece(ovr.setBgColor(1), x, y, piece_sel); + ovr.setBgColor(0); // back to transparent + } + } + Bangle.setLCDOverlay({width:ovr.getWidth(), height:ovr.getHeight(), + bpp:2, transparent:0, + palette:new Uint16Array([0, g.toColor("#F00"), g.toColor("#0F0"), 0]), + buffer:ovr.buffer + },Bangle.appRect.x,Bangle.appRect.y); +}; + +const isInside = (rect, e) => { + return e.x>=rect.x && e.x=rect.y && e.y<=rect.y+rect.h; +}; + +const showAlert = (msg) => { + showmenu = true; + drawSelectedField(); + E.showAlert(msg).then(function() { + showmenu = false; + drawBoard(); + drawSelectedField(); + }); +}; + +const move = (from,to) => { + const res = state.move(from, to); + //console.log(res); + if (!res.ok) { + showAlert("Illegal move"); + } else { + if (res.flags & engine.P4_MOVE_FLAG_MATE) { + showAlert("Checkmate or stalemate"); + } else if (res.flags & engine.P4_MOVE_FLAG_CHECK) { + showAlert("A king is in check"); + } else if (res.flags & engine.P4_MOVE_FLAG_DRAW) { + showAlert("A draw is available"); + } + } + return res; +}; + +const showMessage = (msg) => { + g.setColor("#f00").setFont("4x6:2").setFontAlign(-1,1).drawString(msg, 10, Bangle.appRect.y2-10); +}; + +// Run + +g.reset(); +const bgImage = generateBgImage(); +let state = engine.p4_fen2state(settings.state); +drawBoard(); +drawSelectedField(); +Bangle.drawWidgets(); + +// drag selected field +Bangle.on('drag', (ev) => { + const newx = curfield[0]+ev.dx; + const newy = curfield[1]+ev.dy; + if (newx >= 0 && newx <= 7*FIELD_WIDTH) { + curfield[0] = newx; + } + if (newy >= 0 && newy <= 7*FIELD_HEIGHT) { + curfield[1] = newy; + } + drawSelectedField(); +}); + +// touch to start/stop moving a piece +Bangle.on('touch', (button, xy) => { + if (isInside(Bangle.appRect, xy) && !showmenu) { + if (piece_sel === 0) { + startfield[0] = roundX(curfield[0]); + startfield[1] = roundY(curfield[1]); + const startpos = idx2Pos(startfield[0]/FIELD_WIDTH, startfield[1]/FIELD_HEIGHT); + piece_sel = state.board[startpos]; + if (piece_sel === 0) { + startfield[0] = startfield[1] = undefined; + // nothing here, do nothing + return; + } + } else { // piece_sel === 0 + const colTo = roundX(curfield[0]); + const rowTo = roundY(curfield[1]); + if (startfield[0] !== colTo || startfield[1] !== rowTo) { + showMessage(/*LANG*/"Moving.."); + const posFrom = idx2Pos(startfield[0]/FIELD_WIDTH, startfield[1]/FIELD_HEIGHT); + const posTo = idx2Pos(colTo/FIELD_WIDTH, rowTo/FIELD_HEIGHT); + setTimeout(() => { + if (move(posFrom, posTo).ok) { + // human move ok, update + drawBoard(); + drawSelectedField(); + // do computer move + Bangle.setLCDTimeout(0.1); // this can take some time, turn off to save power + showMessage(/*LANG*/"Calculating.."); + setTimeout(() => { + const compMove = state.findmove(settings.computer_level+1); + const result = move(compMove[0], compMove[1]); + writeSettings(); + Bangle.setLCDPower(true); + Bangle.setLocked(false); + Bangle.setLCDTimeout(DEFAULT_TIMEOUT/1000); // restore + if (!showmenu) { + showAlert(result.string); + } + }, 200); // execute after display update + } + }, 100); // execute after display update + } // piece_sel === 0 + startfield[0] = startfield[1] = undefined; + piece_sel = 0; + } + drawSelectedField(); + } +}); + +// show menu on button +setWatch(() => { + showmenu = true; + drawSelectedField(); + + const closeMenu = () => { + showmenu = false; + E.showMenu(); + drawBoard(); + drawSelectedField(); + }; + + E.showMenu({ + "" : { title : /*LANG*/"Chess settings" }, + "< Back" : () => closeMenu(), + /*LANG*/"New Game" : () => { + state = engine.p4_fen2state(engine.P4_INITIAL_BOARD); + writeSettings(); + closeMenu(); + }, + /*LANG*/"Undo Move" : () => { + state.jump_to_moveno(-2); + closeMenu(); + }, + /*LANG*/'Level': { + value: settings.computer_level, + min: 0, max: 4, + format: v => [/*LANG*/'stupid', /*LANG*/'middling', /*LANG*/'default', /*LANG*/'slow', /*LANG*/'slowest'][v], + onchange: v => { + settings.computer_level = v; + writeSettings(); + } + }, + /*LANG*/"Exit" : () => load(), + }); +}, BTN, { repeat: true, edge: "falling" }); diff --git a/apps/chess/app.png b/apps/chess/app.png new file mode 100644 index 000000000..9db78e27d Binary files /dev/null and b/apps/chess/app.png differ diff --git a/apps/chess/engine.js b/apps/chess/engine.js new file mode 100644 index 000000000..88edf78f5 --- /dev/null +++ b/apps/chess/engine.js @@ -0,0 +1,1614 @@ +/* p4wn, AKA 5k chess - by Douglas Bagnall + * + * This code is in the public domain, or as close to it as various + * laws allow. No warranty; no restrictions. + * + * lives at http://p4wn.sf.net/ + */ + +/*Compatibility tricks: + * backwards for old MSIEs (to 5.5) + * sideways for seed command-line javascript.*/ +var p4_log; +if (this.imports !== undefined && + this.printerr !== undefined){//seed or gjs + p4_log = function(){ + var args = Array.prototype.slice.call(arguments); + printerr(args.join(', ')); + }; +} +else if (this.console === undefined){//MSIE + p4_log = function(){}; +} +else { + p4_log = function(){console.log.apply(console, arguments);}; +} + +/*MSIE Date.now backport */ +if (Date.now === undefined) + Date.now = function(){return (new Date).getTime();}; + +/* The pieces are stored as numbers between 2 and 13, inclusive. + * Empty squares are stored as 0, and off-board squares as 16. + * There is some bitwise logic to it: + * piece & 1 -> colour (white: 0, black: 1) + * piece & 2 -> single move piece (including pawn) + * if (piece & 2) == 0: + * piece & 4 -> row and column moves + * piece & 8 -> diagonal moves + */ +var P4_PAWN = 2, P4_ROOK = 4, P4_KNIGHT = 6, P4_BISHOP = 8, P4_QUEEN = 12, P4_KING = 10; +var P4_EDGE = 16; + +/* in order, even indices: , pawn, rook, knight, bishop, king, queen. Only the + * even indices are used.*/ +var P4_MOVES = [[], [], + [], [], + [1,10,-1,-10], [], + [21,19,12,8,-21,-19,-12,-8], [], + [11,9,-11,-9], [], + [1,10,11,9,-1,-10,-11,-9], [], + [1,10,11,9,-1,-10,-11,-9], [] + ]; + +/*P4_VALUES defines the relative value of various pieces. + * + * It follows the 1,3,3,5,9 pattern you learn as a kid, multiplied by + * 20 to give sub-pawn resolution to other factors, with bishops given + * a wee boost over knights. + */ +var P4_VALUES=[0, 0, //Piece values + 20, 20, //pawns + 100, 100, //rooks + 60, 60, //knights + 61, 61, //bishops + 8000, 8000,//kings + 180, 180, //queens + 0]; + +/* A score greater than P4_WIN indicates a king has been taken. It is + * less than the value of a king, in case someone finds a way to, say, + * sacrifice two queens in order to checkmate. + */ +var P4_KING_VALUE = P4_VALUES[10]; +var P4_WIN = P4_KING_VALUE >> 1; + +/* every move, a winning score decreases by this much */ +var P4_WIN_DECAY = 300; +var P4_WIN_NOW = P4_KING_VALUE - 250; + +/* P4_{MAX,MIN}_SCORE should be beyond any possible evaluated score */ + +var P4_MAX_SCORE = 9999; // extremes of evaluation range +var P4_MIN_SCORE = -P4_MAX_SCORE; + +/*initialised in p4_initialise_state */ +var P4_CENTRALISING_WEIGHTS; +var P4_BASE_PAWN_WEIGHTS; +var P4_KNIGHT_WEIGHTS; + +/*P4_DEBUG turns on debugging features */ +var P4_DEBUG = 0; +var P4_INITIAL_BOARD = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 1 1"; + +/*use javascript typed arrays rather than plain arrays + * (faster in some browsers, unsupported in others, possibly slower elsewhere) */ +var P4_USE_TYPED_ARRAYS = this.Int32Array !== undefined; + +var P4_PIECE_LUT = { /*for FEN, PGN interpretation */ + P: 2, + p: 3, + R: 4, + r: 5, + N: 6, + n: 7, + B: 8, + b: 9, + K: 10, + k: 11, + Q: 12, + q: 13 +}; + +var P4_ENCODE_LUT = ' PPRRNNBBKKQQ'; + + +function p4_alphabeta_treeclimber(state, count, colour, score, s, e, alpha, beta){ + var move = p4_make_move(state, s, e, P4_QUEEN); + var i; + var ncolour = 1 - colour; + var movelist = p4_parse(state, colour, move.ep, -score); + var movecount = movelist.length; + if(count){ + //branch nodes + var t; + for(i = 0; i < movecount; i++){ + var mv = movelist[i]; + var mscore = mv[0]; + var ms = mv[1]; + var me = mv[2]; + if (mscore > P4_WIN){ //we won! Don't look further. + alpha = P4_KING_VALUE; + break; + } + t = -p4_alphabeta_treeclimber(state, count - 1, ncolour, mscore, ms, me, + -beta, -alpha); + if (t > alpha){ + alpha = t; + } + if (alpha >= beta){ + break; + } + } + if (alpha < -P4_WIN_NOW && ! p4_check_check(state, colour)){ + /* Whatever we do, we lose the king. + * But if it is not check then this is stalemate, and the + * score doesn't apply. + */ + alpha = state.stalemate_scores[colour]; + } + if (alpha < -P4_WIN){ + /*make distant checkmate seem less bad */ + alpha += P4_WIN_DECAY; + } + } + else{ + //leaf nodes + while(beta > alpha && --movecount != -1){ + if(movelist[movecount][0] > alpha){ + alpha = movelist[movecount][0]; + } + } + } + p4_unmake_move(state, move); + return alpha; +} + + +/* p4_prepare() works out weightings for assessing various moves, + * favouring centralising moves early, for example. + * + * It is called before each tree search, not for each parse(), so it + * is OK for it to be a little bit slow. But that also means it drifts + * out of sync with the real board state, especially on deep searches. + */ + +function p4_prepare(state){ + var i, j, x, y, a; + var pieces = state.pieces = [[], []]; + /*convert state.moveno half move count to move cycle count */ + var moveno = state.moveno >> 1; + var board = state.board; + + /* high earliness_weight indicates a low move number. The formula + * should work above moveno == 50, but this is javascript. + */ + var earliness_weight = (moveno > 50) ? 0 : parseInt(6 * Math.exp(moveno * -0.07)); + var king_should_hide = moveno < 12; + var early = moveno < 5; + /* find the pieces, kings, and weigh material*/ + var kings = [0, 0]; + var material = [0, 0]; + var best_pieces = [0, 0]; + for(i = 20; i < 100; i++){ + a = board[i]; + var piece = a & 14; + var colour = a & 1; + if(piece){ + pieces[colour].push([a, i]); + if (piece == P4_KING){ + kings[colour] = i; + } + else{ + material[colour] += P4_VALUES[piece]; + best_pieces[colour] = Math.max(best_pieces[colour], P4_VALUES[piece]); + } + } + } + + /*does a draw seem likely soon?*/ + var draw_likely = (state.draw_timeout > 90 || state.current_repetitions >= 2); + if (draw_likely) + p4_log("draw likely", state.current_repetitions, state.draw_timeout); + state.values = [[], []]; + var qvalue = P4_VALUES[P4_QUEEN]; /*used as ballast in various ratios*/ + var material_sum = material[0] + material[1] + 2 * qvalue; + var wmul = 2 * (material[1] + qvalue) / material_sum; + var bmul = 2 * (material[0] + qvalue) / material_sum; + var multipliers = [wmul, bmul]; + var emptiness = 4 * P4_QUEEN / material_sum; + state.stalemate_scores = [parseInt(0.5 + (wmul - 1) * 2 * qvalue), + parseInt(0.5 + (bmul - 1) * 2 * qvalue)]; + //p4_log("value multipliers (W, B):", wmul, bmul, + // "stalemate scores", state.stalemate_scores); + for (i = 0; i < P4_VALUES.length; i++){ + var v = P4_VALUES[i]; + if (v < P4_WIN){//i.e., not king + state.values[0][i] = parseInt(v * wmul + 0.5); + state.values[1][i] = parseInt(v * bmul + 0.5); + } + else { + state.values[0][i] = v; + state.values[1][i] = v; + } + } + /*used for pruning quiescence search */ + state.best_pieces = [parseInt(best_pieces[0] * wmul + 0.5), + parseInt(best_pieces[1] * bmul + 0.5)]; + + var kx = [kings[0] % 10, kings[1] % 10]; + var ky = [parseInt(kings[0] / 10), parseInt(kings[1] / 10)]; + + /* find the frontmost pawns in each file */ + var pawn_cols = [[], []]; + for (y = 3; y < 9; y++){ + for (x = 1; x < 9; x++){ + i = y * 10 + x; + a = board[i]; + if ((a & 14) != P4_PAWN) + continue; + if ((a & 1) == 0){ + pawn_cols[0][x] = y; + } + else if (pawn_cols[1][x] === undefined){ + pawn_cols[1][x] = y; + } + } + } + var target_king = (moveno >= 20 || material_sum < 5 * qvalue); + var weights = state.weights; + + for (y = 2; y < 10; y++){ + for (x = 1; x < 9; x++){ + i = y * 10 + x; + var early_centre = P4_CENTRALISING_WEIGHTS[i] * earliness_weight; + var plateau = P4_KNIGHT_WEIGHTS[i]; + for (var c = 0; c < 2; c++){ + var dx = Math.abs(kx[1 - c] - x); + var dy = Math.abs(ky[1 - c] - y); + var our_dx = Math.abs(kx[c] - x); + var our_dy = Math.abs(ky[c] - y); + + var d = Math.max(Math.sqrt(dx * dx + dy * dy), 1) + 1; + var mul = multipliers[c]; /*(mul < 1) <==> we're winning*/ + var mul3 = mul * mul * mul; + var at_home = y == 2 + c * 7; + var pawn_home = y == 3 + c * 5; + var row4 = y == 5 + c; + var promotion_row = y == 9 - c * 7; + var get_out = (early && at_home) * -5; + + var knight = parseInt(early_centre * 0.3) + 2 * plateau + get_out; + var rook = parseInt(early_centre * 0.3); + var bishop = parseInt(early_centre * 0.6) + plateau + get_out; + if (at_home){ + rook += (x == 4 || x == 5) * (earliness_weight + ! target_king); + rook += (x == 1 || x == 8) * (moveno > 10 && moveno < 20) * -3; + rook += (x == 2 || x == 7) * (moveno > 10 && moveno < 20) * -1; + } + + /*Queen wants to stay home early, then jump right in*/ + /*keep kings back on home row for a while*/ + var queen = parseInt(plateau * 0.5 + early_centre * (0.5 - early)); + var king = (king_should_hide && at_home) * 2 * earliness_weight; + + /*empty board means pawn advancement is more urgent*/ + var get_on_with_it = Math.max(emptiness * 2, 1); + var pawn = get_on_with_it * P4_BASE_PAWN_WEIGHTS[c ? 119 - i : i]; + if (early){ + /* Early pawn weights are slightly randomised, so each game is different. + */ + if (y >= 4 && y <= 7){ + var boost = 1 + 3 * (y == 5 || y == 6); + pawn += parseInt((boost + p4_random_int(state, 4)) * 0.1 * + early_centre); + } + if (x == 4 || x == 5){ + //discourage middle pawns from waiting at home + pawn -= 3 * pawn_home; + pawn += 3 * row4; + } + } + /*pawn promotion row is weighted as a queen minus a pawn.*/ + if (promotion_row) + pawn += state.values[c][P4_QUEEN] - state.values[c][P4_PAWN]; + + /*pawns in front of a castled king should stay there*/ + pawn += 4 * (y == 3 && ky[c] == 2 && Math.abs(our_dx) < 2 && + kx[c] != 5 && x != 4 && x != 5); + /*passed pawns (having no opposing pawn in front) are encouraged. */ + var cols = pawn_cols[1 - c]; + if (cols[x] == undefined || + (c == 0 && cols[x] < y) || + (c == 1 && cols[x] > y)) + pawn += 2; + + /* After a while, start going for opposite king. Just + * attract pieces into the area so they can mill about in + * the area, waiting for an opportunity. + * + * As prepare is only called at the beginning of each tree + * search, the king could wander out of the targetted area + * in deep searches. But that's OK. Heuristics are + * heuristics. + */ + if (target_king){ + knight += 2 * parseInt(8 * mul / d); + rook += 2 * ((dx < 2) + (dy < 2)); + bishop += 3 * (Math.abs((dx - dy)) < 2); + queen += 2 * parseInt(8 / d) + (dx * dy == 0) + (dx - dy == 0); + /* The losing king wants to stay in the middle, while + the winning king goes in for the kill.*/ + var king_centre_wt = 8 * emptiness * P4_CENTRALISING_WEIGHTS[i]; + king += parseInt(150 * emptiness / (mul3 * d) + king_centre_wt * mul3); + } + weights[P4_PAWN + c][i] = pawn; + weights[P4_KNIGHT + c][i] = knight; + weights[P4_ROOK + c][i] = rook; + weights[P4_BISHOP + c][i] = bishop; + weights[P4_QUEEN + c][i] = queen; + weights[P4_KING + c][i] = king; + + if (draw_likely && mul < 1){ + /*The winning side wants to avoid draw, so adds jitter to its weights.*/ + var range = 3 / mul3; + for (j = 2 + c; j < 14; j += 2){ + weights[j][i] += p4_random_int(state, range); + } + } + } + } + } + state.prepared = true; +} + +function p4_maybe_prepare(state){ + if (! state.prepared) + p4_prepare(state); +} + + +function p4_parse(state, colour, ep, score) { + var board = state.board; + var s, e; //start and end position + var E, a; //E=piece at end place, a= piece moving + var i, j; + var other_colour = 1 - colour; + var dir = (10 - 20 * colour); //dir= 10 for white, -10 for black + var movelist = []; + var captures = []; + var weight; + var pieces = state.pieces[colour]; + var castle_flags = (state.castles >> (colour * 2)) & 3; + var values = state.values[other_colour]; + var all_weights = state.weights; + for (j = pieces.length - 1; j >= 0; j--){ + s = pieces[j][1]; // board position + a = board[s]; //piece number + var weight_lut = all_weights[a]; + weight = score - weight_lut[s]; + a &= 14; + if(a > 2){ //non-pawns + var moves = P4_MOVES[a]; + if(a & 2){ + for(i = 0; i < 8; i++){ + e = s + moves[i]; + E = board[e]; + if(!E){ + movelist.push([weight + values[E] + weight_lut[e], s, e]); + } + else if((E&17)==other_colour){ + captures.push([weight + values[E] + weight_lut[e] + all_weights[E][e], s, e]); + } + } + if(a == P4_KING && castle_flags){ + if((castle_flags & 1) && + (board[s - 1] + board[s - 2] + board[s - 3] == 0) && + p4_check_castling(board, s - 2, other_colour, dir, -1)){//Q side + movelist.push([weight + 12, s, s - 2]); //no analysis, just encouragement + } + if((castle_flags & 2) && (board[s + 1] + board[s + 2] == 0) && + p4_check_castling(board, s, other_colour, dir, 1)){//K side + movelist.push([weight + 13, s, s + 2]); + } + } + } + else{//rook, bishop, queen + var mlen = moves.length; + for(i=0;i= 0; i--){ + var mv = movelist[i]; + var score = mv[0]; + s = mv[1]; + e = mv[2]; + if(! board[e]){ + var x = scores[s]; + x.score = Math.max(x.score, score); + } + } + /* moving out of a threat is worth considering, especially + * if it is a pawn and you are not.*/ + for(i = threats.length - 1; i >= 0; i--){ + var mv = threats[i]; + var x = scores[mv[2]]; + if (x !== undefined){ + var S = board[mv[1]]; + var r = (1 + x.piece > 3 + S < 4) * 0.01; + if (x.threatened < r) + x.threatened = r; + } + } + var pieces2 = []; + for (i = 20; i < 100; i++){ + p = scores[i]; + if (p !== undefined){ + p.score += p.threatened * our_values[p.piece]; + pieces2.push(p); + } + } + pieces2.sort(function(a, b){return a.score - b.score;}); + for (i = 0; i < pieces2.length; i++){ + p = pieces2[i]; + pieces[i] = [p.piece, p.pos]; + } + } +} + +function p4_findmove(state, level, colour, ep){ + p4_prepare(state); + p4_optimise_piece_list(state); + var board = state.board; + + // Changed for espruino compatibility + if (colour === undefined) { + colour = state.to_play; + } + if (ep === undefined) { + ep = state.enpassant; + } + var movelist = p4_parse(state, colour, ep, 0); + var alpha = P4_MIN_SCORE; + var mv, t, i; + var bs = 0; + var be = 0; + + if (level <= 0){ + for (i = 0; i < movelist.length; i++){ + mv = movelist[i]; + if(movelist[i][0] > alpha){ + alpha = mv[0]; + bs = mv[1]; + be = mv[2]; + } + } + return [bs, be, alpha]; + } + + for(i = 0; i < movelist.length; i++){ + mv = movelist[i]; + var mscore = mv[0]; + var ms = mv[1]; + var me = mv[2]; + if (mscore > P4_WIN){ + p4_log("XXX taking king! it should never come to this"); + alpha = P4_KING_VALUE; + bs = ms; + be = me; + break; + } + t = -state.treeclimber(state, level - 1, 1 - colour, mscore, ms, me, + P4_MIN_SCORE, -alpha); + if (t > alpha){ + alpha = t; + bs = ms; + be = me; + } + } + if (alpha < -P4_WIN_NOW && ! p4_check_check(state, colour)){ + alpha = state.stalemate_scores[colour]; + } + return [bs, be, alpha]; +} + +/*p4_make_move changes the state and returns an object containing + * everything necesary to undo the change. + * + * p4_unmake_move uses the p4_make_move return value to restore the + * previous state. + */ + +function p4_make_move(state, s, e, promotion){ + var board = state.board; + var S = board[s]; + var E = board[e]; + board[e] = S; + board[s] = 0; + var piece = S & 14; + var moved_colour = S & 1; + var end_piece = S; /* can differ from S in queening*/ + //now some stuff to handle queening, castling + var rs = 0, re, rook; + var ep_taken = 0, ep_position; + var ep = 0; + if(piece == P4_PAWN){ + if((60 - e) * (60 - e) > 900){ + /*got to end; replace the pawn on board and in pieces cache.*/ + promotion |= moved_colour; + board[e] = promotion; + end_piece = promotion; + } + else if (((s ^ e) & 1) && E == 0){ + /*this is a diagonal move, but the end spot is empty, so we surmise enpassant */ + ep_position = e - 10 + 20 * moved_colour; + ep_taken = board[ep_position]; + board[ep_position] = 0; + } + else if ((s - e) * (s - e) == 400){ + /*delta is 20 --> two row jump at start*/ + ep = (s + e) >> 1; + } + } + else if (piece == P4_KING && ((s - e) * (s - e) == 4)){ //castling - move rook too + rs = s - 4 + (s < e) * 7; + re = (s + e) >> 1; //avg of s,e=rook's spot + rook = moved_colour + P4_ROOK; + board[rs] = 0; + board[re] = rook; + //piece_locations.push([rook, re]); + } + + var old_castle_state = state.castles; + if (old_castle_state){ + var mask = 0; + var shift = moved_colour * 2; + var side = moved_colour * 70; + var s2 = s - side; + var e2 = e + side; + //wipe both our sides if king moves + if (s2 == 25) + mask |= 3 << shift; + //wipe one side on any move from rook points + else if (s2 == 21) + mask |= 1 << shift; + else if (s2 == 28) + mask |= 2 << shift; + //or on any move *to* opposition corners + if (e2 == 91) + mask |= 4 >> shift; + else if (e2 == 98) + mask |= 8 >> shift; + state.castles &= ~mask; + } + + var old_pieces = state.pieces.concat(); + var our_pieces = old_pieces[moved_colour]; + var dest = state.pieces[moved_colour] = []; + for (var i = 0; i < our_pieces.length; i++){ + var x = our_pieces[i]; + var pp = x[0]; + var ps = x[1]; + if (ps != s && ps != rs){ + dest.push(x); + } + } + dest.push([end_piece, e]); + if (rook) + dest.push([rook, re]); + + if (E || ep_taken){ + var their_pieces = old_pieces[1 - moved_colour]; + dest = state.pieces[1 - moved_colour] = []; + var gone = ep_taken ? ep_position : e; + for (i = 0; i < their_pieces.length; i++){ + var x = their_pieces[i]; + if (x[1] != gone){ + dest.push(x); + } + } + } + + return { + /*some of these (e.g. rook) could be recalculated during + * unmake, possibly more cheaply. */ + s: s, + e: e, + S: S, + E: E, + ep: ep, + castles: old_castle_state, + rs: rs, + re: re, + rook: rook, + ep_position: ep_position, + ep_taken: ep_taken, + pieces: old_pieces + }; +} + +function p4_unmake_move(state, move){ + var board = state.board; + if (move.ep_position){ + board[move.ep_position] = move.ep_taken; + } + board[move.s] = move.S; + board[move.e] = move.E; + //move.piece_locations.length--; + if(move.rs){ + board[move.rs] = move.rook; + board[move.re] = 0; + //move.piece_locations.length--; + } + state.pieces = move.pieces; + state.castles = move.castles; +} + + +function p4_insufficient_material(state){ + var knights = false; + var bishops = undefined; + var i; + var board = state.board; + for(i = 20; i < 100; i++){ + var piece = board[i] & 14; + if(piece == 0 || piece == P4_KING){ + continue; + } + if (piece == P4_KNIGHT){ + /* only allow one knight of either colour, never with a bishop */ + if (knights || bishops !== undefined){ + return false; + } + knights = true; + } + else if (piece == P4_BISHOP){ + /*any number of bishops, but on only one colour square */ + var x = i & 1; + var y = parseInt(i / 10) & 1; + var parity = x ^ y; + if (knights){ + return false; + } + else if (bishops === undefined){ + bishops = parity; + } + else if (bishops != parity){ + return false; + } + } + else { + return false; + } + } + return true; +} + +/* p4_move(state, s, e, promotion) + * s, e are start and end positions + * + * promotion is the desired pawn promotion if the move gets a pawn to the other + * end. + * + * return value contains bitwise flags +*/ + +var P4_MOVE_FLAG_OK = 1; +var P4_MOVE_FLAG_CHECK = 2; +var P4_MOVE_FLAG_MATE = 4; +var P4_MOVE_FLAG_CAPTURE = 8; +var P4_MOVE_FLAG_CASTLE_KING = 16; +var P4_MOVE_FLAG_CASTLE_QUEEN = 32; +var P4_MOVE_FLAG_DRAW = 64; + +var P4_MOVE_ILLEGAL = 0; +var P4_MOVE_MISSED_MATE = P4_MOVE_FLAG_CHECK | P4_MOVE_FLAG_MATE; +var P4_MOVE_CHECKMATE = P4_MOVE_FLAG_OK | P4_MOVE_FLAG_CHECK | P4_MOVE_FLAG_MATE; +var P4_MOVE_STALEMATE = P4_MOVE_FLAG_OK | P4_MOVE_FLAG_MATE; + +function p4_move(state, s, e, promotion){ + var board = state.board; + var colour = state.to_play; + var other_colour = 1 - colour; + if (s != parseInt(s)){ + if (e === undefined){ + var mv = p4_interpret_movestring(state, s); + s = mv[0]; + e = mv[1]; + if (s == 0) + return {flags: P4_MOVE_ILLEGAL, ok: false}; + promotion = mv[2]; + } + else {/*assume two point strings: 'e2', 'e4'*/ + s = p4_destringify_point(s); + e = p4_destringify_point(e); + } + } + if (promotion === undefined) + promotion = P4_QUEEN; + var E=board[e]; + var S=board[s]; + + /*See if this move is even slightly legal, disregarding check. + */ + var i; + var legal = false; + p4_maybe_prepare(state); + var moves = p4_parse(state, colour, state.enpassant, 0); + for (i = 0; i < moves.length; i++){ + if (e == moves[i][2] && s == moves[i][1]){ + legal = true; + break; + } + } + if (! legal) { + return {flags: P4_MOVE_ILLEGAL, ok: false}; + } + + /*Try the move, and see what the response is.*/ + var changes = p4_make_move(state, s, e, promotion); + + /*is it check? */ + if (p4_check_check(state, colour)){ + p4_unmake_move(state, changes); + p4_log('in check', changes); + return {flags: P4_MOVE_ILLEGAL, ok: false, string: "in check!"}; + } + /*The move is known to be legal. We won't be undoing it.*/ + + var flags = P4_MOVE_FLAG_OK; + + state.enpassant = changes.ep; + state.history.push([s, e, promotion]); + + /*draw timeout: 50 moves without pawn move or capture is a draw */ + if (changes.E || changes.ep_position){ + state.draw_timeout = 0; + flags |= P4_MOVE_FLAG_CAPTURE; + } + else if ((S & 14) == P4_PAWN){ + state.draw_timeout = 0; + } + else{ + state.draw_timeout++; + } + if (changes.rs){ + flags |= (s > e) ? P4_MOVE_FLAG_CASTLE_QUEEN : P4_MOVE_FLAG_CASTLE_KING; + } + var shortfen = p4_state2fen(state, true); + var repetitions = (state.position_counts[shortfen] || 0) + 1; + state.position_counts[shortfen] = repetitions; + state.current_repetitions = repetitions; + if (state.draw_timeout > 100 || repetitions >= 3 || + p4_insufficient_material(state)){ + flags |= P4_MOVE_FLAG_DRAW; + } + state.moveno++; + state.to_play = other_colour; + + if (p4_check_check(state, other_colour)){ + flags |= P4_MOVE_FLAG_CHECK; + } + /* check for (stale|check)mate, by seeing if there is a move for + * the other side that doesn't result in check. (In other words, + * reduce the pseudo-legal-move list down to a legal-move list, + * and check it isn't empty). + * + * We don't need to p4_prepare because other colour pieces can't + * have moved (just disappeared) since previous call. Also, + * setting the promotion piece is unnecessary, because all + * promotions block check equally well. + */ + var is_mate = true; + var replies = p4_parse(state, other_colour, changes.ep, 0); + for (i = 0; i < replies.length; i++){ + var m = replies[i]; + var change2 = p4_make_move(state, m[1], m[2], P4_QUEEN); + var check = p4_check_check(state, other_colour); + p4_unmake_move(state, change2); + if (!check){ + is_mate = false; + break; + } + } + if (is_mate) + flags |= P4_MOVE_FLAG_MATE; + + var movestring = p4_move2string(state, s, e, S, promotion, flags, moves); + p4_log("successful move", s, e, movestring, flags); + state.prepared = false; + return { + flags: flags, + string: movestring, + ok: true + }; +} + + +function p4_move2string(state, s, e, S, promotion, flags, moves){ + var piece = S & 14; + var src, dest; + var mv, i; + var capture = flags & P4_MOVE_FLAG_CAPTURE; + + src = p4_stringify_point(s); + dest = p4_stringify_point(e); + if (piece == P4_PAWN){ + if (capture){ + mv = src.charAt(0) + 'x' + dest; + } + else + mv = dest; + if (e > 90 || e < 30){ //end row, queening + if (promotion === undefined) + promotion = P4_QUEEN; + mv += '=' + P4_ENCODE_LUT.charAt(promotion); + } + } + else if (piece == P4_KING && (s-e) * (s-e) == 4) { + if (e < s) + mv = 'O-O-O'; + else + mv = 'O-O'; + } + else { + var row_qualifier = ''; + var col_qualifier = ''; + var pstr = P4_ENCODE_LUT.charAt(S); + var sx = s % 10; + var sy = parseInt(s / 10); + + /* find any other pseudo-legal moves that would put the same + * piece in the same place, for which we'd need + * disambiguation. */ + var co_landers = []; + for (i = 0; i < moves.length; i++){ + var m = moves[i]; + if (e == m[2] && s != m[1] && state.board[m[1]] == S){ + co_landers.push(m[1]); + } + } + if (co_landers.length){ + for (i = 0; i < co_landers.length; i++){ + var c = co_landers[i]; + var cx = c % 10; + var cy = parseInt(c / 10); + if (cx == sx)/*same column, so qualify by row*/ + row_qualifier = src.charAt(1); + if (cy == sy) + col_qualifier = src.charAt(0); + } + if (row_qualifier == '' && col_qualifier == ''){ + /*no co-landers on the same rank or file, so one or the other will do. + * By convention, use the column (a-h) */ + col_qualifier = src.charAt(0); + } + } + mv = pstr + col_qualifier + row_qualifier + (capture ? 'x' : '') + dest; + } + if (flags & P4_MOVE_FLAG_CHECK){ + if (flags & P4_MOVE_FLAG_MATE) + mv += '#'; + else + mv += '+'; + } + else if (flags & P4_MOVE_FLAG_MATE) + mv += ' stalemate'; + return mv; +} + + +function p4_jump_to_moveno(state, moveno){ + p4_log('jumping to move', moveno); + if (moveno === undefined || moveno > state.moveno) + moveno = state.moveno; + else if (moveno < 0){ + moveno = state.moveno + moveno; + } + var state2 = p4_fen2state(state.beginning); + var i = 0; + while (state2.moveno < moveno){ + var m = state.history[i++]; + p4_move(state2, m[0], m[1], m[2]); + } + /* copy the replayed state across, not all that deeply, but + * enough to cover, eg, held references to board. */ + var attr, dest; + for (attr in state2){ + var src = state2[attr]; + if (attr instanceof Array){ + dest = state[attr]; + dest.length = 0; + for (i = 0; i < src.length; i++){ + dest[i] = src[i]; + } + } + else { + state[attr] = src; + } + } + state.prepared = false; +} + + +/* write a standard FEN notation + * http://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation + * */ +function p4_state2fen(state, reduced){ + var piece_lut = ' PpRrNnBbKkQq'; + var board = state.board; + var fen = ''; + //fen does Y axis backwards, X axis forwards */ + for (var y = 9; y > 1; y--){ + var count = 0; + for (var x = 1; x < 9; x++){ + var piece = board[y * 10 + x]; + if (piece == 0) + count++; + else{ + if (count) + fen += count.toString(); + fen += piece_lut.charAt(piece); + count = 0; + } + } + if (count) + fen += count; + if (y > 2) + fen += '/'; + } + /*white or black */ + fen += ' ' + 'wb'.charAt(state.to_play) + ' '; + /*castling */ + if (state.castles){ + var lut = [2, 'K', 1, 'Q', 8, 'k', 4, 'q']; + for (var i = 0; i < 8; i += 2){ + if (state.castles & lut[i]){ + fen += lut[i + 1]; + } + } + } + else + fen += '-'; + /*enpassant */ + if (state.enpassant !== 0){ + fen += ' ' + p4_stringify_point(state.enpassant); + } + else + fen += ' -'; + if (reduced){ + /*if the 'reduced' flag is set, the move number and draw + *timeout are not added. This form is used to detect draws by + *3-fold repetition.*/ + return fen; + } + fen += ' ' + state.draw_timeout + ' '; + fen += (state.moveno >> 1) + 1; + return fen; +} + +function p4_stringify_point(p){ + var letters = " abcdefgh"; + var x = p % 10; + var y = (p - x) / 10 - 1; + return letters.charAt(x) + y; +} + +function p4_destringify_point(p){ + var x = parseInt(p.charAt(0), 19) - 9; //a-h <-> 10-18, base 19 + var y = parseInt(p.charAt(1)) + 1; + if (y >= 2 && y < 10 && x >= 1 && x < 9) + return y * 10 + x; + return undefined; +} + +/* read a standard FEN notation + * http://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notation + * */ +function p4_fen2state(fen, state){ + if (state === undefined) + state = p4_initialise_state(); + var board = state.board; + var fenbits = fen.split(' '); + var fen_board = fenbits[0]; + var fen_toplay = fenbits[1]; + var fen_castles = fenbits[2]; + var fen_enpassant = fenbits[3]; + var fen_timeout = fenbits[4]; + var fen_moveno = fenbits[5]; + if (fen_timeout === undefined) + fen_timeout = 0; + //fen does Y axis backwards, X axis forwards */ + var y = 90; + var x = 1; + var i, c; + for (var j = 0; j < fen_board.length; j++){ + c = fen_board.charAt(j); + if (c == '/'){ + x = 1; + y -= 10; + if (y < 20) + break; + continue; + } + var piece = P4_PIECE_LUT[c]; + if (piece && x < 9){ + board[y + x] = piece; + x++; + } + else { + var end = Math.min(x + parseInt(c), 9); + for (; x < end; x++){ + board[y + x] = 0; + } + } + } + state.to_play = (fen_toplay.toLowerCase() == 'b') ? 1 : 0; + state.castles = 0; + /* Sometimes we meet bad FEN that says it can castle when it can't. */ + var wk = board[25] == P4_KING; + var bk = board[95] == P4_KING + 1; + var castle_lut = { + k: 8 * (bk && board[98] == P4_ROOK + 1), + q: 4 * (bk && board[91] == P4_ROOK + 1), + K: 2 * (wk && board[28] == P4_ROOK), + Q: 1 * (wk && board[21] == P4_ROOK) + }; + for (i = 0; i < fen_castles.length; i++){ + c = fen_castles.charAt(i); + var castle = castle_lut[c]; + if (castle !== undefined){ + state.castles |= castle; + if (castle == 0){ + console.log("FEN claims castle state " + fen_castles + + " but pieces are not in place for " + c); + } + } + } + + state.enpassant = (fen_enpassant != '-') ? p4_destringify_point(fen_enpassant) : 0; + state.draw_timeout = parseInt(fen_timeout); + if (fen_moveno === undefined){ + /*have a guess based on entropy and pieces remaining*/ + var pieces = 0; + var mix = 0; + var p, q, r; + for (y = 20; y < 100; y+=10){ + for (x = 1; x < 9; x++){ + p = board[y + x] & 15; + pieces += (!!p); + if (x < 8){ + q = board[y + x + 1]; + mix += (!q) != (!p); + } + if (y < 90){ + q = board[y + x + 10]; + mix += (!q) != (!p); + } + } + } + fen_moveno = Math.max(1, parseInt((32 - pieces) * 1.3 + (4 - fen_castles.length) * 1.5 + ((mix - 16) / 5))); + //p4_log("pieces", pieces, "mix", mix, "estimate", fen_moveno); + } + state.moveno = 2 * (parseInt(fen_moveno) - 1) + state.to_play; + state.history = []; + state.beginning = fen; + state.prepared = false; + state.position_counts = {}; + /* Wrap external functions as methods. */ + state.move = function(s, e, promotion){ + return p4_move(this, s, e, promotion); + }; + state.findmove = function(level){ + return p4_findmove(this, level); + }; + state.jump_to_moveno = function(moveno){ + return p4_jump_to_moveno(this, moveno); + }; + return state; +} + +/* +Weights would all fit within an Int8Array *except* for the last row +for pawns, which is close to the queen value (180, max is 127). + +Int8Array seems slightly quicker in Chromium 18, no different in +Firefox 12. + +Int16Array is no faster, perhaps slower than Int32Array. + +Int32Array is marginally slower than plain arrays with Firefox 12, but +significantly quicker with Chromium. + */ + +var P4_ZEROS = []; +function p4_zero_array(){ + if (P4_USE_TYPED_ARRAYS) + return new Int32Array(120); + if (P4_ZEROS.length == 0){ + for(var i = 0; i < 120; i++){ + P4_ZEROS[i] = 0; + } + } + return P4_ZEROS.slice(); +} + +/* p4_initialise_state() creates the board and initialises weight + * arrays etc. Some of this is really only needs to be done once. + */ + +function p4_initialise_state(){ + var board = p4_zero_array(); + P4_CENTRALISING_WEIGHTS = p4_zero_array(); + P4_BASE_PAWN_WEIGHTS = p4_zero_array(); + P4_KNIGHT_WEIGHTS = p4_zero_array(); + for(var i = 0; i < 120; i++){ + var y = parseInt(i / 10); + var x = i % 10; + var dx = Math.abs(x - 4.5); + var dy = Math.abs(y - 5.5); + P4_CENTRALISING_WEIGHTS[i] = parseInt(6 - Math.pow((dx * dx + dy * dy) * 1.5, 0.6)); + //knights have a flat topped centre (bishops too, but less so). + P4_KNIGHT_WEIGHTS[i] = parseInt(((dx < 2) + (dy < 2) * 1.5) + + (dx < 3) + (dy < 3)) - 2; + P4_BASE_PAWN_WEIGHTS[i] = parseInt('000012347000'.charAt(y)); + if (y > 9 || y < 2 || x < 1 || x > 8) + board[i] = 16; + } + var weights = []; + for (i = 0; i < 14; i++){ + weights[i] = p4_zero_array(); + } + var state = { + board: board, + weights: weights, + history: [], + treeclimber: p4_alphabeta_treeclimber + }; + p4_random_seed(state, P4_DEBUG ? 1 : Date.now()); + return state; +} + +function p4_new_game(){ + return p4_fen2state(P4_INITIAL_BOARD); +} + +/*convert an arbitrary movestring into a pair of integers offsets into + * the board. The string might be in any of these forms: + * + * "d2-d4" "d2d4" "d4" -- moving a pawn + * + * "b1-c3" "b1c3" "Nc3" "N1c3" "Nbc3" "Nb1c3" -- moving a knight + * + * "b1xc3" "b1xc3" "Nxc3" -- moving a knight, also happens to capture. + * + * "O-O" "O-O-O" -- special cases for castling ("e1-c1", etc, also work) + * + * Note that for the "Nc3" (pgn) format, some knowledge of the board + * is necessary, so the state parameter is required. If it is + * undefined, the other forms will still work. + */ + +function p4_interpret_movestring(state, str){ + /* Ignore any irrelevant characters, then tokenise. + * + */ + var FAIL = [0, 0]; + var algebraic_re = /^\s*([RNBQK]?[a-h]?[1-8]?)[ :x-]*([a-h][1-8]?)(=[RNBQ])?[!?+#e.p]*\s*$/; + var castle_re = /^\s*([O0o]-[O0o](-[O0o])?)\s*$/; + var position_re = /^[a-h][1-8]$/; + + var m = algebraic_re.exec(str); + if (m == null){ + /*check for castling notation (O-O, O-O-O) */ + m = castle_re.exec(str); + if (m){ + s = 25 + state.to_play * 70; + if (m[2])/*queenside*/ + e = s - 2; + else + e = s + 2; + } + else + return FAIL; + } + var src = m[1]; + var dest = m[2]; + var queen = m[3]; + var s, e, q; + var moves, i; + if (src == '' || src == undefined){ + /* a single coordinate pawn move */ + e = p4_destringify_point(dest); + s = p4_find_source_point(state, e, 'P' + dest.charAt(0)); + } + else if (/^[RNBQK]/.test(src)){ + /*pgn format*/ + e = p4_destringify_point(dest); + s = p4_find_source_point(state, e, src); + } + else if (position_re.test(src) && position_re.test(dest)){ + s = p4_destringify_point(src); + e = p4_destringify_point(dest); + } + else if (/^[a-h]$/.test(src)){ + e = p4_destringify_point(dest); + s = p4_find_source_point(state, e, 'P' + src); + } + if (s == 0) + return FAIL; + + if (queen){ + /* the chosen queen piece */ + q = P4_PIECE_LUT[queen.charAt(1)]; + } + return [s, e, q]; +} + + +function p4_find_source_point(state, e, str){ + var colour = state.to_play; + var piece = P4_PIECE_LUT[str.charAt(0)]; + piece |= colour; + var s, i; + + var row, column; + /* can be specified as Na, Na3, N3, and who knows, N3a? */ + for (i = 1; i < str.length; i++){ + var c = str.charAt(i); + if (/[a-h]/.test(c)){ + column = str.charCodeAt(i) - 96; + } + else if (/[1-8]/.test(c)){ + /*row goes 2 - 9 */ + row = 1 + parseInt(c); + } + } + var possibilities = []; + p4_prepare(state); + var moves = p4_parse(state, colour, + state.enpassant, 0); + for (i = 0; i < moves.length; i++){ + var mv = moves[i]; + if (e == mv[2]){ + s = mv[1]; + if (state.board[s] == piece && + (column === undefined || column == s % 10) && + (row === undefined || row == parseInt(s / 10)) + ){ + var change = p4_make_move(state, s, e, P4_QUEEN); + if (! p4_check_check(state, colour)) + possibilities.push(s); + p4_unmake_move(state, change); + } + } + } + p4_log("finding", str, "that goes to", e, "got", possibilities); + + if (possibilities.length == 0){ + return 0; + } + else if (possibilities.length > 1){ + p4_log("p4_find_source_point seems to have failed", + state, e, str, + possibilities); + } + return possibilities[0]; +} + + +/*random number generator based on + * http://burtleburtle.net/bob/rand/smallprng.html + */ +function p4_random_seed(state, seed){ + seed &= 0xffffffff; + state.rng = (P4_USE_TYPED_ARRAYS) ? new Uint32Array(4) : []; + state.rng[0] = 0xf1ea5eed; + state.rng[1] = seed; + state.rng[2] = seed; + state.rng[3] = seed; + for (var i = 0; i < 20; i++) + p4_random31(state); +} + +function p4_random31(state){ + var rng = state.rng; + var b = rng[1]; + var c = rng[2]; + /* These shifts amount to rotates. + * Note the three-fold right shift '>>>', meaning an unsigned shift. + * The 0xffffffff masks are needed to keep javascript to 32bit. (supposing + * untyped arrays). + */ + var e = rng[0] - ((b << 27) | (b >>> 5)); + rng[0] = b ^ ((c << 17) | (c >>> 15)); + rng[1] = (c + rng[3]) & 0xffffffff; + rng[2] = (rng[3] + e) & 0xffffffff; + rng[3] = (e + rng[0]) & 0xffffffff; + return rng[3] & 0x7fffffff; +} + +function p4_random_int(state, top){ + /* uniform integer in range [0 <= n < top), supposing top < 2 ** 31 + * + * This method is slightly (probably pointlessly) more accurate + * than converting to 0-1 float, multiplying and truncating, and + * considerably more accurate than a simple modulus. + * Obviously it is a bit slower. + */ + /* mask becomes one less than the next highest power of 2 */ + var mask = top; + mask--; + mask |= mask >>> 1; + mask |= mask >>> 2; + mask |= mask >>> 4; + mask |= mask >>> 8; + mask |= mask >>> 16; + var r; + do{ + r = p4_random31(state) & mask; + } while (r >= top); + return r; +} + +// Added for espruino +exports.p4_new_game = p4_new_game; +exports.p4_fen2state = p4_fen2state; +exports.p4_state2fen = p4_state2fen; +exports.P4_INITIAL_BOARD = P4_INITIAL_BOARD; +exports.P4_PAWN = P4_PAWN; +exports.P4_ROOK = P4_ROOK; +exports.P4_KNIGHT = P4_KNIGHT; +exports.P4_BISHOP = P4_BISHOP; +exports.P4_QUEEN = P4_QUEEN; +exports.P4_KING = P4_KING; +exports.P4_MOVE_FLAG_OK = P4_MOVE_FLAG_OK; +exports.P4_MOVE_FLAG_CHECK = P4_MOVE_FLAG_CHECK; +exports.P4_MOVE_FLAG_MATE = P4_MOVE_FLAG_MATE; +exports.P4_MOVE_FLAG_CAPTURE = P4_MOVE_FLAG_CAPTURE; +exports.P4_MOVE_FLAG_CASTLE_KING = P4_MOVE_FLAG_CASTLE_KING; +exports.P4_MOVE_FLAG_CASTLE_QUEEN = P4_MOVE_FLAG_CASTLE_QUEEN; +exports.P4_MOVE_FLAG_DRAW = P4_MOVE_FLAG_DRAW; +exports.P4_MOVE_ILLEGAL = P4_MOVE_ILLEGAL; +exports.P4_MOVE_MISSED_MATE = P4_MOVE_MISSED_MATE; +exports.P4_MOVE_CHECKMATE = P4_MOVE_CHECKMATE; +exports.P4_MOVE_STALEMATE = P4_MOVE_STALEMATE; diff --git a/apps/chess/metadata.json b/apps/chess/metadata.json new file mode 100644 index 000000000..a66391deb --- /dev/null +++ b/apps/chess/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "chess", + "name": "Chess", + "shortName": "Chess", + "version": "0.01", + "description": "Chess game based on the [p4wn engine](https://p4wn.sourceforge.net/). Drag on the touchscreen to move the green cursor onto a piece, select it with a single touch and drag the now red cursor around. Release the piece with another touch to finish the move. The button opens a menu.", + "icon": "app.png", + "tags": "game", + "supports": ["BANGLEJS2"], + "storage": [ + {"name":"chess.app.js","url":"app.js"}, + {"name":"chessengine","url":"engine.js"}, + {"name":"chess.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"chess.json"}], + "screenshots": [ {"url":"screenshot.png"} ] +} diff --git a/apps/chess/screenshot.png b/apps/chess/screenshot.png new file mode 100644 index 000000000..11d96163b Binary files /dev/null and b/apps/chess/screenshot.png differ diff --git a/apps/chimer/ChangeLog b/apps/chimer/ChangeLog index 51842b5cd..6c6f5312e 100644 --- a/apps/chimer/ChangeLog +++ b/apps/chimer/ChangeLog @@ -1,3 +1,4 @@ 0.01: Initial Creation 0.02: Fixed some sleep bugs. Added a sleep mode toggle 0.03: Reduce busy-loop and code +0.04: Separate buzz-time and sleep-time diff --git a/apps/chimer/metadata.json b/apps/chimer/metadata.json index dfbabf405..cfa0da00f 100644 --- a/apps/chimer/metadata.json +++ b/apps/chimer/metadata.json @@ -1,7 +1,7 @@ { "id": "chimer", "name": "Chimer", - "version": "0.03", + "version": "0.04", "description": "A fork of Hour Chime that adds extra features such as: \n - Buzz or beep on every 60, 30 or 15 minutes. \n - Repeat Chime up to 3 times \n - Set hours to disable chime", "icon": "widget.png", "type": "widget", diff --git a/apps/chimer/widget.js b/apps/chimer/widget.js index a587b61de..3b7de9d7a 100644 --- a/apps/chimer/widget.js +++ b/apps/chimer/widget.js @@ -20,15 +20,16 @@ let count = settings.repeat; const chime1 = () => { + let p; if (settings.type === 1) { - Bangle.buzz(100); + p = Bangle.buzz(100); } else if (settings.type === 2) { - Bangle.beep(); + p = Bangle.beep(); } else { return; } if (--count > 0) - setTimeout(chime1, 150); + p.then(() => setTimeout(chime1, 150)); }; chime1(); diff --git a/apps/clkinfostopw/settings.ts b/apps/clkinfostopw/settings.ts deleted file mode 100644 index a7bfce1e5..000000000 --- a/apps/clkinfostopw/settings.ts +++ /dev/null @@ -1,36 +0,0 @@ -const enum StopWatchFormat { - HMS, - Colon, -} -type StopWatchSettings = { - format: StopWatchFormat, -}; - -(back => { - const SETTINGS_FILE = "clkinfostopw.setting.json"; - - const storage = require("Storage"); - const settings: StopWatchSettings = Object.assign( - { format: StopWatchFormat.HMS }, - storage.readJSON(SETTINGS_FILE, true), - ); - - const save = () => { - storage.writeJSON(SETTINGS_FILE, settings) - }; - - E.showMenu({ - "": { "title": "stopwatch" }, - "< Back": back, - "Format": { - value: settings.format, - min: StopWatchFormat.HMS, - max: StopWatchFormat.Colon, - format: v => v === StopWatchFormat.HMS ? "12m34s" : "12:34", - onchange: v => { - settings.format = v; - save(); - }, - }, - }); -}) satisfies SettingsFunc diff --git a/apps/clkinfosunrise/metadata.json b/apps/clkinfosunrise/metadata.json index d130c6453..7bcbb289b 100644 --- a/apps/clkinfosunrise/metadata.json +++ b/apps/clkinfosunrise/metadata.json @@ -5,6 +5,7 @@ "icon": "app.png", "type": "clkinfo", "tags": "clkinfo,sunrise", + "dependencies": {"mylocation":"app"}, "supports" : ["BANGLEJS2"], "storage": [ {"name":"sunrise.clkinfo.js","url":"clkinfo.js"} diff --git a/apps/clock_info/ChangeLog b/apps/clock_info/ChangeLog index ae33f6f26..e12b30692 100644 --- a/apps/clock_info/ChangeLog +++ b/apps/clock_info/ChangeLog @@ -3,4 +3,5 @@ 0.03: Reported image for battery now reflects charge level 0.04: On 2v18+ firmware, we can now stop swipe events from being handled by other apps eg. when a clockinfo is selected, swipes won't affect swipe-down widgets -0.05: Reported image for battery is now transparent (2v18+) \ No newline at end of file +0.05: Reported image for battery is now transparent (2v18+) +0.06: When >1 clockinfo, swiping one back tries to ensure they don't display the same thing \ No newline at end of file diff --git a/apps/clock_info/lib.js b/apps/clock_info/lib.js index 206022272..9dd975f1e 100644 --- a/apps/clock_info/lib.js +++ b/apps/clock_info/lib.js @@ -10,7 +10,12 @@ if (stepGoal == undefined) { stepGoal = d != undefined && d.settings != undefined ? d.settings.goal : 10000; } -// Load the settings, with defaults +/// How many times has addInteractive been called? +exports.loadCount = 0; +/// A list of all the instances returned by addInteractive +exports.clockInfos = []; + +/// Load the settings, with defaults exports.loadSettings = function() { return Object.assign({ hrmOn : 0, // 0(Always), 1(Tap) @@ -22,6 +27,7 @@ exports.loadSettings = function() { ); }; +/// Load a list of ClockInfos - this does not cache and reloads each time exports.load = function() { var settings = exports.loadSettings(); delete settings.apps; // keep just the basic settings in memory @@ -63,7 +69,7 @@ exports.load = function() { } else img=atob("GBiBAAABgAADwAAHwAAPgACfAAHOAAPkBgHwDwP4Hwf8Pg/+fB//OD//kD//wD//4D//8D//4B//QB/+AD/8AH/4APnwAHAAACAAAA=="); return { text : v + "%", v : v, min:0, max:100, img : img - } + }; }, show : function() { this.interval = setInterval(()=>this.emit('redraw'), 60000); Bangle.on("charging", batteryUpdateHandler); batteryUpdateHandler(); }, hide : function() { clearInterval(this.interval); delete this.interval; Bangle.removeListener("charging", batteryUpdateHandler); }, @@ -73,7 +79,7 @@ exports.load = function() { get : () => { let v = Bangle.getHealthStatus("day").steps; return { text : v, v : v, min : 0, max : stepGoal, img : atob("GBiBAAcAAA+AAA/AAA/AAB/AAB/gAA/g4A/h8A/j8A/D8A/D+AfH+AAH8AHn8APj8APj8AHj4AHg4AADAAAHwAAHwAAHgAAHgAADAA==") - }}, + };}, show : function() { Bangle.on("step", stepUpdateHandler); stepUpdateHandler(); }, hide : function() { Bangle.removeListener("step", stepUpdateHandler); }, }, @@ -82,7 +88,7 @@ exports.load = function() { get : () => { return { text : (hrm||"--") + " bpm", v : hrm, min : 40, max : 200, img : atob("GBiBAAAAAAAAAAAAAAAAAAAAAADAAADAAAHAAAHjAAHjgAPngH9n/n82/gA+AAA8AAA8AAAcAAAYAAAYAAAAAAAAAAAAAAAAAAAAAA==") - }}, + };}, run : function() { Bangle.setHRMPower(1,"clkinfo"); if (settings.hrmOn==1/*Tap*/) { @@ -131,11 +137,11 @@ exports.load = function() { require("Storage").list(/clkinfo.js$/).forEach(fn => { try{ var a = eval(require("Storage").read(fn))(); - var b = menu.find(x => x.name === a.name) + var b = menu.find(x => x.name === a.name); if(b) b.items = b.items.concat(a.items); else menu = menu.concat(a); } catch(e){ - console.log("Could not load clock info "+E.toJS(fn)) + console.log("Could not load clock info "+E.toJS(fn)); } }); @@ -204,11 +210,12 @@ exports.addInteractive = function(menu, options) { if ("function" == typeof options) options = {draw:options}; // backwards compatibility options.index = 0|exports.loadCount; exports.loadCount = options.index+1; + exports.clockInfos[options.index] = options; options.focus = options.index==0 && options.x===undefined; // focus if we're the first one loaded and no position has been defined const appName = (options.app||"default")+":"+options.index; // load the currently showing clock_infos - let settings = exports.loadSettings() + let settings = exports.loadSettings(); if (settings.apps[appName]) { let a = settings.apps[appName].a|0; let b = settings.apps[appName].b|0; @@ -259,6 +266,10 @@ exports.addInteractive = function(menu, options) { //can happen for dynamic ones (alarms, events) //in the worst case we come back to 0 } while(menu[options.menuA].items.length==0); + // When we change, ensure we don't display the same thing as another clockinfo if we can avoid it + while ((options.menuB < menu[options.menuA].items.length) && + exports.clockInfos.some(m => (m!=options) && m.menuA==options.menuA && m.menuB==options.menuB)) + options.menuB++; } if (oldMenuItem) { menuHideItem(oldMenuItem); @@ -319,6 +330,7 @@ exports.addInteractive = function(menu, options) { delete Bangle.CLKINFO_FOCUS; menuHideItem(menu[options.menuA].items[options.menuB]); exports.loadCount--; + delete exports.clockInfos[options.index]; }; options.redraw = function() { drawItem(menu[options.menuA].items[options.menuB]); @@ -339,8 +351,7 @@ exports.addInteractive = function(menu, options) { menuShowItem(menu[options.menuA].items[options.menuB]); return true; - } - + }; delete settings; // don't keep settings in RAM - save space return options; }; diff --git a/apps/clock_info/metadata.json b/apps/clock_info/metadata.json index a45741253..ef9a3effa 100644 --- a/apps/clock_info/metadata.json +++ b/apps/clock_info/metadata.json @@ -1,7 +1,7 @@ { "id": "clock_info", "name": "Clock Info Module", "shortName": "Clock Info", - "version":"0.05", + "version":"0.06", "description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)", "icon": "app.png", "type": "module", diff --git a/apps/compass/ChangeLog b/apps/compass/ChangeLog index cb1c6d463..1e7360018 100644 --- a/apps/compass/ChangeLog +++ b/apps/compass/ChangeLog @@ -6,3 +6,4 @@ 0.06: Add button for force compass calibration 0.07: Use 360-heading to output the correct heading value (fix #1866) 0.08: Added adjustment for Bangle.js magnetometer heading fix +0.09: use falling edge of button to reset compass (allows exit without compass reset) \ No newline at end of file diff --git a/apps/compass/compass.js b/apps/compass/compass.js index 9a7aec2fc..b7c8ebb71 100644 --- a/apps/compass/compass.js +++ b/apps/compass/compass.js @@ -68,7 +68,7 @@ g.clear(1); g.setFont("6x8").setFontAlign(0,0,3).drawString(/*LANG*/"RESET", g.getWidth()-5, g.getHeight()/2); setWatch(function() { Bangle.resetCompass(); -}, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat:true}); +}, (process.env.HWVERSION==2) ? BTN1 : BTN2, {repeat:true, edge:"falling"}); Bangle.loadWidgets(); Bangle.drawWidgets(); diff --git a/apps/compass/metadata.json b/apps/compass/metadata.json index 1a614e1f8..9cc03d6c8 100644 --- a/apps/compass/metadata.json +++ b/apps/compass/metadata.json @@ -1,7 +1,7 @@ { "id": "compass", "name": "Compass", - "version": "0.08", + "version": "0.09", "description": "Simple compass that points North", "icon": "compass.png", "screenshots": [{"url":"screenshot_compass.png"}], diff --git a/apps/contourclock/metadata.json b/apps/contourclock/metadata.json index d59999c04..ca5ee114f 100644 --- a/apps/contourclock/metadata.json +++ b/apps/contourclock/metadata.json @@ -15,5 +15,6 @@ {"name":"contourclock.settings.js","url":"contourclock.settings.js"}, {"name":"contourclock","url":"lib.js"}, {"name":"contourclock.img","url":"app-icon.js","evaluate":true} - ] + ], + "data": [{"name":"contourclock.json"}] } diff --git a/apps/cprassist/metadata.json b/apps/cprassist/metadata.json index d832e98c5..94ba71d1b 100644 --- a/apps/cprassist/metadata.json +++ b/apps/cprassist/metadata.json @@ -13,5 +13,6 @@ {"name":"cprassist.app.js","url":"cprassist.js"}, {"name":"cprassist.img","url":"cprassist-icon.js","evaluate":true}, {"name":"cprassist.settings.js","url":"settings.js"} - ] + ], + "data":[{"name":"cprassist.settings.json"}] } diff --git a/apps/cscsensor/metadata.json b/apps/cscsensor/metadata.json index 87eb5d12f..d7c3add53 100644 --- a/apps/cscsensor/metadata.json +++ b/apps/cscsensor/metadata.json @@ -12,5 +12,8 @@ {"name":"cscsensor.app.js","url":"cscsensor.app.js"}, {"name":"cscsensor.settings.js","url":"settings.js"}, {"name":"cscsensor.img","url":"cscsensor-icon.js","evaluate":true} + ], + "data": [ + {"name":"cscsensor.json"} ] } diff --git a/apps/cycling/metadata.json b/apps/cycling/metadata.json index cb4260bb2..caf93eda3 100644 --- a/apps/cycling/metadata.json +++ b/apps/cycling/metadata.json @@ -13,5 +13,8 @@ {"name":"cycling.settings.js","url":"settings.js"}, {"name":"blecsc","url":"blecsc.js"}, {"name":"cycling.img","url":"cycling.icon.js","evaluate": true} + ], + "data": [ + {"name":"cycling.json"} ] } diff --git a/apps/dragboard/metadata.json b/apps/dragboard/metadata.json index 964ace3a7..58de5153c 100644 --- a/apps/dragboard/metadata.json +++ b/apps/dragboard/metadata.json @@ -6,10 +6,13 @@ "type":"textinput", "tags": "keyboard", "supports" : ["BANGLEJS2"], - "screenshots": [{"url":"screenshot.png"}], + "screenshots": [{"url":"screenshot.png"}], "readme": "README.md", "storage": [ {"name":"textinput","url":"lib.js"}, {"name":"dragboard.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"dragboard.json"} ] } diff --git a/apps/draguboard/metadata.json b/apps/draguboard/metadata.json index 926e36807..dc9b06254 100644 --- a/apps/draguboard/metadata.json +++ b/apps/draguboard/metadata.json @@ -6,10 +6,13 @@ "type":"textinput", "tags": "keyboard", "supports" : ["BANGLEJS2"], - "screenshots": [{"url":"screenshot.png"}], + "screenshots": [{"url":"screenshot.png"}], "readme": "README.md", "storage": [ {"name":"textinput","url":"lib.js"}, {"name":"draguboard.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"draguboard.json"} ] } diff --git a/apps/f9lander/metadata.json b/apps/f9lander/metadata.json index e53805ee0..5a3887c9e 100644 --- a/apps/f9lander/metadata.json +++ b/apps/f9lander/metadata.json @@ -12,5 +12,6 @@ {"name":"f9lander.app.js","url":"app.js"}, {"name":"f9lander.img","url":"app-icon.js","evaluate":true}, {"name":"f9lander.settings.js", "url":"settings.js"} - ] + ], + "data":[{"name":"f9settings.json"}] } diff --git a/apps/fastload/ChangeLog b/apps/fastload/ChangeLog index 53e3c2591..4e68ab2c7 100644 --- a/apps/fastload/ChangeLog +++ b/apps/fastload/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Allow redirection of loads to the launcher 0.03: Allow hiding the fastloading info screen +0.04: (WIP) Allow use of app history when going back (`load()` or `Bangle.load()` calls without specified app). diff --git a/apps/fastload/README.md b/apps/fastload/README.md index a1feedcf8..be4175f55 100644 --- a/apps/fastload/README.md +++ b/apps/fastload/README.md @@ -8,9 +8,16 @@ This allows fast loading of all apps with two conditions: ## Settings +* Activate app history and navigate back through recent apps instead of immediately loading the clock face +* If Quick Launch is installed it can be excluded from app history * Allows to redirect all loads usually loading the clock to the launcher instead * The "Fastloading..." screen can be switched off +## App history + +* Long press of hardware button clears the app history and loads the clock face +* Installing the 'Fast Reset' app allows doing fastloads directly to the clock face by pressing the hardware button for one second. Useful if there are many apps in the history and the user want to access the clock quickly. + ## Technical infos This is still experimental but it uses the same mechanism as `.bootcde` does. @@ -19,3 +26,6 @@ It checks the app to be loaded for widget use and stores the result of that and # Creator [halemmerich](https://github.com/halemmerich) + +# Contributors +[thyttan](https://github.com/thyttan) diff --git a/apps/fastload/boot.js b/apps/fastload/boot.js index c9271abbf..c7fc2fd86 100644 --- a/apps/fastload/boot.js +++ b/apps/fastload/boot.js @@ -1,5 +1,6 @@ { -const SETTINGS = require("Storage").readJSON("fastload.json") || {}; +const s = require("Storage"); +const SETTINGS = s.readJSON("fastload.json") || {}; let loadingScreen = function(){ g.reset(); @@ -16,26 +17,26 @@ let loadingScreen = function(){ g.flip(true); }; -let cache = require("Storage").readJSON("fastload.cache") || {}; +let cache = s.readJSON("fastload.cache") || {}; let checkApp = function(n){ // no widgets, no problem if (!global.WIDGETS) return true; - let app = require("Storage").read(n); + let app = s.read(n); if (cache[n] && E.CRC32(app) == cache[n].crc) - return cache[n].fast + return cache[n].fast; cache[n] = {}; cache[n].fast = app.includes("Bangle.loadWidgets"); cache[n].crc = E.CRC32(app); - require("Storage").writeJSON("fastload.cache", cache); + s.writeJSON("fastload.cache", cache); return cache[n].fast; -} +}; global._load = load; let slowload = function(n){ global._load(n); -} +}; let fastload = function(n){ if (!n || checkApp(n)){ @@ -50,17 +51,40 @@ let fastload = function(n){ }; global.load = fastload; +let appHistory, resetHistory, recordHistory; +if (SETTINGS.useAppHistory){ + appHistory = s.readJSON("fastload.history.json",true)||[]; + resetHistory = ()=>{appHistory=[];s.writeJSON("fastload.history.json",appHistory);}; + recordHistory = ()=>{s.writeJSON("fastload.history.json",appHistory);}; +} + Bangle.load = (o => (name) => { if (Bangle.uiRemove && !SETTINGS.hideLoading) loadingScreen(); + if (SETTINGS.useAppHistory){ + if (name && name!=".bootcde" && !(name=="quicklaunch.app.js" && SETTINGS.disregardQuicklaunch)) { + // store the name of the app to launch + appHistory.push(name); + } else if (name==".bootcde") { // when Bangle.showClock is called + resetHistory(); + } else if (name=="quicklaunch.app.js" && SETTINGS.disregardQuicklaunch) { + // do nothing with history + } else { + // go back in history + appHistory.pop(); + name = appHistory[appHistory.length-1]; + } + } if (SETTINGS.autoloadLauncher && !name){ let orig = Bangle.load; Bangle.load = (n)=>{ Bangle.load = orig; fastload(n); - } + }; Bangle.showLauncher(); Bangle.load = orig; - } else + } else o(name); })(Bangle.load); + +if (SETTINGS.useAppHistory) E.on('kill', ()=>{if (!BTN.read()) recordHistory(); else resetHistory();}); // Usually record history, but reset it if long press of HW button was used. } diff --git a/apps/fastload/metadata.json b/apps/fastload/metadata.json index 15adcb7e3..954a7d8b4 100644 --- a/apps/fastload/metadata.json +++ b/apps/fastload/metadata.json @@ -1,7 +1,7 @@ { "id": "fastload", "name": "Fastload Utils", "shortName" : "Fastload Utils", - "version": "0.03", + "version": "0.04", "icon": "icon.png", "description": "Enable experimental fastloading for more apps", "type":"bootloader", diff --git a/apps/fastload/settings.js b/apps/fastload/settings.js index 4904e057e..66c990df1 100644 --- a/apps/fastload/settings.js +++ b/apps/fastload/settings.js @@ -1,7 +1,8 @@ (function(back) { var FILE="fastload.json"; var settings; - + var isQuicklaunchPresent = !!require('Storage').read("quicklaunch.app.js", 0, 1); + function writeSettings(key, value) { var s = require('Storage').readJSON(FILE, true) || {}; s[key] = value; @@ -12,25 +13,52 @@ function readSettings(){ settings = require('Storage').readJSON(FILE, true) || {}; } - + readSettings(); function buildMainMenu(){ - var mainmenu = { - '': { 'title': 'Fastload', back: back }, - 'Force load to launcher': { + var mainmenu = {}; + + mainmenu[''] = { 'title': 'Fastload', back: back }; + + mainmenu['Activate app history'] = { + value: !!settings.useAppHistory, + onchange: v => { + writeSettings("useAppHistory",v); + if (v && settings.autoloadLauncher) { + writeSettings("autoloadLauncher",!v); // Don't use app history and load to launcher together. + setTimeout(()=>E.showMenu(buildMainMenu()), 0); // Update the menu so it can be seen if a value was automatically set to false (app history vs load launcher). + } + } + }; + + if (isQuicklaunchPresent) { + mainmenu['Exclude Quick Launch from history'] = { + value: !!settings.disregardQuicklaunch, + onchange: v => { + writeSettings("disregardQuicklaunch",v); + } + }; + } + + mainmenu['Force load to launcher'] = { value: !!settings.autoloadLauncher, onchange: v => { writeSettings("autoloadLauncher",v); + if (v && settings.useAppHistory) { + writeSettings("useAppHistory",!v); + setTimeout(()=>E.showMenu(buildMainMenu()), 0); // Update the menu so it can be seen if a value was automatically set to false (app history vs load launcher). + } // Don't use app history and load to launcher together. } - }, - 'Hide "Fastloading..."': { + }; + + mainmenu['Hide "Fastloading..."'] = { value: !!settings.hideLoading, onchange: v => { writeSettings("hideLoading",v); } - } - }; + }; + return mainmenu; } diff --git a/apps/fastreset/ChangeLog b/apps/fastreset/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/fastreset/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/fastreset/README.md b/apps/fastreset/README.md new file mode 100644 index 000000000..381d80cf5 --- /dev/null +++ b/apps/fastreset/README.md @@ -0,0 +1,32 @@ +# Fast Reset + +Reset the watch by holding the hardware button for half a second. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator. + +Fast Reset was developed with the app history feature of 'Fastload Utils' in mind. If many apps are in the history stack, the user may want a fast way to exit directly to the clock face without using the firmwares reset function. + +## Usage + +Just install and it will run as boot code. + +## Features + +If 'Fastload Utils' is installed fastloading will be used when possible. Otherwise a standard `load(.bootcde)` is used. + +If the hardware button is held for longer the standard reset functionality of the firmware is executed as well (total 1.5 seconds). And eventually the watchdog will be kicked. + +## Controls + +Hold the hardware button for half a second to feel the buzz, loading the clock face. + +## Requests + +Mention @[thyttan](https://github.com/thyttan) in an issue to the official [BangleApps repository](https://github.com/espruino/BangleApps/issues) for feature requests and bug reports. + +## Acknowledgements + +Rewind icon by Icons8 + +## Creator + +[thyttan](https://github.com/thyttan) + diff --git a/apps/fastreset/app.png b/apps/fastreset/app.png new file mode 100644 index 000000000..79e0f310e Binary files /dev/null and b/apps/fastreset/app.png differ diff --git a/apps/fastreset/boot.js b/apps/fastreset/boot.js new file mode 100644 index 000000000..681a5ddb7 --- /dev/null +++ b/apps/fastreset/boot.js @@ -0,0 +1,5 @@ +{let buzzTimeout; +setWatch((e)=>{ + if (e.state) buzzTimeout = setTimeout(()=>{Bangle.buzz(80,0.40);Bangle.showClock();}, 500); + if (!e.state && buzzTimeout) clearTimeout(buzzTimeout);}, +BTN,{repeat:true, edge:'both' });} diff --git a/apps/fastreset/metadata.json b/apps/fastreset/metadata.json new file mode 100644 index 000000000..098e0eeb1 --- /dev/null +++ b/apps/fastreset/metadata.json @@ -0,0 +1,14 @@ +{ "id": "fastreset", + "name": "Fast Reset", + "shortName":"Fast Reset", + "version":"0.01", + "description": "Reset the watch by holding the hardware button for half a second. If 'Fastload Utils' is installed this will typically be done with fastloading. A buzz acts as indicator.", + "icon": "app.png", + "type": "bootloader", + "tags": "system", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"fastreset.boot.js","url":"boot.js"} + ] +} diff --git a/apps/game1024/metadata.json b/apps/game1024/metadata.json index f3b72aad3..45d6e3f61 100644 --- a/apps/game1024/metadata.json +++ b/apps/game1024/metadata.json @@ -14,5 +14,6 @@ {"name":"game1024.app.js","url":"app.js"}, {"name":"game1024.settings.js","url":"settings.js"}, {"name":"game1024.img","url":"app-icon.js","evaluate":true} - ] + ], + "data":[{"name":"game1024.settings.json"}] } diff --git a/apps/gassist/ChangeLog b/apps/gassist/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/gassist/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/gassist/app.js b/apps/gassist/app.js new file mode 100644 index 000000000..69e8001e3 --- /dev/null +++ b/apps/gassist/app.js @@ -0,0 +1,11 @@ +Bluetooth.println(""); +Bluetooth.println(JSON.stringify({ + t:"intent", + target:"activity", + action:"android.intent.action.VOICE_COMMAND", + flags:["FLAG_ACTIVITY_NEW_TASK"] +})); + +setTimeout(function() { + Bangle.showClock(); +}, 0); diff --git a/apps/gassist/app.png b/apps/gassist/app.png new file mode 100644 index 000000000..8c190d344 Binary files /dev/null and b/apps/gassist/app.png differ diff --git a/apps/gassist/boot.js b/apps/gassist/boot.js new file mode 100644 index 000000000..eb2155796 --- /dev/null +++ b/apps/gassist/boot.js @@ -0,0 +1,21 @@ +// load settings +var settings = Object.assign({ + enableTap: true +}, require("Storage").readJSON("gassist.json", true) || {}); + +if (settings.enableTap) { + Bangle.on("tap", function(e) { + if (e.dir=="front" && e.double) { + Bluetooth.println(""); + Bluetooth.println(JSON.stringify({ + t:"intent", + target:"activity", + action:"android.intent.action.VOICE_COMMAND", + flags:["FLAG_ACTIVITY_NEW_TASK"] + })); + } + }); +} + +// clear variable +settings = undefined; \ No newline at end of file diff --git a/apps/gassist/icon.js b/apps/gassist/icon.js new file mode 100644 index 000000000..9e84990e3 --- /dev/null +++ b/apps/gassist/icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+AH4A/AH4ALiAAFFtoxmFpQxjFxwwfFyAwdFyQwcF9wuUGDQvuFywwYF/4vUnAABF9YuCGBAv/F/6/PGC4bE3QACG5YvdFoYxSLzAvuFw4wjLxbCidhAvVGB4UFF7QxMCZAuaGJIRKF7oATFtoA/AEPMAAQttGNQuHGE4vuFxIwlF/4v/d/4vwGBAumGIwtpAH4A/AEIA==")) \ No newline at end of file diff --git a/apps/gassist/metadata.json b/apps/gassist/metadata.json new file mode 100644 index 000000000..995c44adb --- /dev/null +++ b/apps/gassist/metadata.json @@ -0,0 +1,18 @@ +{ + "id": "gassist", + "name": "Google Assist", + "version": "0.01", + "description": "A simple way to initiate Google Assistant on Android. Intents must be enabled in Gadgetbridge.", + "icon": "app.png", + "type": "app", + "tags": "tool, voice, tasker", + "supports": ["BANGLEJS","BANGLEJS2"], + "allow_emulator": false, + "storage": [ + {"name":"gassist.boot.js","url":"boot.js"}, + {"name":"gassist.app.js","url":"app.js"}, + {"name":"gassist.settings.js","url":"settings.js"}, + {"name":"gassist.img","url":"icon.js","evaluate":true} + ], + "data": [{"name":"gassist.json"}] +} \ No newline at end of file diff --git a/apps/gassist/settings.js b/apps/gassist/settings.js new file mode 100644 index 000000000..987c3fdfc --- /dev/null +++ b/apps/gassist/settings.js @@ -0,0 +1,33 @@ +(function (back) { + let storage = require('Storage'); + let file = "gassist.json"; + + // Load and set default settings + let appSettings = Object.assign({ + enableTap : true + }, storage.readJSON(file, true) || {}); + + // Save settings to storage + function writeSettings() { + storage.writeJSON(file, appSettings); + } + + function showMenu() { + E.showMenu({ + "": { + "title": "Google Assist" + }, + "< Back": () => back(), + 'Front Tap:': { + value: (appSettings.enableTap === true), + format: v => v ? "On" : "Off", + onchange: v => { + appSettings.enableTap = v; + writeSettings(); + } + }, + }); + } + // Initially show the menu + showMenu(); +}); diff --git a/apps/geissclk/ChangeLog b/apps/geissclk/ChangeLog index cd46173f7..680d820ae 100644 --- a/apps/geissclk/ChangeLog +++ b/apps/geissclk/ChangeLog @@ -2,3 +2,4 @@ 0.02: BTN2->launcher, use smaller text to allow "20:00" to fit on screen 0.03: Changed setWatch to Bangle.setUI 0.04: Tell clock widgets to hide. +0.05: Making geissclock work on Bangle.js 2 (but only animate when unlocked!) \ No newline at end of file diff --git a/apps/geissclk/clock.js b/apps/geissclk/clock.js index 5401fb142..2f1428d24 100644 --- a/apps/geissclk/clock.js +++ b/apps/geissclk/clock.js @@ -1,4 +1,7 @@ var W = 79, H = 64; +// if screen is always on, only animate when unlocked +var isScreenAlwaysOn = process.env.BOARD=="BANGLEJS2"; + /*var compiled = E.compiledC(` // void transl(int, int, int ) int transl(unsigned char *map, unsigned char *imgfrom, unsigned char *imgto) { @@ -46,6 +49,7 @@ var map = new Uint8Array(W*H); var pal = new Uint16Array(256); var PALETTES = 3; var MAPS = 6; +var animInterval; // If we're missing any maps, compute them! (function() { @@ -65,6 +69,8 @@ function randomPalette() { var n = (0|Math.random()*200000) % PALETTES; var p = new Uint8Array(pal.buffer); p.set(require("Storage").readArrayBuffer("geissclk."+n+".pal")); + if (!g.theme.dark) // if not dark, invert colors + E.mapInPlace(pal,pal,x=>x^0xFFFF); } function randomMap() { @@ -93,7 +99,7 @@ var im = { }; var lastSeconds = -1; -function iterate() { "ram" +function iterate(clearBuf) { "ram" var d = new Date(); var time = require("locale").time(d,1); var seconds = d.getSeconds().toString().padStart(2,0); @@ -108,27 +114,59 @@ function iterate() { "ram" gfx.buffer = dataa.buffer; } var x,y,n,t = getTime()/10; - var amt = 100*Bangle.getAccel().diff; - for (var i=0;i> 16; @@ -55,17 +56,17 @@ function convert24to16(input) RGB565 = RGB565 | b; return "0x"+RGB565.toString(16); -} +}; -var color1C = convert24to16(color1);//Converting colors to the correct format. -var color2C = convert24to16(color2); -var color3C = convert24to16(color3); +let color1C = convert24to16(color1);//Converting colors to the correct format. +let color2C = convert24to16(color2); +let color3C = convert24to16(color3); /* * Requirements and globals */ -var colorPalette = new Uint16Array([//Used to change the color of the image if the user selects a color that is diffrent than the default. +let colorPalette = new Uint16Array([//Used to change the color of the image if the user selects a color that is diffrent than the default. 0x0000, // not used color2C, // second color3C, // third @@ -84,73 +85,73 @@ var colorPalette = new Uint16Array([//Used to change the color of the image if t 0x0000 // not used ],0,1); -var bgLeftFullscreen = { +let bgLeftFullscreen = { width : 27, height : 176, bpp : 3, transparent : 0, buffer : require("heatshrink").decompress((atob("/4AB+VJkmSAQV///+BAtJn//5IIFkmf/4IGyVP/gIGpMnF41PHIImGF4ImHJoQmGJoIdK8hNHNY47C/JNGBIJZGyYJBQA5GCKH5Q/KAQAoUP7y/KH5QGDoQAy0hGF34JB6RGFr4JB9JkFl4JB+gdFy4JB/QdFpYJB/odFkqrCS4xGCWoyDCKH5Q1GShlJChQLCCg5TCHw5TMAD35FAoIIkgJB8hGGv/8Mg8/+QIFp4cB5IRGBIIvI/4IFybyCF4wTCDp5NBHZZiGz4JBLJKAGk4JBO4xQ/KGQA8UP7y/KH5QnAHih/eX5Q/GQ4JCGRJlKCgxTDBAwgCCg5TCHwxTCNA4"))), palette: colorPalette }; -var bgLeftNotFullscreen = { +let bgLeftNotFullscreen = { width : 27, height : 152, bpp : 3, transparent : 0, buffer : require("heatshrink").decompress((atob("/4AB+VJkmSAQV///+BAtJn//5IIFkmf/4IGyVP/gIGpMnF41PHIImGF4ImHJoQmGJoIdK8hNHNY47C/JNGBIJZGyYJBQA5GCKH5Q/KAQAy0hGF34JB6RGFr4JB9JkFl4JB+gdFy4JB/QdFpYJB/odFkqrCS4xGCWoyhCKH5Q1GShlJChQLCCg5TCHw5TMAD35FAoIIkgJB8hGGv/8Mg8/+QIFp4cB5IRGBIIvI/4IFybyCF4wTCDp5NBHZZiGz4JBLJKAGk4JBO4xQ/KGQA8UP7y/KH5QnAHih/eX5Q/GQ4JCGRJlKCgxTDBAwgCCg5TCHwxTCNA4A=="))), palette: colorPalette }; -var bgRightFullscreen = { +let bgRightFullscreen = { width : 27, height : 176, bpp : 3, transparent : 0, buffer : require("heatshrink").decompress((atob("yVJkgCCyf/AAPJBAYCBk4JB8gUFyVP//yBAoCB//5BAwUCAAIUHAAIgGChopGv5TIn5TIz4yLKYxxC/iGI/xxGKH5Q/agwAnUP7y/KH4yGeVYAJ0hGF34JB6RGFr4JB9JkFl4JB+gdFy4JB/QdFpYJB/odFkp4CS4xGCWoyhCKH5QuDoxQCDpI7GDoJZGHYIRGLIQvGO4QvGMQRNJADv+GIqTC/5PGz4JBJ41JBIPJCg2TD4QLGn4JB/gUaHwRTGHwRTHBIRTGNAQyJ8gyI+QdFp4JB/IdFk5lLKH5QvAHih/eX5Q/KE4A8UP7y/KH5QGDpg7HJoxZCCIx3CJowmCF4yACJoyJC/4A=="))), palette: colorPalette }; -var bgRightNotFullscreen = { +let bgRightNotFullscreen = { width : 27, height : 152, bpp : 3, transparent : 0, buffer : require("heatshrink").decompress((atob("yVJkgCCyf/AAPJBAYCBk4JB8gUFyVP//yBAoCB//5BAwUCAAIUHAAIgGChopGv5TIn5TIz4yLKYxxC/iGI/xxGKH5Q/agwAx0hGF34JB6RGFr4JB9JkFl4JB+gdFy4JB/QdFpYJB/odFkqrCS4xGCWoyhCKH5QuDoxQCDpI7GDoJZGHYIRGLIQvGO4QvGMQRNJADv+GIqTC/5PGz4JBJ41JBIPJCg2TD4QLGn4JB/gUaHwRTGHwRTHBIRTGNAQyJ8gyI+QdFp4JB/IdFk5lLKH5QvAHih/eX5Q/KE4A8UP7y/KH5QGDpg7HJoxZCCIx3CJowmCF4yACJoyJC/4A="))), palette: colorPalette }; -var bgLeft = settings.fullscreen ? bgLeftFullscreen : bgLeftNotFullscreen; -var bgRight= settings.fullscreen ? bgRightFullscreen : bgRightNotFullscreen; +let bgLeft = settings.fullscreen ? bgLeftFullscreen : bgLeftNotFullscreen; +let bgRight= settings.fullscreen ? bgRightFullscreen : bgRightNotFullscreen; -var iconEarth = { +let iconEarth = { width : 50, height : 50, bpp : 3, buffer : require("heatshrink").decompress(atob("AFtx48ECBsDwU5k/yhARLjgjBjlzAQMQEZcIkOP/fn31IEZgCBnlz58cEpM4geugEgwU/8+WNZJHDuHHvgmBCQ8goEOnVgJoMnyV58mACItHI4X8uAFBuVHnnz4BuGxk4////Egz3IkmWvPgNw8f/prB//BghTC+AjE7848eMjNnzySBwUJkmf/BuGuPDAQIjBiPHhhTCSQnjMo0ITANJn44Dg8MuFBggCCiFBcAJ0Bv5xEh+ITo2OhHkyf/OIQdBWwVHhgjBNwUE+fP/5EEgePMoYLBhMgyVJk/+BQQdC688I4XxOIc8v//NAvr+QEBj/5NwKVBy1/QYUciPBhk1EAJrC+KeC489QYaMBgU/8BNB9+ChEjz1Jkn/QYMBDQIgCcYTCCiP/nlzJQmenMAgV4//uy/9wRaB/1J8iVCcAfHjt9TYYICnhKCgRKBw159/v//r927OIeeoASBDQccvv3791KYVDBYPLJQeCnPnz//AAP6ocEjEkXgMgJQtz79fLAP8KYkccAcJ8Gf/f/xu/cAMQ4eP5MlyQRCMolx40YsOGBAPfnnzU4KVDpKMBvz8Dh0/8me7IICgkxJQXPIgZTD58sEgcJk+eNoONnFBhk4/5uB/pcDg5KD+4mEv4CBXISVDhEn31/8/+mH7x//JQK5CAAMB4JBCnnxJQf/+fJEgkAa4L+CAQOOjMn/1bXIRxDJQXx58f//Hhlz/88EgsChMgz/Zs/+nfkyV/8huDOI6SD498NwoACi1Z8+S/Plz17/+QCI7jC+ZxBmfPnojIAAMDcYWSp//2wRJEwq2GABECjMgNYwAmA=")) }; -var iconSaturn = { +let iconSaturn = { width : 50, height : 50, bpp : 3, transparent : 1, buffer : require("heatshrink").decompress(atob("AH4A/AEkQuPHCJ0ChEAwARNjAjBjgjOhs06Q2OEYVx4ARMhEggUMkANIDoIgBoEEgEBNxJEC6ZrBAAMwNxAjDNYcHNxIjB7dtEwIHBwRoKj158+cuPEjlwCRAjC23bpu0wRNDAAsHEYWeEwaSJ6YjCAQUNSRQjEzxQBWZMNEYlsmg2JWAIjCz95SoJuJggjDtuw6dMG5JKCz998wFBJRVNEYW0yaVBJRNhJQN9+4pCzhKJmBKC4YpB/fINxIgCzFxSoQ3J4ENm3CAQPb98wbpEcAQMYWwKYBNxMDXgc2/fv3g2IEAOAgAjBjy5CEhEMfYICBgfPnjdLjj+CgMHiC3JknDhhoINw4jCAB0IJQIANR4QjPAH4A/AFA")) }; -var iconMoon = { +let iconMoon = { width : 50, height : 50, bpp : 3, transparent : 1, buffer : require("heatshrink").decompress(atob("AH4AQjlx44CCCZsg8eOkHDwAQKEYgmPhEgEQM48AOIgMHEYoCB4ATI8UAmH/x04JoRuJsImHuBKLn37EwZuIgEQOI8cEpXj/yYBhE8+YNGgkYoJxITBUPnAaC///nC+FjBuIOJZEB8YeCh/8AoYACoMEEAnEjhQDPQJKJ/DCDAoi5DoLdHAoMQgLjFWYPOnngh02IwXzwDjEgPGEYS8BI4MBYoSVG4fP/nghkAgZrDkngJQqSG4gvBg4sBQgkImHihEAWwP8ZBMBEYl5/+cSoVAGQIUFh04weJn///0gj/OEw5KEz45BzhuCTYQAEgePB4IACAoJuBnAQEa4XHjxKB//xFgWHJQsCRgMDEonipwjENwUBDQNx8+evvn/hTDLw3igE+EgZxB8UOXIvEJQUfEYOfv53DEQkgga5BJQvzx84cAj+CDoNh8/eEYJKDuCSEcocnEon+/7xEgFBIIcfB4Mf/IICXI2DgDdBAAn758gCIq5Dv4zBvJuIOIfjEgvP/ARHgwdCB4P3AoTdFAAk4EYk8SQgAFTALaDSQwAGh08//vnDmBABYmEEZYAzA==")) }; -var iconMars = { +let iconMars = { width : 50, height : 50, bpp : 3, transparent : 1, buffer : require("heatshrink").decompress(atob("AH4ATjlwCJ+Dh0wwAQMg0cuPHjFhCZkDps0yVJkmQCBMEjFx42atOmzQmLhMkEYQCCCREQoOGEYmmzB0IEY4CBkARGoJKBEYQCEzgSGkGSpAjDyYCCphuGiFhJQgCD8ASFgRHGAQKbB6BuHJRGeOIsINxEk6dNmARDgMEjQjHAQPnVQojIyZKB6YSDNwK5FAQt54BuDXJIjBEwK5EgxKKXgq5BJRdgXIojJAQJKMcAM0EwM2JUApDoCVFExa7FkGCgAmIkAREEwUEjAmHCIgABhEggQmFpACBCIojBEwRQCzVhwkQU4YADgQmBwQCCI4IFBCAojFAQojGJQQjDAQgRGEZICBEo4gFyUIkilFJQUYEAZrBAQMYNw5KDSQSbCNwwABgOGEwgCBsPACQ5xGwdNnARJcAVh48evvnCJK8Chs+/fv33gCRcB48cuPHCBYA/ADAA==")) }; -var iconSatellite = { +let iconSatellite = { width : 50, height : 50, bpp : 3, transparent : 2, buffer : require("heatshrink").decompress(atob("pMkyQC/ATGXhIRPyNl0gmPjlwCJ9ly1aCJ1c+fHJR1Hy1ZJR1I+fPnlx6QRLpe+/JKBr5KMuYjBJQMdCJce/fvJQW0CJUlEYQCBSpvvJQbXJjl0NwnzNxGQwEOnHhgF78+WqQyIrFx48cAQXz4ShJgAABh0+8cP//9LJEhg4jDuP3//0LhGQgYlBgeAn///5cIy8MuAmDCIP/9I4HkmCEYMOgHfCQWkCI0cuBuDgF/CIP+CI1Ny1IkeAgHANwIAB/QRFrj7BhkxEwQRC/4RFpbXDgSVBg4RCSorXDI4MJAQMfCIP8cwImDn37fwN58+kwHgLgSVFub7CI4NyBAJKDLgkuEYX78+evKtCLg0jEYRKC58JMoRcFkwjDJQTFDl65EkojEAQMdcwn/+gFC3YjEJQLXEpYRDWwQmEdI6SHAQO0CJUkx4jDF4gCIJQgRMXIjCEARIjCCJ2XEYPKCJqJBJQIROcAUpCJ0kybaDARtdCKAC2kAA=")) }; -var iconCharging = { +let iconCharging = { width : 50, height : 50, bpp : 3, transparent : 5, buffer : require("heatshrink").decompress(atob("23btugAwUBtoICARG0h048eODQYCJ6P/AAUCCJfbo4SDxYRLtEcuPHjlwgoRJ7RnIloUHoYjDAQfAExEAwUIkACEkSAIEYwCBhZKH6EIJI0CJRFHEY0BJRWBSgf//0AJRYSE4BKLj4SE8BKLv4RD/hK/JS2AXY0gXwRKG4cMmACCJQMAg8csEFJQsBAwfasEAm379u0gFbcBfHzgFBz1xMQZKBjY/D0E2+BOChu26yVEEYdww+cgAFCg+cgIfB6RKF4HbgEIkGChEAthfCJQ0eEAIjBBAMxk6GCJQtgtyVBwRKBAQMbHAJKGXIIFCgACBhl54qVG2E+EAJKBJoWAm0WJQ6SCXgdxFgMLJQvYjeAEAUwFIUitEtJQ14NwUHgEwKYZKGwOwNYX7XgWCg3CJQ5rB4MevPnAoPDJRJrCgEG/ECAoNsJRUwoEesIIBiJKI3CVDti/CJRKVDiJHBSo0YsOGjED8AjBcAcIgdhcAXAPIUAcAYIBcA4dBAQUG8BrBgBuCgOwcBEeXIK2BBAIFBgRqBGoYAChq8CcYUE4FbUYOACQsHzgjDgwFBCIImBAQsDtwYD7cAloRI22B86YBw5QBgoRJ7dAgYEDCJaeBJoMcsARMAQNoJIIRE6A")) }; -var iconWarning = { +let iconWarning = { width : 50, height : 50, bpp : 3, transparent : 1, buffer : require("heatshrink").decompress(atob("kmSpIC/AWMyoQIFsmECJFJhMmA4QXByVICIwODAQ4RRFIQGD5JVLkIGDzJqMyAGDph8MiRKGyApEAoZKFyYIDQwMkSQNkQZABBhIIOOJRuEL5gRIAUKACVQMhmUSNYNDQYJTBBwYFByGTkOE5FJWYNMknCAQKYCiaSCpmGochDoSYBhMwTAZrChILBhmEzKPBF4ImBTAREBDoMmEwJVDoYjBycJFgWEJQRuLJQ1kmQCCjJlCBYbjCagaDBwyDBmBuBF4TjJAUQKINBChCDQxZBcZIIQF4NIgEAgKSDiQmEVQKMBoARBAAMCSQLLBVoxqKL4gaCChVCNwoRKOIo4CJIgABBoSMHpIRFgDdJOIJUBCAUJRgJuEAQb+DIIgRIAX4C/ASOQA")) @@ -172,26 +173,26 @@ Graphics.prototype.setFontAntonioLarge = function(scale) { /* * Draw watch face */ -var drawTimeout; -function queueDraw() { +let drawTimeout; +let queueDraw = function() { // Faster updates during alarm to ensure that it is // shown correctly... - var timeout = isAlarmEnabled() ? 10000 : 60000; + let timeout = isAlarmEnabled() ? 10000 : 60000; if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = setTimeout(function() { drawTimeout = undefined; draw(); }, timeout - (Date.now() % timeout)); -} +}; /** * This function plots a data row in LCARS style. * Note: It can be called async and therefore, the text alignment and * font is set each time the function is called. */ -function printRow(text, value, y, c){ +let printRow = function(text, value, y, c){ g.setFontAntonioMedium(); g.setFontAlign(-1,-1,0); @@ -210,23 +211,23 @@ function printRow(text, value, y, c){ g.setColor(c); g.setFontAlign(1,-1,0); g.drawString(value, 126, y); -} +}; -function drawData(key, y, c){ +let drawData = function(key, y, c){ try{ _drawData(key, y, c); } catch(ex){ // Show last error - next try hopefully works. } -} +}; -function _drawData(key, y, c){ +let _drawData = function(key, y, c){ key = key.toUpperCase() - var text = key; - var value = "ERR"; - var should_print= true; + let text = key; + let value = "ERR"; + let should_print= true; if(key == "STEPS"){ text = "STEP"; @@ -239,21 +240,24 @@ function _drawData(key, y, c){ } else if (key == "VREF"){ value = E.getAnalogVRef().toFixed(2) + "V"; + } else if (key =="BATTVOLT" ) { + text = "BATV"; + value = (E.getAnalogVRef()*analogRead(3)*4).toFixed(2) + "V"; } else if(key == "HRM"){ value = Math.round(Bangle.getHealthStatus().bpm||Bangle.getHealthStatus("last").bpm); } else if (key == "TEMP"){ - var weather = getWeather(); + let weather = getWeather(); value = weather.temp; } else if (key == "HUMIDITY"){ text = "HUM"; - var weather = getWeather(); + let weather = getWeather(); value = weather.hum; } else if (key == "WIND"){ text = "WND"; - var weather = getWeather(); + let weather = getWeather(); value = weather.wind; } else if (key == "ALTITUDE"){ @@ -277,18 +281,18 @@ function _drawData(key, y, c){ if(should_print){ printRow(text, value, y, c); } -} +}; -function drawHorizontalBgLine(color, x1, x2, y, h){ +let drawHorizontalBgLine = function(color, x1, x2, y, h){ g.setColor(color); - for(var i=0; i{ data[h.day]+=h.bpm; if (h.bpm) cnt[h.day]++; @@ -462,9 +466,9 @@ function drawPosition1(){ }); // Plot step graph - var data = new Uint16Array(32); + data = new Uint16Array(32); health.readDailySummaries(new Date(), h=>data[h.day]+=h.steps/1000); - var gridY = parseInt(Math.max.apply(Math, data)/2); + let gridY = parseInt(Math.max.apply(Math, data)/2); gridY = gridY <= 0 ? 1 : gridY; require("graph").drawBar(g, data, { axes : true, @@ -490,8 +494,8 @@ function drawPosition1(){ // Plot day } else { - var data = new Uint16Array(24); - var cnt = new Uint8Array(24); + let data = new Uint16Array(24); + let cnt = new Uint8Array(24); health.readDay(new Date(), h=>{ data[h.hr]+=h.bpm; if (h.bpm) cnt[h.hr]++; @@ -508,9 +512,9 @@ function drawPosition1(){ }); // Plot step graph - var data = new Uint16Array(24); + data = new Uint16Array(24); health.readDay(new Date(), h=>data[h.hr]+=h.steps); - var gridY = parseInt(Math.max.apply(Math, data)/1000)*1000; + let gridY = parseInt(Math.max.apply(Math, data)/1000)*1000; gridY = gridY <= 0 ? 1000 : gridY; require("graph").drawBar(g, data, { axes : true, @@ -534,9 +538,9 @@ function drawPosition1(){ g.drawString("DAY", 154, 115); } } -} +}; -function draw(){ +let draw = function(){ // Queue draw first to ensure that its called in one minute again. queueDraw(); @@ -557,14 +561,14 @@ function draw(){ } else { Bangle.drawWidgets(); } -} +}; /* * Step counter via widget */ -function getSteps() { - var steps = 0; +let getSteps = function() { + let steps = 0; try{ if (WIDGETS.wpedom !== undefined) { steps = WIDGETS.wpedom.getSteps(); @@ -578,15 +582,15 @@ function getSteps() { } return steps; -} +}; -function getWeather(){ - var weatherJson; +let getWeather = function(){ + let weatherJson; try { weatherJson = storage.readJSON('weather.json'); - var weather = weatherJson.weather; + let weather = weatherJson.weather; // Temperature weather.temp = locale.temp(weather.temp-273.15); @@ -596,7 +600,7 @@ function getWeather(){ // Wind const wind = locale.speed(weather.wind).match(/^(\D*\d*)(.*)$/); - var speedFactor = settings.speed == "kph" ? 1.0 : 1.0 / 1.60934; + let speedFactor = settings.speed == "kph" ? 1.0 : 1.0 / 1.60934; weather.wind = Math.round(wind[1] * speedFactor); return weather @@ -613,15 +617,15 @@ function getWeather(){ wdir: " ? ", wrose: " ? " }; -} +}; /* * Handle alarm */ -function isAlarmEnabled(){ +let isAlarmEnabled = function(){ try{ - var alarm = require('sched'); - var alarmObj = alarm.getAlarm(TIMER_IDX); + let alarm = require('sched'); + let alarmObj = alarm.getAlarm(TIMER_IDX); if(alarmObj===undefined || !alarmObj.on){ return false; } @@ -630,35 +634,35 @@ function isAlarmEnabled(){ } catch(ex){ } return false; -} +}; -function getAlarmMinutes(){ +let getAlarmMinutes = function(){ if(!isAlarmEnabled()){ return -1; } - var alarm = require('sched'); - var alarmObj = alarm.getAlarm(TIMER_IDX); + let alarm = require('sched'); + let alarmObj = alarm.getAlarm(TIMER_IDX); return Math.round(alarm.getTimeToAlarm(alarmObj)/(60*1000)); -} +}; -function increaseAlarm(){ +let increaseAlarm = function(){ try{ - var minutes = isAlarmEnabled() ? getAlarmMinutes() : 0; - var alarm = require('sched') + let minutes = isAlarmEnabled() ? getAlarmMinutes() : 0; + let alarm = require('sched') alarm.setAlarm(TIMER_IDX, { timer : (minutes+5)*60*1000, }); alarm.reload(); } catch(ex){ } -} +}; -function decreaseAlarm(){ +let decreaseAlarm = function(){ try{ - var minutes = getAlarmMinutes(); + let minutes = getAlarmMinutes(); minutes -= 5; - var alarm = require('sched') + let alarm = require('sched') alarm.setAlarm(TIMER_IDX, undefined); if(minutes > 0){ @@ -669,13 +673,13 @@ function decreaseAlarm(){ alarm.reload(); } catch(ex){ } -} +}; /* * Listeners */ -Bangle.on('lcdPower',on=>{ +let onLcdPower = on=>{ if (on) { // Whenever we connect to Gadgetbridge, reading data from // health failed. Therefore, we update only partially... @@ -685,31 +689,34 @@ Bangle.on('lcdPower',on=>{ if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; } -}); +}; +Bangle.on('lcdPower', onLcdPower); -Bangle.on('lock', function(isLocked) { +let onLock = function(isLocked) { drawInfo(); -}); +}; +Bangle.on('lock', onLock); -Bangle.on('charging',function(charging) { + +let onCharge = function(charging) { drawState(); -}); +}; +Bangle.on('charging', onCharge); -function feedback(){ +let feedback = function(){ Bangle.buzz(40, 0.3); -} +}; -// Touch gestures to control clock. We don't use swipe to be compatible with the bangle ecosystem -Bangle.on('touch', function(btn, e){ - var left = parseInt(g.getWidth() * 0.2); - var right = g.getWidth() - left; - var upper = parseInt(g.getHeight() * 0.2); - var lower = g.getHeight() - upper; +let onTouch = function(btn, e){ + let left = parseInt(g.getWidth() * 0.2); + let right = g.getWidth() - left; + let upper = parseInt(g.getHeight() * 0.2); + let lower = g.getHeight() - upper; - var is_left = e.x < left; - var is_right = e.x > right; - var is_upper = e.y < upper; - var is_lower = e.y > lower; + let is_left = e.x < left; + let is_right = e.x > right; + let is_upper = e.y < upper; + let is_lower = e.y > lower; if(!settings.disableData){ if(is_left && lcarsViewPos == 1){ @@ -744,16 +751,30 @@ Bangle.on('touch', function(btn, e){ draw(); return; } -}); - +}; +// Touch gestures to control clock. We don't use swipe to be compatible with the bangle ecosystem +Bangle.on('touch', onTouch); +let themeBefore = g.theme; /* * Lets start widgets, listen for btn etc. */ // Show launcher when middle button pressed -Bangle.setUI("clock"); +Bangle.setUI({mode:"clock",remove:function() { + if (drawTimeout) clearTimeout(drawTimeout); + delete Graphics.prototype.setFontAntonioMedium; + delete Graphics.prototype.setFontAntonioLarge; + Bangle.removeListener("lcdPower",onLcdPower); + Bangle.removeListener("lock",onLock); + Bangle.removeListener("charging",onCharge); + Bangle.removeListener("touch",onTouch); + require('sched').setAlarm(TIMER_IDX, undefined); + g.setTheme(themeBefore); + widget_utils.cleanup(); +}}); Bangle.loadWidgets(); // Clear the screen once, at startup and draw clock g.setTheme({bg:"#000",fg:"#fff",dark:true}).clear(); draw(); +} \ No newline at end of file diff --git a/apps/lcars/lcars.settings.js b/apps/lcars/lcars.settings.js index 7837eb4d1..81c71020f 100644 --- a/apps/lcars/lcars.settings.js +++ b/apps/lcars/lcars.settings.js @@ -26,7 +26,7 @@ } - var dataOptions = ["Steps", "Battery", "VREF", "HRM", "Temp", "Humidity", "Wind", "Altitude", "CoreT"]; + var dataOptions = ["Steps", "Battery", "BattVolt", "VREF", "HRM", "Temp", "Humidity", "Wind", "Altitude", "CoreT"]; var speedOptions = ["kph", "mph"]; var color_options = ['Green','Orange','Cyan','Purple','Red','Blue','Yellow','White','Purple','Pink','Light Green','Dark Green']; var bg_code = ['#00ff00','#FF9900','#0094FF','#FF00DC','#ff0000','#0000ff','#ffef00','#FFFFFF','#FF00FF','#6C00FF','#99FF00','#556B2F']; diff --git a/apps/lcars/metadata.json b/apps/lcars/metadata.json index 65c59081f..5fbb8dcf2 100644 --- a/apps/lcars/metadata.json +++ b/apps/lcars/metadata.json @@ -3,7 +3,7 @@ "name": "LCARS Clock", "shortName":"LCARS", "icon": "lcars.png", - "version":"0.27", + "version":"0.29", "readme": "README.md", "supports": ["BANGLEJS2"], "description": "Library Computer Access Retrieval System (LCARS) clock.", @@ -16,5 +16,6 @@ {"name":"lcars.app.js","url":"lcars.app.js"}, {"name":"lcars.img","url":"lcars.icon.js","evaluate":true}, {"name":"lcars.settings.js","url":"lcars.settings.js"} - ] + ], + "data":[{"name":"lcars.setting.json"}] } diff --git a/apps/limelight/metadata.json b/apps/limelight/metadata.json index 0e5dfd565..5c6bcfd76 100644 --- a/apps/limelight/metadata.json +++ b/apps/limelight/metadata.json @@ -13,5 +13,8 @@ {"name":"limelight.app.js","url":"limelight.app.js"}, {"name":"limelight.settings.js","url":"limelight.settings.js"}, {"name":"limelight.img","url":"limelight.icon.js","evaluate":true} + ], + "data": [ + {"name":"limelight.json"} ] } diff --git a/apps/linuxclock/metadata.json b/apps/linuxclock/metadata.json index 2bfe1d51f..ccd9db5e7 100644 --- a/apps/linuxclock/metadata.json +++ b/apps/linuxclock/metadata.json @@ -14,5 +14,6 @@ {"name":"linuxclock.app.js","url":"app.js"}, {"name":"linuxclock.img","url":"app-icon.js","evaluate":true}, {"name":"linuxclock.settings.js","url":"settings.js"} - ] + ], + "data":[{"name":"linuxclock.setting.json"}] } diff --git a/apps/loadingscreen/metadata.json b/apps/loadingscreen/metadata.json index 199f4a2b4..1815e4ce8 100644 --- a/apps/loadingscreen/metadata.json +++ b/apps/loadingscreen/metadata.json @@ -10,5 +10,6 @@ "readme": "README.md", "storage": [ {"name":"loadingscreen.settings.js","url":"settings.js"} - ] + ], + "data":[{"name":".loading"}] } diff --git a/apps/messagegui/ChangeLog b/apps/messagegui/ChangeLog index 2c9738dab..7fa45c496 100644 --- a/apps/messagegui/ChangeLog +++ b/apps/messagegui/ChangeLog @@ -96,4 +96,7 @@ Nav messages with '/' now get split on newlines 0.70: Handle nav messages from newer Gadgetbridge builds that output distance as a String If we receive a 'music' message and we're in the messages app (but not showing a message) show music (#2814) -0.71: Cancel buzzing when watch unlocked, or when different messages viewed \ No newline at end of file +0.71: Cancel buzzing when watch unlocked, or when different messages viewed + On 2v18.64+ firmware, 'No Messages' now has a 'back' button +0.72: Nav message updastes don't automatically launch navigation menu unless they're new +0.73: Add sharp left+right nav icons \ No newline at end of file diff --git a/apps/messagegui/app.js b/apps/messagegui/app.js index 030781df1..2df9875a3 100644 --- a/apps/messagegui/app.js +++ b/apps/messagegui/app.js @@ -70,6 +70,8 @@ var onMessagesModified = function(type,msg) { if (msg.state && msg.state!="play") openMusic = false; // no longer playing music to go back to if ((active!=undefined) && (active!="list") && (active!="music")) return; // don't open music over other screens (but do if we're in the main menu) } + if (msg && msg.id=="nav" && msg.t=="modify" && active!="map") + return; // don't show an updated nav message if we're just in the menu showMessage(msg&&msg.id); }; Bangle.on("message", onMessagesModified); @@ -102,6 +104,8 @@ function showMapMessage(msg) { case "right": img = "GhcBAABgAAA8AAAPgAAB8AAAPgAAB8D///j///9///+/AAPPAAHjgAD44AB8OAA+DgAPA4ABAOAAADgAAA4AAAOAAADgAAA4AAAOAAAA";break; case "left_slight": img = "ERgB//B/+D/8H4AP4Af4A74Bz4Dj4HD4OD4cD4AD4ADwADwADgAHgAPAAOAAcAA4ABwADgAH";break; case "right_slight": img = "ERgBB/+D/8H/4APwA/gD/APuA+cD44Phw+Dj4HPgAeAB4ADgAPAAeAA4ABwADgAHAAOAAcAA";break; + case "left_sharp": img = "GBaBAAAA+AAB/AAH/gAPjgAeBwA8BwB4B+DwB+HgB+PAB+eAB+8AB+4AB/wAB/gAB//gB//gB//gBwAABwAABwAABwAABw=="; break; + case "right_sharp": img = "GBaBAB8AAD+AAH/gAHHwAOB4AOA8AOAeAOAPB+AHh+ADx+AB5+AA9+AAd+AAP+AAH+AH/+AH/+AH/+AAAOAAAOAAAOAAAA==";break; case "keep_left": img = "ERmBAACAAOAB+AD+AP+B/+H3+PO+8c8w4wBwADgAHgAPAAfAAfAAfAAfAAeAAeAAcAA8AA4ABwADgA==";break; case "keep_right": img = "ERmBAACAAOAA/AD+AP+A//D/fPueeceY4YBwADgAPAAeAB8AHwAfAB8ADwAPAAcAB4ADgAHAAOAAAA==";break; case "uturn_left": img = "GRiBAAAH4AAP/AAP/wAPj8APAfAPAHgHgB4DgA8BwAOA4AHAcADsOMB/HPA7zvgd9/gOf/gHH/gDh/gBwfgA4DgAcBgAOAAAHAAADgAABw==";break; @@ -414,8 +418,9 @@ function checkMessages(options) { if (!options.clockIfNoMsg) return E.showPrompt(/*LANG*/"No Messages",{ title:/*LANG*/"Messages", img:require("heatshrink").decompress(atob("kkk4UBrkc/4AC/tEqtACQkBqtUDg0VqAIGgoZFDYQIIM1sD1QAD4AIBhnqA4WrmAIBhc6BAWs8AIBhXOBAWz0AIC2YIC5wID1gkB1c6BAYFBEQPqBAYXBEQOqBAnDAIQaEnkAngaEEAPDFgo+IKA5iIOhCGIAFb7RqAIGgtUBA0VqobFgNVA")), - buttons : {/*LANG*/"Ok":1} - }).then(() => { load() }); + buttons : {/*LANG*/"Ok":1}, + back: () => load() + }).then(() => load()); return load(); } // we have >0 messages diff --git a/apps/messagegui/metadata.json b/apps/messagegui/metadata.json index 9eb20a666..31d7fe262 100644 --- a/apps/messagegui/metadata.json +++ b/apps/messagegui/metadata.json @@ -2,7 +2,7 @@ "id": "messagegui", "name": "Message UI", "shortName": "Messages", - "version": "0.71", + "version": "0.73", "description": "Default app to display notifications from iOS and Gadgetbridge/Android", "icon": "app.png", "type": "app", diff --git a/apps/messages/README.md b/apps/messages/README.md index 83524d7c8..dce2a26c1 100644 --- a/apps/messages/README.md +++ b/apps/messages/README.md @@ -1,6 +1,6 @@ # Messages library -This library handles the passing of messages. It can storess a list of messages +This library handles the passing of messages. It can stores a list of messages and allows them to be retrieved by other apps. ## Example @@ -37,18 +37,10 @@ myMessageListener = Bangle.on("message", (type, message)=>{ }); ``` -Apps can launch the full GUI by calling `require("messages").openGUI()`, if you -want to write your own GUI, it should include boot code that listens for -`"messageGUI"` events: - -```js -Bangle.on("messageGUI", message=>{ - if (message.handled) return; // another app already opened it's GUI - message.handled = true; // prevent other apps form launching - Bangle.load("my_message_gui.app.js"); -}) - -``` +Apps can launch the currently installed Message GUI by calling `require("messages").openGUI()`. +If you want to write your own GUI, it should include a library called `messagegui` +with a method called `open` that will cause it to be opened, with the +optionally supplied message. See `apps/messagegui/lib.js` for an example. ## Requests diff --git a/apps/messages/lib.js b/apps/messages/lib.js index 7a515a1f8..f3ae253e6 100644 --- a/apps/messages/lib.js +++ b/apps/messages/lib.js @@ -107,7 +107,7 @@ exports.dismiss = function(msg) { }; /** - * Emit a "type=openGUI" event, to open GUI app + * Open the Messages GUI app * * @param {object} [msg={}] Message the app should show */ @@ -215,7 +215,7 @@ exports.buzz = function(msgSrc) { let repeat = msgSettings.repeat; if (repeat===undefined) repeat = 4; // repeat may be zero - if (repeat) + if (repeat) { exports.buzzInterval = setInterval(() => require("buzz").pattern(pattern), repeat*1000); let vibrateTimeout = msgSettings.vibrateTimeout; diff --git a/apps/metronome/metadata.json b/apps/metronome/metadata.json index 7f8582ca5..8ada5d98c 100644 --- a/apps/metronome/metadata.json +++ b/apps/metronome/metadata.json @@ -13,5 +13,6 @@ {"name":"metronome.app.js","url":"metronome.js"}, {"name":"metronome.img","url":"metronome-icon.js","evaluate":true}, {"name":"metronome.settings.js","url":"settings.js"} - ] + ], + "data":[{"name":"metronome.settings.json"}] } diff --git a/apps/multitimer/ChangeLog b/apps/multitimer/ChangeLog index 9a2ab0ff4..842384c8d 100644 --- a/apps/multitimer/ChangeLog +++ b/apps/multitimer/ChangeLog @@ -1,3 +1,4 @@ 0.01: Initial version 0.02: Update for time_utils module 0.03: Use default Bangle formatter for booleans +0.04: Remove copied sched alarm.js & import newer features (oneshot alarms) diff --git a/apps/multitimer/README.md b/apps/multitimer/README.md index f1e2eb281..04bdf4a6e 100644 --- a/apps/multitimer/README.md +++ b/apps/multitimer/README.md @@ -2,9 +2,10 @@ With this app, you can set timers and chronographs (stopwatches) and watch them count down/up in real time. You can also set alarms - swipe left or right to switch between the three functions. "Hard mode" is also available for timers and alarms. It will double the number of buzz counts and you will have to swipe the screen five to eight times correctly - make a mistake, and you will need to start over. +"Delete after expiration" can be set on a timer/alarm to have it delete itself once it's sounded (the same as the alarm app). ## WARNING * Editing timers in another app (such as the default Alarm app) is not recommended. Editing alarms should not be a problem (in theory). -* This app uses the [Scheduler library](https://banglejs.com/apps/?id=sched). +* This app uses the [Scheduler library](https://banglejs.com/apps/?id=sched). * To avoid potential conflicts with other apps that uses sched (especially ones that make use of the data and js field), this app only lists timers and alarms that it created - any made outside the app will be ignored. GB alarms are currently an exception as they do not make use of the data and js field. * A keyboard app is only used for adding messages to timers and is therefore not strictly needed. diff --git a/apps/multitimer/alarm.js b/apps/multitimer/alarm.js index eb1b3b259..b202ae662 100644 --- a/apps/multitimer/alarm.js +++ b/apps/multitimer/alarm.js @@ -1,148 +1,89 @@ -//sched.js, modified -// Chances are boot0.js got run already and scheduled *another* -// 'load(sched.js)' - so let's remove it first! -if (Bangle.SCHED) { - clearInterval(Bangle.SCHED); - delete Bangle.SCHED; -} - -function hardMode(tries, max) { - var R = Bangle.appRect; - - function adv() { - tries++; - hardMode(tries, max); - } - - if (tries < max) { - g.clear(); - g.reset(); - g.setClipRect(R.x,R.y,R.x2,R.y2); - var code = Math.abs(E.hwRand()%4); - if (code == 0) dir = "up"; - else if (code == 1) dir = "right"; - else if (code == 2) dir = "down"; - else dir = "left"; - g.setFont("6x8:2").setFontAlign(0,0).drawString(tries+"/"+max+"\nSwipe "+dir, (R.x2-R.x)/2, (R.y2-R.y)/2); - var drag; - Bangle.setUI({ - mode : "custom", - drag : e=>{ - if (!drag) { // start dragging - drag = {x: e.x, y: e.y}; - } else if (!e.b) { // released - const dx = e.x-drag.x, dy = e.y-drag.y; - drag = null; - //horizontal swipes - if (Math.abs(dx)>Math.abs(dy)+10) { - //left - if (dx<0 && code == 3) adv(); - //right - else if (dx>0 && code == 1) adv(); - //wrong swipe - reset - else startHM(); - } - //vertical swipes - else if (Math.abs(dy)>Math.abs(dx)+10) { - //up - if (dy<0 && code == 0) adv(); - //down - else if (dy>0 && code == 2) adv(); - //wrong swipe - reset - else startHM(); - } - } - } - }); - } - else { - if (!active[0].timer) active[0].last = (new Date()).getDate(); - if (!active[0].rp) active[0].on = false; - if (active[0].timer) active[0].timer = active[0].data.ot; - require("sched").setAlarms(alarms); - load(); - } -} - -function startHM() { - //between 5-8 random swipes - hardMode(0, Math.abs(E.hwRand()%4)+5); -} - -function showAlarm(alarm) { - const settings = require("sched").getSettings(); - - let msg = ""; - if (alarm.timer) msg += require("time_utils").formatTime(alarm.timer); - if (alarm.msg) { - msg += "\n"+alarm.msg; - } - else msg = atob("ACQswgD//33vRcGHIQAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAABVVVAAAAAAAAAP/wAAAAAAAAAP/wAAAAAAAAAqqoAPAAAAAAqqqqoP8AAAAKqqqqqv/AAACqqqqqqq/wAAKqqqlWqqvwAAqqqqlVaqrAACqqqqlVVqqAAKqqqqlVVaqgAKqaqqlVVWqgAqpWqqlVVVqoAqlWqqlVVVaoCqlV6qlVVVaqCqVVfqlVVVWqCqVVf6lVVVWqKpVVX/lVVVVqqpVVV/+VVVVqqpVVV//lVVVqqpVVVfr1VVVqqpVVVfr1VVVqqpVVVb/lVVVqqpVVVW+VVVVqqpVVVVVVVVVqiqVVVVVVVVWqCqVVVVVVVVWqCqlVVVVVVVaqAqlVVVVVVVaoAqpVVVVVVVqoAKqVVVVVVWqgAKqlVVVVVaqgACqpVVVVVqqAAAqqlVVVaqoAAAKqqVVWqqgAAACqqqqqqqAAAAAKqqqqqgAAAAAAqqqqoAAAAAAAAqqoAAAAA==")+" "+msg; - - Bangle.loadWidgets(); - Bangle.drawWidgets(); - - let buzzCount = settings.buzzCount; - - if (alarm.data.hm && alarm.data.hm == true) { - //hard mode extends auto-snooze time - buzzCount = buzzCount * 3; - startHM(); - } - - else { - E.showPrompt(msg,{ - title: "TIMER!", - buttons : {"Snooze":true,"Ok":false} // default is sleep so it'll come back in 10 mins - }).then(function(sleep) { - buzzCount = 0; - if (sleep) { - if(alarm.ot===undefined) alarm.ot = alarm.t; - alarm.t += settings.defaultSnoozeMillis; - } else { - if (!alarm.timer) alarm.last = (new Date()).getDate(); - if (alarm.ot!==undefined) { - alarm.t = alarm.ot; - delete alarm.ot; - } - if (!alarm.rp) alarm.on = false; - } - //reset timer value - if (alarm.timer) alarm.timer = alarm.data.ot; - // alarm is still a member of 'alarms', so writing to array writes changes back directly - require("sched").setAlarms(alarms); - load(); - }); - } - - function buzz() { - if (settings.unlockAtBuzz) { - Bangle.setLocked(false); - } - - require("buzz").pattern(alarm.vibrate === undefined ? "::" : alarm.vibrate).then(() => { - if (buzzCount--) { - setTimeout(buzz, settings.buzzIntervalMillis); - } else if (alarm.as) { // auto-snooze - buzzCount = settings.buzzCount; - setTimeout(buzz, settings.defaultSnoozeMillis); - } - }); - } - - if ((require("Storage").readJSON("setting.json", 1) || {}).quiet > 1) - return; - - buzz(); -} - -// Check for alarms -let alarms = require("sched").getAlarms(); -let active = require("sched").getActiveAlarms(alarms); -if (active.length) { - // if there's an alarm, show it - showAlarm(active[0]); -} else { - // otherwise just go back to default app - setTimeout(load, 100); -} +// called by getActiveAlarms(...)[0].js +if (Bangle.SCHED) { + clearInterval(Bangle.SCHED); + delete Bangle.SCHED; +} + +function hardMode(tries, max) { + var R = Bangle.appRect; + + function adv() { + tries++; + hardMode(tries, max); + } + + if (tries < max) { + g.clear(); + g.reset(); + g.setClipRect(R.x,R.y,R.x2,R.y2); + var code = Math.abs(E.hwRand()%4); + if (code == 0) dir = "up"; + else if (code == 1) dir = "right"; + else if (code == 2) dir = "down"; + else dir = "left"; + g.setFont("6x8:2").setFontAlign(0,0).drawString(tries+"/"+max+"\nSwipe "+dir, (R.x2-R.x)/2, (R.y2-R.y)/2); + var drag; + Bangle.setUI({ + mode : "custom", + drag : e=>{ + if (!drag) { // start dragging + drag = {x: e.x, y: e.y}; + } else if (!e.b) { // released + const dx = e.x-drag.x, dy = e.y-drag.y; + drag = null; + //horizontal swipes + if (Math.abs(dx)>Math.abs(dy)+10) { + //left + if (dx<0 && code == 3) adv(); + //right + else if (dx>0 && code == 1) adv(); + //wrong swipe - reset + else startHM(); + } + //vertical swipes + else if (Math.abs(dy)>Math.abs(dx)+10) { + //up + if (dy<0 && code == 0) adv(); + //down + else if (dy>0 && code == 2) adv(); + //wrong swipe - reset + else startHM(); + } + } + } + }); + } + else { + if (!active[0].timer) active[0].last = (new Date()).getDate(); + if (!active[0].rp) active[0].on = false; + if (active[0].timer) active[0].timer = active[0].data.ot; + require("sched").setAlarms(alarms); + load(); + } +} + +function startHM() { + //between 5-8 random swipes + hardMode(0, Math.abs(E.hwRand()%4)+5); +} + +function buzz() { + const settings = require("sched").getSettings(); + let buzzCount = 3 * settings.buzzCount; + + require("buzz").pattern(alarm.vibrate === undefined ? "::" : alarm.vibrate).then(() => { + if (buzzCount--) { + setTimeout(buzz, settings.buzzIntervalMillis); + } else if (alarm.as) { // auto-snooze + buzzCount = settings.buzzCount; + setTimeout(buzz, settings.defaultSnoozeMillis); + } + }); +} + +let alarms = require("sched").getAlarms(); +let active = require("sched").getActiveAlarms(alarms); +let alarm = active[0]; +// active[0] is a HM alarm (otherwise we'd have triggered sched.js instead of this file) +startHM(); +buzz(); diff --git a/apps/multitimer/app.js b/apps/multitimer/app.js index 8832d1a25..ae8647db0 100644 --- a/apps/multitimer/app.js +++ b/apps/multitimer/app.js @@ -56,6 +56,14 @@ function clearInt() { timerInt2 = []; } +function setHM(alarm, on) { + if (on) + alarm.js = "(require('Storage').read('multitimer.alarm.js') !== undefined) ? load('multitimer.alarm.js') : load('sched.js')"; + else + delete alarm.js; + alarm.data.hm = on; +} + function drawTimers() { layer = 0; var timers = require("sched").getAlarms().filter(a => a.timer && a.appid == "multitimer"); @@ -228,12 +236,11 @@ function editTimer(idx, a) { else a = timers[idx]; } if (!a.data) { - a.data = {}; - a.data.hm = false; + a.data = { hm: false }; } var t = decodeTime(a.timer); - function editMsg(idx, a) { + function editMsg(idx, a) { g.clear(); idx < 0 ? msg = "" : msg = a.msg; require("textinput").input({text:msg}).then(result => { @@ -267,7 +274,10 @@ function editTimer(idx, a) { }, "Enabled": { value: a.on, - onchange: v => a.on = v + onchange: v => { + delete a.last; + a.on = v; + } }, "Hours": { value: t.hrs, min: 0, max: 23, wrap: true, @@ -292,9 +302,13 @@ function editTimer(idx, a) { }, "Hard Mode": { value: a.data.hm, - onchange: v => a.data.hm = v + onchange: v => setHM(a, v), }, "Vibrate": require("buzz_menu").pattern(a.vibrate, v => a.vibrate = v), + "Delete After Expiration": { + value: !!a.del, + onchange: v => a.del = v + }, "Msg": { value: !a.msg ? "" : a.msg.length > 6 ? a.msg.substring(0, 6)+"..." : a.msg, //menu glitch? setTimeout required here @@ -382,7 +396,7 @@ function swMenu(idx, a) { }, 100 - (a.t % 100)); } - function editMsg(idx, a) { + function editMsg(idx, a) { g.clear(); msg = a.msg; require("textinput").input({text:msg}).then(result => { @@ -556,12 +570,11 @@ function editAlarm(idx, a) { else a = require("sched").newDefaultAlarm(); } if (!a.data) { - a.data = {}; - a.data.hm = false; + a.data = { hm: false }; } var t = decodeTime(a.t); - function editMsg(idx, a) { + function editMsg(idx, a) { g.clear(); idx < 0 ? msg = "" : msg = a.msg; require("textinput").input({text:msg}).then(result => { @@ -582,8 +595,6 @@ function editAlarm(idx, a) { var menu = { "": { "title": "Alarm" }, "< Back": () => { - if (a.data.hm == true) a.js = "(require('Storage').read('multitimer.alarm.js') !== undefined) ? load('multitimer.alarm.js') : load('sched.js')"; - if (a.data.hm == false && a.js) delete a.js; if (idx >= 0) alarms[alarmIdx[idx]] = a; else alarms.push(a); require("sched").setAlarms(alarms); @@ -592,7 +603,10 @@ function editAlarm(idx, a) { }, "Enabled": { value: a.on, - onchange: v => a.on = v + onchange: v => { + delete a.last; + a.on = v; + } }, "Hours": { value: t.hrs, min: 0, max: 23, wrap: true, @@ -618,9 +632,13 @@ function editAlarm(idx, a) { }, "Hard Mode": { value: a.data.hm, - onchange: v => a.data.hm = v + onchange: v => setHM(a, v), }, "Vibrate": require("buzz_menu").pattern(a.vibrate, v => a.vibrate = v), + "Delete After Expiration": { + value: !!a.del, + onchange: v => a.del = v + }, "Auto Snooze": { value: a.as, onchange: v => a.as = v @@ -653,7 +671,7 @@ Bangle.on("drag", e=>{ if (layer < 0) return; if (!drag) { // start dragging drag = {x: e.x, y: e.y}; - } + } else if (!e.b) { // released const dx = e.x-drag.x, dy = e.y-drag.y; drag = null; diff --git a/apps/multitimer/boot.js b/apps/multitimer/boot.js new file mode 100644 index 000000000..70b9032f6 --- /dev/null +++ b/apps/multitimer/boot.js @@ -0,0 +1,8 @@ +{ + const resetTimer = alarm => { + if (alarm.timer) alarm.timer = alarm.data.ot; + }; + + Bangle.on("alarmSnooze", resetTimer); + Bangle.on("alarmDismiss", resetTimer); +} diff --git a/apps/multitimer/metadata.json b/apps/multitimer/metadata.json index ee77d2ecb..7a23052c9 100644 --- a/apps/multitimer/metadata.json +++ b/apps/multitimer/metadata.json @@ -1,7 +1,7 @@ { "id": "multitimer", "name": "Multi Timer", - "version": "0.03", + "version": "0.04", "description": "Set timers and chronographs (stopwatches) and watch them count down in real time. Pause, create, edit, and delete timers and chronos, and add custom labels/messages. Also sets alarms.", "icon": "app.png", "screenshots": [ @@ -14,6 +14,7 @@ "readme": "README.md", "storage": [ {"name":"multitimer.app.js","url":"app.js"}, + {"name":"multitimer.boot.js","url":"boot.js"}, {"name":"multitimer.alarm.js","url":"alarm.js"}, {"name":"multitimer.img","url":"app-icon.js","evaluate":true} ], diff --git a/apps/mylocation/ChangeLog b/apps/mylocation/ChangeLog index afe1810e9..ea1c77bde 100644 --- a/apps/mylocation/ChangeLog +++ b/apps/mylocation/ChangeLog @@ -7,3 +7,4 @@ 0.07: Move mylocation app into 'Settings -> Apps' 0.08: Allow setting location from webinterface in the AppLoader 0.09: Fix web interface so app can be installed (replaced custom with interface html) +0.10: Add waypoints as location source diff --git a/apps/mylocation/README.md b/apps/mylocation/README.md index 11a644262..7fecfdfcb 100644 --- a/apps/mylocation/README.md +++ b/apps/mylocation/README.md @@ -9,7 +9,7 @@ next to it - and you can choose your location on a map. **On Bangle.js** go to `Settings -> Apps -> My Location` -* Select one of the preset Cities, setup through the GPS or use the webinterface from the AppLoader +* Select one of the preset Cities, setup through the GPS, waypoints (if installed) or use the webinterface from the AppLoader * Other Apps can read this information to do calculations based on location * When the City shows ??? it means the location has been set through the GPS diff --git a/apps/mylocation/metadata.json b/apps/mylocation/metadata.json index 1c2974030..7e0d16d16 100644 --- a/apps/mylocation/metadata.json +++ b/apps/mylocation/metadata.json @@ -4,8 +4,8 @@ "icon": "app.png", "type": "settings", "screenshots": [{"url":"screenshot_1.png"}], - "version":"0.09", - "description": "Sets and stores the latitude and longitude of your preferred City. It can be set from GPS or webinterface. `mylocation.json` can be used by other apps that need your main location. See README for details.", + "version":"0.10", + "description": "Sets and stores the latitude and longitude of your preferred City. It can be set from GPS, waypoints or webinterface. `mylocation.json` can be used by other apps that need your main location. See README for details.", "readme": "README.md", "tags": "tool,utility", "supports": ["BANGLEJS", "BANGLEJS2"], diff --git a/apps/mylocation/settings.js b/apps/mylocation/settings.js index 7033500fa..fcae0389c 100644 --- a/apps/mylocation/settings.js +++ b/apps/mylocation/settings.js @@ -13,7 +13,7 @@ let s = { function loadSettings() { settings = require('Storage').readJSON(SETTINGS_FILE, 1) || {}; for (const key in settings) { - s[key] = settings[key] + s[key] = settings[key]; } } @@ -31,7 +31,7 @@ function setFromGPS() { //console.log("."); if (gps.fix === 0) return; //console.log("fix from GPS"); - s = {'lat': gps.lat, 'lon': gps.lon, 'location': '???' }; + s = {'lat': gps.lat, 'lon': gps.lon, 'location': 'GPS' }; Bangle.buzz(1500); // buzz on first position Bangle.setGPSPower(0, "mylocation"); saveSettings(); @@ -50,6 +50,25 @@ function setFromGPS() { Bangle.setUI("updown", undefined); } +function setFromWaypoint() { + wpmenu = { + '': { 'title': /*LANG*/'Waypoint' }, + '< Back': ()=>{ showMainMenu(); }, + }; + require("waypoints").load().forEach(wp => { + if (typeof(wp.lat) === 'number' && typeof(wp.lon) === 'number') { + wpmenu[wp.name] = ()=>{ + s.location = wp.name; + s.lat = parseFloat(wp.lat); + s.lon = parseFloat(wp.lon); + saveSettings(); + showMainMenu(); + }; + } + }); + return E.showMenu(wpmenu); +} + function showMainMenu() { //console.log("showMainMenu"); const mainmenu = { @@ -58,7 +77,13 @@ function showMainMenu() { /*LANG*/'City': { value: 0 | locations.indexOf(s.location), min: 0, max: locations.length - 1, - format: v => locations[v], + format: v => { + if (v === -1) { + return s.location; + } else { + return locations[v]; + } + }, onchange: v => { if (locations[v] !== "???") { s.location = locations[v]; @@ -70,6 +95,12 @@ function showMainMenu() { }, /*LANG*/'Set From GPS': ()=>{ setFromGPS(); } }; + try { + require("waypoints"); + mainmenu[/*LANG*/'Set From Waypoint'] = ()=>{ setFromWaypoint(); }; + } catch(err) { + // waypoints not installed, thats ok + } return E.showMenu(mainmenu); } diff --git a/apps/mysticclock/metadata.json b/apps/mysticclock/metadata.json index bd2df2f8d..51f44b79b 100644 --- a/apps/mysticclock/metadata.json +++ b/apps/mysticclock/metadata.json @@ -14,5 +14,8 @@ {"name":"mysticclock.app.js","url":"mystic-clock-app.js"}, {"name":"mysticclock.settings.js","url":"mystic-clock-settings.js"}, {"name":"mysticclock.img","url":"mystic-clock-icon.js","evaluate":true} + ], + "data": [ + {"name":"mysticclock.settings.json"} ] } diff --git a/apps/mysticdock/metadata.json b/apps/mysticdock/metadata.json index 2775b0b72..0ebff3b9b 100644 --- a/apps/mysticdock/metadata.json +++ b/apps/mysticdock/metadata.json @@ -13,5 +13,8 @@ {"name":"mysticdock.boot.js","url":"mystic-dock-boot.js"}, {"name":"mysticdock.settings.js","url":"mystic-dock-settings.js"}, {"name":"mysticdock.img","url":"mystic-dock-icon.js","evaluate":true} + ], + "data": [ + {"name":"mysticdock.settings.json"} ] } diff --git a/apps/openstmap/ChangeLog b/apps/openstmap/ChangeLog index d8ab55482..c1b8e5f21 100644 --- a/apps/openstmap/ChangeLog +++ b/apps/openstmap/ChangeLog @@ -18,4 +18,6 @@ 0.15: Make track drawing an option (default off) 0.16: Draw waypoints, too. 0.17: With new Recorder app allow track to be drawn in the background - Switch tile layer URL for faster/more reliable map tiles \ No newline at end of file + Switch tile layer URL for faster/more reliable map tiles +0.18: Prefer map with highest resolution +0.19: Remember latitude, longitude & scale diff --git a/apps/openstmap/app.js b/apps/openstmap/app.js index c69ccece3..88883b6d6 100644 --- a/apps/openstmap/app.js +++ b/apps/openstmap/app.js @@ -6,11 +6,27 @@ var mapVisible = false; var hasScrolled = false; var settings = require("Storage").readJSON("openstmap.json",1)||{}; var plotTrack; +let checkMapPos = false; // Do we need to check the if the coordinates we have are valid + +if (settings.lat !== undefined && settings.lon !== undefined && settings.scale !== undefined) { + // restore last view + m.lat = settings.lat; + m.lon = settings.lon; + m.scale = settings.scale; + checkMapPos = true; +} // Redraw the whole page function redraw() { g.setClipRect(R.x,R.y,R.x2,R.y2); - m.draw(); + const count = m.draw(); + if (checkMapPos && count === 0) { + // no map at these coordinates, lets try again with first map + m.lat = m.map.lat; + m.lon = m.map.lon; + m.scale = m.map.scale; + m.draw(); + } drawPOI(); drawMarker(); // if track drawing is enabled... @@ -45,7 +61,7 @@ function drawPOI() { 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); + //print(wp.name); }) } @@ -59,24 +75,33 @@ function drawMarker() { Bangle.on('GPS',function(f) { fix=f; - if (HASWIDGETS) WIDGETS["sats"].draw(WIDGETS["sats"]); + if (HASWIDGETS && WIDGETS["sats"]) WIDGETS["sats"].draw(WIDGETS["sats"]); if (mapVisible) drawMarker(); }); Bangle.setGPSPower(1, "app"); if (HASWIDGETS) { Bangle.loadWidgets(); - WIDGETS["sats"] = { area:"tl", width:48, draw:w=>{ - var txt = (0|fix.satellites)+" Sats"; - if (!fix.fix) txt += "\nNO FIX"; - g.reset().setFont("6x8").setFontAlign(0,0) - .drawString(txt,w.x+24,w.y+12); - } - }; + if (!WIDGETS["gps"]) { // one GPS Widget is enough + WIDGETS["sats"] = { area:"tl", width:48, draw:w=>{ + var txt = (0|fix.satellites)+" Sats"; + if (!fix.fix) txt += "\nNO FIX"; + g.reset().setFont("6x8").setFontAlign(0,0) + .drawString(txt,w.x+24,w.y+12); + } + }; + } Bangle.drawWidgets(); } R = Bangle.appRect; +function writeSettings() { + settings.lat = m.lat; + settings.lon = m.lon; + settings.scale = m.scale; + require("Storage").writeJSON("openstmap.json",settings); +} + function showMap() { mapVisible = true; g.reset().clearRect(R); @@ -108,7 +133,7 @@ function showMap() { }, /*LANG*/"Draw Track": { value : !!settings.drawTrack, - onchange : v => { settings.drawTrack=v; require("Storage").writeJSON("openstmap.json",settings); } + onchange : v => { settings.drawTrack=v; writeSettings(); } }, /*LANG*/"Center Map": () =>{ m.lat = m.map.lat; @@ -126,3 +151,6 @@ function showMap() { } showMap(); + +// Write settings on exit via button +setWatch(() => writeSettings(), BTN, { repeat: true, edge: "rising" }); diff --git a/apps/openstmap/imagefilter.js b/apps/openstmap/imagefilter.js new file mode 100644 index 000000000..aa976dc0f --- /dev/null +++ b/apps/openstmap/imagefilter.js @@ -0,0 +1,106 @@ +/* Image filtering code that helps to transform the OSM tile +into something that's usable on a 3bpp screen. + +Stick this in a file so we can +*/ + + +function imageFilterFor3BPP(srcData, dstData, options) { + options = options || {}; + if (options.colLo === undefined) + options.colLo = 140; // when adding contrast/saturation, this is the max saturaton we add + if (options.colHi === undefined) + options.colHi = 250; + if (options.sharpen === undefined) + options.sharpen = true; + if (options.dither === undefined) + options.dither = false; + + const width = srcData.width; + const height = srcData.height; + var rgbaSrc = srcData.data; + var rgbaDst = dstData.data; + function getPixel(x,y) { + if (x<0) x=0; + if (y<0) y=0; + if (x>=width) x=width-1; + if (y>=height) y=height-1; + var i = (x + y*width)*4; + return [ + rgbaSrc[i+0], rgbaSrc[i+1], rgbaSrc[i+2] + ]; + } + function dmul(a, mul) { return a.map(a => a.map(n=>n*mul)); } + const KS = 5; // kernel size + const KO = 2; // kernel offset + const K = dmul([ // 5x5 sharpening kernel + [ 1,4,6,4,1 ], + [4,16,24,16,4], + [6,24,-476,24,6], + [4,16,24,16,4], + [ 1,4,6,4,1 ], + ], -1/256); + /*const KS = 7; // kernel size + const KO = 3; // kernel offset + const K = dmul([ // 7x7 sharpening (gaussian - 2x middle pixel) + [ 0, 0, 1, 2, 1, 0, 0 ], + [ 0, 3,13,22,13, 3, 0 ], + [ 1,13,59,97,59,13, 1 ], + [ 2,22,97,159-2006,97,22,2 ], + [ 1,13,59,97,59,13, 1 ], + [ 0, 3,13,22,13, 3, 0 ], + [ 0, 0, 1, 2, 1, 0, 0 ], + ], -1/1003);*/ + const DITHERM = 3; // dither width -1 (dither must be power 2) + const DITHER = dmul([ // dithering matrix + [ 0,1,2,3 ], + [ 1,2,3,0 ], + [ 2,3,2,1 ], + [ 3,2,1,0 ], + ], 256/4); + + + var idx=0; + for (var y=0;y255) col[n]=255; + } + } else { // if not sharpening, just get pixel + col = getPixel(x,y); + } + // increase saturation / contrast + var min = Math.min(col[0], col[1], col[2]); + var max = Math.max(col[0], col[1], col[2]); + var d = max-min; + if (min>options.colLo) min=options.colLo; + if (max DITHER[x&DITHERM][y&DITHERM]) // dither + col[n] = 255; + else + col[n] = 0; + } + rgbaDst[idx+n] = col[n]; + } + rgbaDst[idx+3] = 255; + idx+=4; + } + } +} diff --git a/apps/openstmap/interface.html b/apps/openstmap/interface.html index 618c5822e..0d9ef3152 100644 --- a/apps/openstmap/interface.html +++ b/apps/openstmap/interface.html @@ -69,7 +69,7 @@ - + + + + + + diff --git a/apps/openstmap/test/osm-test.png b/apps/openstmap/test/osm-test.png new file mode 100644 index 000000000..af22331f5 Binary files /dev/null and b/apps/openstmap/test/osm-test.png differ diff --git a/apps/openwind/metadata.json b/apps/openwind/metadata.json index 01d7ca124..5e0a60972 100644 --- a/apps/openwind/metadata.json +++ b/apps/openwind/metadata.json @@ -6,10 +6,11 @@ "icon": "openwind.png", "readme": "README.md", "tags": "ble,outdoors,gps,sailing", - "supports" : ["BANGLEJS", "BANGLEJS2"], + "supports" : ["BANGLEJS", "BANGLEJS2"], "storage": [ {"name":"openwind.app.js","url":"app.js"}, {"name":"openwind.img","url":"app-icon.js","evaluate":true}, {"name":"openwind.settings.js", "url":"settings.js"} - ] + ], + "data":[{"name":"openwindsettings.json"}] } diff --git a/apps/pebble/metadata.json b/apps/pebble/metadata.json index 0ccb8e2af..49dbcd820 100644 --- a/apps/pebble/metadata.json +++ b/apps/pebble/metadata.json @@ -15,5 +15,8 @@ {"name":"pebble.app.js","url":"pebble.app.js"}, {"name":"pebble.settings.js","url":"pebble.settings.js"}, {"name":"pebble.img","url":"pebble.icon.js","evaluate":true} + ], + "data": [ + {"name":"pebble.settings.json"} ] } diff --git a/apps/pebbled/metadata.json b/apps/pebbled/metadata.json index 62fabc3e7..8924e600a 100644 --- a/apps/pebbled/metadata.json +++ b/apps/pebbled/metadata.json @@ -14,5 +14,6 @@ {"name":"pebbled.app.js","url":"pebbled.app.js"}, {"name":"pebbled.settings.js","url":"pebbled.settings.js"}, {"name":"pebbled.img","url":"pebbled.icon.js","evaluate":true} - ] + ], + "data":[{"name":"pebbleDistance.json"}] } diff --git a/apps/pebblepp/metadata.json b/apps/pebblepp/metadata.json index 6821fdc50..88c1d8fd5 100644 --- a/apps/pebblepp/metadata.json +++ b/apps/pebblepp/metadata.json @@ -15,5 +15,8 @@ {"name":"pebblepp.app.js","url":"app.js"}, {"name":"pebblepp.settings.js","url":"settings.js"}, {"name":"pebblepp.img","url":"icon.js","evaluate":true} + ], + "data": [ + {"name":"pebblepp.settings.json"} ] } diff --git a/apps/podadrem/ChangeLog b/apps/podadrem/ChangeLog index 3c68f15ac..2f6fa8764 100644 --- a/apps/podadrem/ChangeLog +++ b/apps/podadrem/ChangeLog @@ -7,3 +7,6 @@ Addict. 0.06: Add compatibility with Fastload Utils. 0.07: Remove just the specific listeners to not interfere with Quick Launch when fastloading. +0.08: Issue newline before GB commands (solves issue with console.log and ignored commands) +0.09: Don't send the gadgetbridge wake command twice. Once should do since we + issue newline before GB commands. diff --git a/apps/podadrem/app.js b/apps/podadrem/app.js index 9c9ed8b04..6f8721126 100644 --- a/apps/podadrem/app.js +++ b/apps/podadrem/app.js @@ -67,7 +67,6 @@ let touchHandler = function(_, xy) { } else if ((R.x-1 { if (e.type == 2) return; diff --git a/apps/quicklaunch/metadata.json b/apps/quicklaunch/metadata.json index c2e3029f7..5fd06ba95 100644 --- a/apps/quicklaunch/metadata.json +++ b/apps/quicklaunch/metadata.json @@ -2,7 +2,7 @@ "id": "quicklaunch", "name": "Quick Launch", "icon": "app.png", - "version": "0.14", + "version": "0.15", "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", diff --git a/apps/rebble/metadata.json b/apps/rebble/metadata.json index c380204a4..6242236c8 100644 --- a/apps/rebble/metadata.json +++ b/apps/rebble/metadata.json @@ -15,5 +15,8 @@ {"name":"rebble.app.js","url":"rebble.app.js"}, {"name":"rebble.settings.js","url":"rebble.settings.js"}, {"name":"rebble.img","url":"rebble.icon.js","evaluate":true} + ], + "data": [ + {"name":"rebble.settings.json"} ] } diff --git a/apps/recorder/ChangeLog b/apps/recorder/ChangeLog index 94e2f28c2..991b811cb 100644 --- a/apps/recorder/ChangeLog +++ b/apps/recorder/ChangeLog @@ -32,4 +32,6 @@ 0.24: Can now specify `setRecording(true, {force:...` to not show a menu 0.25: Widget now has `isRecording()` for retrieving recording status. 0.26: Now record filename based on date -0.27: Fix first ever recorded filename being log0 (now all are dated) \ No newline at end of file +0.27: Fix first ever recorded filename being log0 (now all are dated) +0.28: Automatically create new track if the filename is different +0.29: When plotting with OpenStMap scale map to track width & height diff --git a/apps/recorder/app-settings.json b/apps/recorder/app-settings.json index 7410af213..cbabcb3f0 100644 --- a/apps/recorder/app-settings.json +++ b/apps/recorder/app-settings.json @@ -1,6 +1,5 @@ { "recording":false, - "file":"recorder.log0.csv", "period":10, "record" : ["gps"] } diff --git a/apps/recorder/app.js b/apps/recorder/app.js index ca3eec525..a2218420a 100644 --- a/apps/recorder/app.js +++ b/apps/recorder/app.js @@ -224,6 +224,12 @@ function viewTrack(filename, info) { // Function to convert lat/lon to XY var getMapXY; if (info.qOSTM) { + // scale map to view full track + const max = Bangle.project({lat: info.maxLat, lon: info.maxLong}); + const min = Bangle.project({lat: info.minLat, lon: info.minLong}); + const scaleX = (max.x-min.x)/Bangle.appRect.w; + const scaleY = (max.y-min.y)/Bangle.appRect.h; + osm.scale = Math.ceil((scaleX > scaleY ? scaleX : scaleY)*1.1); // add 10% margin getMapXY = osm.latLonToXY.bind(osm); } else { getMapXY = function(lat, lon) { "ram" diff --git a/apps/recorder/metadata.json b/apps/recorder/metadata.json index beb5648c8..e714abf8d 100644 --- a/apps/recorder/metadata.json +++ b/apps/recorder/metadata.json @@ -2,7 +2,7 @@ "id": "recorder", "name": "Recorder", "shortName": "Recorder", - "version": "0.27", + "version": "0.29", "description": "Record GPS position, heart rate and more in the background, then download to your PC.", "icon": "app.png", "tags": "tool,outdoors,gps,widget", diff --git a/apps/recorder/widget.d.ts b/apps/recorder/widget.d.ts new file mode 100644 index 000000000..30430eac4 --- /dev/null +++ b/apps/recorder/widget.d.ts @@ -0,0 +1,40 @@ +type RecorderWidget = Widget & { + getRecorders(): Recorders; + + reload(): void, + + isRecording(): boolean, + + setRecording( + isOn: boolean, + options?: { force?: "append" | "new" | "overwrite" }, + ): Promise; + + plotTrack( + m: unknown /* osm module */, + options?: { + async: true, + callback?: ()=>void, + } + ): { stop(): void }; + plotTrack( + m: unknown /* osm module */, + options?: { + async?: false, + callback?: ()=>void, + } + ): void; +}; + +type Recorders = { + [key: string]: Recorder; +}; + +type Recorder = () => { + name: string, + fields: string[], + getValues(): unknown[], + start(): void, + stop(): void, + draw(x: number, y: number): void, +}; diff --git a/apps/recorder/widget.js b/apps/recorder/widget.js index 3c9afcf70..e34fecfbc 100644 --- a/apps/recorder/widget.js +++ b/apps/recorder/widget.js @@ -241,9 +241,12 @@ options = options||{}; if (isOn && !settings.recording) { var date=(new Date()).toISOString().substr(0,10).replace(/-/g,""), trackNo=10; - if (!settings.file) { // if no filename set - settings.file = "recorder.log" + date + trackNo.toString(36) + ".csv"; - } else if (require("Storage").list(settings.file).length){ // if file exists + function getTrackFilename() { return "recorder.log" + date + trackNo.toString(36) + ".csv"; } + if (!settings.file || !settings.file.startsWith("recorder.log" + date)) { + // if no filename set or date different, set up a new filename + settings.file = getTrackFilename(); + } + if (require("Storage").list(settings.file).length){ // if file exists if (!options.force) { // if not forced, ask the question g.reset(); // work around bug in 2v17 and earlier where bg color wasn't reset return E.showPrompt( @@ -266,7 +269,7 @@ // new file - use the current date var newFileName; do { // while a file exists, add one to the letter after the date - newFileName = "recorder.log" + date + trackNo.toString(36) + ".csv"; + newFileName = getTrackFilename(); trackNo++; } while (require("Storage").list(newFileName).length); settings.file = newFileName; diff --git a/apps/rep/README.md b/apps/rep/README.md new file mode 100644 index 000000000..343837f4d --- /dev/null +++ b/apps/rep/README.md @@ -0,0 +1,13 @@ +# Description + +A running/activity repetition app. Program your reps using the web interface, and then run through them with the app. The time left for the current rep is shown, along with the rep's description and total duration, and the next rep's description and duration. + +You can rewind, fast-forward and play/pause with the control buttons. + + +# Todo + +- [X] Recorder functionality + - [ ] Recorder toggle functionality +- [ ] Fastload: scoping, unregister layout handlers etc +- [ ] Swipe handlers as well as "<<" / ">>" buttons diff --git a/apps/rep/app-icon.js b/apps/rep/app-icon.js new file mode 100644 index 000000000..6ad60b46f --- /dev/null +++ b/apps/rep/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwxH+r/+ACIZBkgACAoIaSFiVJumYzwAEzF0p4yggFP4XC4YAB4QtBAwnCGLpbBzwjCuclklJAAUkkt0HYWepIxZgAgCzFPR4ZcCMQKPBpNPzARBugwSstlFwYcCKwIrDFQQ0CGYYPBIQQwDxgvQFwInBklzDwKPBRwiQCBgo3BGAiMSJoIyBXoRaBdYYJBMgMlGIIsBaYSSSgFPFwMkMIQiDEYNzLoNzBgI8DLoRhB4VPGCEAXYV0FwKUCcAUxmNbrYDBOISKCLoTDCF54bCuhhFP4Nbw2G2AACAoNbT4RdEJAKRPkhCBkphCD4UAwAtEGImANwQ/BDQkkF5q1BWYICBDQUrFpAxElZEDYwICCRxpHBkoCEudbxgvLxlbuZgBkoCESBikCOYJeCzEGLxhgCgxaBpIcDbQIvLFIN0pJbBAQNtqpRBgwABgAAEBARtBqtt4dJMYNJug1BF5dPFgIUFwxQBrYAKNwOGIgJKDDoNPF5tPF4R6BkovCwoAKF4SnDF4IgCF5hZBuYCBzGYkiwCF5uwkgVBVoKqB4QvMgFJgEkAQMlAQIvRDQMlAQMkEARf7TwV0X4WeF6eeX4V0X54v/F/4v/F9/CpNzAQOYzAvTCoNJ4VzAQIvMgFJgEkAQMlAQIvRDQMlAQMkEARf7TwVzX4XCkuGF5+GkvCX4Vzd6FzF4V0MQIvCrYAKF4TsCF4QdBF5huBCgYCBttVxgjBABVbxlVtpHBJQovLkmeRQPCzw1BzCwCABhuBUgNJDgkkF5cAzHDkoCEuZRBFxZtBLYOYkoCEgAvL/1zLQICCMAPClZgMw0rCINJMINzAQQuMSAIXBOYIaCI4WAGJGGrh3CIgYaCRxiQCunDulPUgQfDreGGIgFBrY/DbQVPDgSONGARdCCwOeKAXCDYMxmNbrYDBIYPCNwWeFYJhCFx4vBLouekrGB4YlBudJpKzBA4K1BkouBMIgvC4aRQzx/CEQV0boIqBFgWYug8DT4IuBRqAwELoUkEAIABuckkpeBAAMlBgphDFyYwDYYQeBLgWeLQJkBE4JiBTwK7CFyySDDgVPewIqBGYQrBGgNJp5CCRigwGVYInBEAKJBR4aVBHwTTCFwvN5oxVp4jCLgZiDAwVPLg4uVMYiPCAAiPCRTIyMgEkAAQFBDSVfA")) diff --git a/apps/rep/app.js b/apps/rep/app.js new file mode 100644 index 000000000..06c8889b7 --- /dev/null +++ b/apps/rep/app.js @@ -0,0 +1,269 @@ +var _a, _b, _c; +{ + var L = require("Layout"); + var storeReps = require("Storage") + .readJSON("rep.json"); + if (storeReps == null) { + E.showAlert("No reps in storage\nLoad them on with the app loader") + .then(function () { return load(); }); + throw new Error("no storage"); + } + var reps_1 = storeReps.map(function (r, i, a) { + var r2 = r; + r2.accDur = i > 0 + ? a[i - 1].accDur + r.dur + : r.dur; + return r2; + }); + var settings = (require("Storage").readJSON("rep.setting.json", true) || {}); + (_a = settings.record) !== null && _a !== void 0 ? _a : (settings.record = false); + (_b = settings.recordStopOnExit) !== null && _b !== void 0 ? _b : (settings.recordStopOnExit = false); + (_c = settings.stepMs) !== null && _c !== void 0 ? _c : (settings.stepMs = 5 * 1000); + var fontSzMain = 54; + var fontScaleRep = 2; + var fontSzRep = 20; + var fontSzRepDesc = 12; + var blue_1 = "#205af7"; + var ffStep_1 = settings.stepMs; + var state_1; + var drawInterval_1; + var lastRepIndex_1 = null; + var firstTime_1 = true; + var renderDuration = function (l) { + var lbl; + g.clearRect(l.x, l.y, l.x + l.w, l.y + l.h); + if (state_1) { + var _a = state_1.currentRepPair(), i = _a[0], repElapsed = _a[1]; + if (i !== null) { + var thisDur = reps_1[i].dur; + var remaining = thisDur - repElapsed; + lbl = msToMinSec_1(remaining); + var fract = repElapsed / thisDur; + g.setColor(blue_1) + .fillRect(l.x, l.y, l.x + fract * l.w, l.y + l.h); + } + else { + lbl = msToMinSec_1(repElapsed); + } + } + else { + lbl = "RDY"; + } + if (l.font) + g.setFont(l.font); + g.setColor(l.col || g.theme.fg) + .setFontAlign(0, 0) + .drawString(lbl, l.x + (l.w >> 1), l.y + (l.h >> 1)); + }; + var layout_1 = new L({ + type: "v", + c: [ + { + type: "h", + c: [ + { + id: "duration", + lazyBuster: 1, + type: "custom", + font: "Vector:".concat(fontSzMain), + fillx: 1, + filly: 1, + render: renderDuration, + }, + { + id: "repIdx", + type: "txt", + font: "6x8:".concat(fontScaleRep), + label: "---", + r: 1, + }, + ] + }, + { + type: "txt", + font: "Vector:".concat(fontSzRepDesc), + label: "Activity / Duration", + }, + { + id: "cur_name", + type: "txt", + font: "Vector:".concat(fontSzRep), + label: "", + col: blue_1, + fillx: 1, + }, + { + type: "txt", + font: "Vector:".concat(fontSzRepDesc), + label: "Next / Duration", + }, + { + id: "next_name", + type: "txt", + font: "Vector:".concat(fontSzRep), + label: "", + fillx: 1, + }, + { + type: "h", + c: [ + { + id: "prev", + type: "btn", + label: "<<", + fillx: 1, + cb: function () { + buzzInteraction_1(); + state_1 === null || state_1 === void 0 ? void 0 : state_1.rewind(); + drawRep_1(); + }, + }, + { + id: "play", + type: "btn", + label: "Play", + fillx: 1, + cb: function () { + buzzInteraction_1(); + if (!state_1) + state_1 = new State_1(); + state_1.toggle(); + if (state_1.paused) { + clearInterval(drawInterval_1); + drawInterval_1 = undefined; + } + else { + drawInterval_1 = setInterval(drawRep_1, 1000); + } + drawRep_1(); + }, + }, + { + id: "next", + type: "btn", + label: ">>", + fillx: 1, + cb: function () { + buzzInteraction_1(); + state_1 === null || state_1 === void 0 ? void 0 : state_1.forward(); + drawRep_1(); + }, + } + ] + } + ] + }, { lazy: true }); + var State_1 = (function () { + function State() { + this.paused = true; + this.begin = Date.now(); + this.accumulated = 0; + } + State.prototype.toggle = function () { + if (this.paused) { + this.begin = Date.now(); + } + else { + var diff = Date.now() - this.begin; + this.accumulated += diff; + } + this.paused = !this.paused; + }; + State.prototype.getElapsedTotal = function () { + return (this.paused ? 0 : Date.now() - this.begin) + this.accumulated; + }; + State.prototype.getElapsedForRep = function () { + return this.currentRepPair()[1]; + }; + State.prototype.currentRepPair = function () { + var elapsed = this.getElapsedTotal(); + var i = this.currentRepIndex(); + var repElapsed = elapsed - (i > 0 ? reps_1[i - 1].accDur : 0); + return [i, repElapsed]; + }; + State.prototype.currentRepIndex = function () { + var elapsed = this.getElapsedTotal(); + var ent; + for (var i = 0; ent = reps_1[i]; i++) + if (elapsed < ent.accDur) + return i; + return null; + }; + State.prototype.forward = function () { + this.accumulated += ffStep_1; + }; + State.prototype.rewind = function () { + this.accumulated -= ffStep_1; + }; + return State; + }()); + var repToLabel_1 = function (i, id) { + var rep = reps_1[i]; + if (rep) + layout_1["".concat(id, "_name")].label = "".concat(rep.label, " / ").concat(msToMinSec_1(rep.dur)); + else + emptyLabel_1(id); + }; + var emptyLabel_1 = function (id) { + layout_1["".concat(id, "_name")].label = " / 0m"; + }; + var pad2_1 = function (s) { return ('0' + s.toFixed(0)).slice(-2); }; + var msToMinSec_1 = function (ms) { + var sec = Math.floor(ms / 1000); + var min = Math.floor(sec / 60); + return min.toFixed(0) + ":" + pad2_1(sec % 60); + }; + var drawRep_1 = function () { + layout_1["duration"].lazyBuster ^= 1; + if (state_1) { + var i = state_1.currentRepIndex(); + if (i !== lastRepIndex_1) { + buzzNewRep_1(); + lastRepIndex_1 = i; + var repIdx = layout_1["repIdx"]; + repIdx.label = i !== null ? "Rep ".concat(i + 1) : "Done"; + layout_1.forgetLazyState(); + layout_1.clear(); + } + layout_1["play"].label = state_1.paused ? "Play" : "Pause"; + if (i !== null) { + repToLabel_1(i, "cur"); + repToLabel_1(i + 1, "next"); + } + else { + emptyLabel_1("cur"); + emptyLabel_1("next"); + } + } + layout_1.render(); + }; + var buzzInteraction_1 = function () { return Bangle.buzz(250); }; + var buzzNewRep_1 = function () { + var n = firstTime_1 ? 1 : 3; + firstTime_1 = false; + var buzz = function () { + Bangle.buzz(1000).then(function () { + if (--n <= 0) + return; + setTimeout(buzz, 250); + }); + }; + buzz(); + }; + var init = function () { + g.clear(); + drawRep_1(); + Bangle.drawWidgets(); + }; + Bangle.loadWidgets(); + if (settings.record && WIDGETS["recorder"]) { + WIDGETS["recorder"] + .setRecording(true) + .then(init); + if (settings.recordStopOnExit) + E.on('kill', function () { return WIDGETS["recorder"].setRecording(false); }); + } + else { + init(); + } +} diff --git a/apps/rep/app.png b/apps/rep/app.png new file mode 100644 index 000000000..795cd94b4 Binary files /dev/null and b/apps/rep/app.png differ diff --git a/apps/rep/app.ts b/apps/rep/app.ts new file mode 100644 index 000000000..181ccd921 --- /dev/null +++ b/apps/rep/app.ts @@ -0,0 +1,336 @@ +type RepSettings = { + record: boolean, + recordStopOnExit: boolean, + stepMs: number, +}; + +{ +const L = require("Layout"); + +type StoreRep = { + /// duration in ms + dur: number, + /// label of this rep + label: string, +}; + +type Rep = StoreRep & { + /// cumulative duration (ms) + accDur: number, +}; + +const storeReps = require("Storage") + .readJSON("rep.json") as Rep[] | undefined; + +if(storeReps == null){ + E.showAlert("No reps in storage\nLoad them on with the app loader") + .then(() => load()); + + throw new Error("no storage"); +} + +const reps = storeReps.map((r: StoreRep, i: number, a: Rep[]): Rep => { + const r2 = r as Rep; + r2.accDur = i > 0 + ? a[i-1]!.accDur + r.dur + : r.dur; + return r2; +}); + +const settings = (require("Storage").readJSON("rep.setting.json", true) || {}) as RepSettings; +settings.record ??= false; +settings.recordStopOnExit ??= false; +settings.stepMs ??= 5 * 1000; + +const fontSzMain = 54; +const fontScaleRep = 2; +const fontSzRep = 20; +const fontSzRepDesc = 12; +const blue = "#205af7"; +const ffStep = settings.stepMs; + +let state: State | undefined; +let drawInterval: IntervalId | undefined; +let lastRepIndex: number | null = null; +let firstTime = true; + +const renderDuration = (l: Layout.RenderedHierarchy) => { + let lbl; + + g.clearRect(l.x, l.y, l.x+l.w, l.y+l.h); + + if(state){ + const [i, repElapsed] = state.currentRepPair(); + + if(i !== null){ + let thisDur = reps[i]!.dur; + + const remaining = thisDur - repElapsed; + lbl = msToMinSec(remaining); + + const fract = repElapsed / thisDur; + g.setColor(blue) + .fillRect( + l.x, + l.y, + l.x + fract * l.w, + l.y + l.h + ); + }else{ + lbl = msToMinSec(repElapsed); + } + }else{ + lbl = "RDY"; + } + + if(l.font) + g.setFont(l.font); // don't chain - might be undefined + + g.setColor(l.col || g.theme.fg) + .setFontAlign(0, 0) + .drawString( + lbl, + l.x+(l.w>>1), + l.y+(l.h>>1) + ); +}; + +const layout = new L({ + type: "v", + c: [ + { + type: "h", + c: [ + { + id: "duration", + lazyBuster: 1, + type: "custom", + font: `Vector:${fontSzMain}` as FontNameWithScaleFactor, + fillx: 1, + filly: 1, + render: renderDuration, + }, + { + id: "repIdx", + type: "txt", + font: `6x8:${fontScaleRep}`, + label: "---", + r: Layout.Rotation.Deg90, + }, + ] + }, + { + type: "txt", + font: `Vector:${fontSzRepDesc}`, + label: "Activity / Duration", + }, + { + id: "cur_name", + type: "txt", + font: `Vector:${fontSzRep}`, + label: "", + col: blue, + //pad: 4, + fillx: 1, + }, + { + type: "txt", + font: `Vector:${fontSzRepDesc}`, + label: "Next / Duration", + }, + { + id: "next_name", + type: "txt", + font: `Vector:${fontSzRep}`, + label: "", + //pad: 4, + fillx: 1, + }, + { + type: "h", + c: [ + { + id: "prev", + type: "btn", + label: "<<", + fillx: 1, + cb: () => { + buzzInteraction(); + state?.rewind(); + drawRep(); + }, + }, + { + id: "play", + type: "btn", + label: "Play", + fillx: 1, + cb: () => { + buzzInteraction(); + if(!state) + state = new State(); + + state.toggle(); + + if(state.paused){ + clearInterval(drawInterval!); + drawInterval = undefined; + }else{ + drawInterval = setInterval(drawRep, 1000); + } + + drawRep(); + }, + }, + { + id: "next", + type: "btn", + label: ">>", + fillx: 1, + cb: () => { + buzzInteraction(); + state?.forward(); + drawRep(); + }, + } + ] + } + ] +}, {lazy: true}); + +class State { + paused: boolean = true; + begin: number = Date.now(); // only valid if !paused + accumulated: number = 0; + + toggle() { + if(this.paused){ + this.begin = Date.now(); + }else{ + const diff = Date.now() - this.begin; + this.accumulated += diff; + } + + this.paused = !this.paused; + } + + getElapsedTotal() { + return (this.paused ? 0 : Date.now() - this.begin) + this.accumulated; + } + + getElapsedForRep() { + return this.currentRepPair()[1]; + } + + currentRepPair(): [number | null, number] { + const elapsed = this.getElapsedTotal(); + const i = this.currentRepIndex(); + const repElapsed = elapsed - (i! > 0 ? reps[i!-1]!.accDur : 0); + + return [i, repElapsed]; + } + + currentRepIndex() { + const elapsed = this.getElapsedTotal(); + let ent; + for(let i = 0; ent = reps[i]; i++) + if(elapsed < ent.accDur) + return i; + return null; + } + + forward() { + this.accumulated += ffStep; + } + + rewind() { + this.accumulated -= ffStep; + } +} + +const repToLabel = (i: number, id: string) => { + const rep = reps[i]; + if(rep) + layout[`${id}_name`]!.label = `${rep.label} / ${msToMinSec(rep.dur)}`; + else + emptyLabel(id); +}; + +const emptyLabel = (id: string) => { + layout[`${id}_name`]!.label = " / 0m"; +}; + +const pad2 = (s: number) => ('0' + s.toFixed(0)).slice(-2); + +const msToMinSec = (ms: number) => { + const sec = Math.floor(ms / 1000); + const min = Math.floor(sec / 60); + return min.toFixed(0) + ":" + pad2(sec % 60); +}; + +const drawRep = () => { + (layout["duration"] as any).lazyBuster ^= 1; + + if(state){ + const i = state.currentRepIndex(); + + if(i !== lastRepIndex){ + buzzNewRep(); + lastRepIndex = i; + + const repIdx = layout["repIdx"]!; + repIdx.label = i !== null ? `Rep ${i+1}` : "Done"; + + // work around a bug in clearing a rotated txt(?) + layout.forgetLazyState(); + layout.clear(); + } + + layout["play"]!.label = state.paused ? "Play" : "Pause"; + + if(i !== null){ + repToLabel(i, "cur"); + repToLabel(i+1, "next"); + }else{ + emptyLabel("cur"); + emptyLabel("next"); + } + } + + layout.render(); +}; + +const buzzInteraction = () => Bangle.buzz(250); +const buzzNewRep = () => { + let n = firstTime ? 1 : 3; + firstTime = false; + const buzz = () => { + Bangle.buzz(1000).then(() => { + if (--n <= 0) + return; + setTimeout(buzz, 250); + }); + }; + buzz(); +}; + +const init = () => { + g.clear(); + drawRep(); + + Bangle.drawWidgets(); +}; + +Bangle.loadWidgets(); +if (settings.record && WIDGETS["recorder"]) { + (WIDGETS["recorder"] as RecorderWidget) + .setRecording(true) + .then(init); + + if (settings.recordStopOnExit) + E.on('kill', () => (WIDGETS["recorder"] as RecorderWidget).setRecording(false)); +} else { + init(); +} + +} diff --git a/apps/rep/interface.html b/apps/rep/interface.html new file mode 100644 index 000000000..fa137bdb2 --- /dev/null +++ b/apps/rep/interface.html @@ -0,0 +1,154 @@ + + + + + + + + + + +

Reps

+ +
+ +
+ + + + + + + + + + + +
DescriptionDuration
+ +
+ + + + + diff --git a/apps/rep/metadata.json b/apps/rep/metadata.json new file mode 100644 index 000000000..4b34175c8 --- /dev/null +++ b/apps/rep/metadata.json @@ -0,0 +1,17 @@ +{ + "id": "rep", + "name": "Rep", + "version":"0.01", + "description": "Time running reps, using a rep schedule/program uploaded via the app loader", + "icon": "app.png", + "tags": "run,running,fitness,outdoors", + "supports" : ["BANGLEJS2"], + "readme": "README.md", + "interface": "interface.html", + "storage": [ + {"name":"rep.app.js","url":"app.js"}, + {"name":"rep.settings.js","url":"settings.js"}, + {"name":"rep.img","url":"app-icon.js","evaluate":true} + ], + "data": [{"name":"rep.json"}] +} diff --git a/apps/rep/settings.js b/apps/rep/settings.js new file mode 100644 index 000000000..bfadacda1 --- /dev/null +++ b/apps/rep/settings.js @@ -0,0 +1,44 @@ +(function (back) { + var _a, _b, _c; + var SETTINGS_FILE = "rep.setting.json"; + var storage = require("Storage"); + var settings = (storage.readJSON(SETTINGS_FILE, true) || {}); + (_a = settings.record) !== null && _a !== void 0 ? _a : (settings.record = false); + (_b = settings.recordStopOnExit) !== null && _b !== void 0 ? _b : (settings.recordStopOnExit = false); + (_c = settings.stepMs) !== null && _c !== void 0 ? _c : (settings.stepMs = 5 * 1000); + var save = function () { + storage.writeJSON(SETTINGS_FILE, settings); + }; + var menu = { + "": { "title": "Rep" }, + "< Back": back, + "Fwd/back seconds": { + value: settings.stepMs / 1000, + min: 1, + max: 60, + step: 1, + format: function (v) { return "".concat(v, "s"); }, + onchange: function (v) { + settings.stepMs = v * 1000; + save(); + }, + }, + }; + if (global["WIDGETS"] && WIDGETS["recorder"]) { + menu["Record activity"] = { + value: !!settings.record, + onchange: function (v) { + settings.record = v; + save(); + } + }; + menu["Stop record on exit"] = { + value: !!settings.recordStopOnExit, + onchange: function (v) { + settings.recordStopOnExit = v; + save(); + } + }; + } + E.showMenu(menu); +}); diff --git a/apps/rep/settings.ts b/apps/rep/settings.ts new file mode 100644 index 000000000..0041b95f9 --- /dev/null +++ b/apps/rep/settings.ts @@ -0,0 +1,48 @@ +(back => { + const SETTINGS_FILE = "rep.setting.json"; + + const storage = require("Storage") + const settings = (storage.readJSON(SETTINGS_FILE, true) || {}) as RepSettings; + settings.record ??= false; + settings.recordStopOnExit ??= false; + settings.stepMs ??= 5 * 1000; + + const save = () => { + storage.writeJSON(SETTINGS_FILE, settings); + }; + + const menu: Menu = { + "": { "title": "Rep" }, + "< Back": back, + /*LANG*/"Fwd/back seconds": { + value: settings.stepMs / 1000, + min: 1, + max: 60, + step: 1, + format: (v: number) => `${v}s`, + onchange: (v: number) => { + settings.stepMs = v * 1000; + save(); + }, + }, + }; + + if (global["WIDGETS"] && WIDGETS["recorder"]) { + menu[/*LANG*/"Record activity"] = { + value: !!settings.record, + onchange: (v: boolean) => { + settings.record = v; + save(); + } + }; + menu[/*LANG*/"Stop record on exit"] = { + value: !!settings.recordStopOnExit, + onchange: (v: boolean) => { + settings.recordStopOnExit = v; + save(); + } + }; + } + + E.showMenu(menu); +}) satisfies SettingsFunc diff --git a/apps/sched/sched.js b/apps/sched/sched.js index 3deb9d1f0..bd84c3e47 100644 --- a/apps/sched/sched.js +++ b/apps/sched/sched.js @@ -102,7 +102,7 @@ function showAlarm(alarm) { date = new Date(date.getFullYear() + rp.num, date.getMonth(), alarm.od); if (date.getDate() != alarm.od) date.setDate(0); break; - default: + default: console.log(`sched: unknown repeat '${JSON.stringify(rp)}'`); break; } diff --git a/apps/sensortools/metadata.json b/apps/sensortools/metadata.json index 48b146617..bffffd090 100644 --- a/apps/sensortools/metadata.json +++ b/apps/sensortools/metadata.json @@ -14,5 +14,8 @@ {"name":"sensortools.settings.js","url":"settings.js"}, {"name":"sensortools","url":"lib.js"}, {"name":"sensortools.default.json","url":"default.json"} + ], + "data": [ + {"name":"sensortools.settings.json"} ] } diff --git a/apps/sleeplogalarm/metadata.json b/apps/sleeplogalarm/metadata.json index 30d3dcda7..fd68ce376 100644 --- a/apps/sleeplogalarm/metadata.json +++ b/apps/sleeplogalarm/metadata.json @@ -17,5 +17,6 @@ {"name": "sleeplogalarm", "url": "lib.js"}, {"name": "sleeplogalarm.settings.js", "url": "settings.js"}, {"name": "sleeplogalarm.wid.js", "url": "widget.js"} - ] + ], + "data":[{"name":"sleeplogalarm.settings.json"}] } diff --git a/apps/speedalt/metadata.json b/apps/speedalt/metadata.json index 89bfd4a57..1ecb2b562 100644 --- a/apps/speedalt/metadata.json +++ b/apps/speedalt/metadata.json @@ -15,5 +15,8 @@ {"name":"speedalt.app.js","url":"app.js"}, {"name":"speedalt.img","url":"app-icon.js","evaluate":true}, {"name":"speedalt.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"speedalt.settings.json"} ] } diff --git a/apps/spotrem/ChangeLog b/apps/spotrem/ChangeLog index 48b82b12a..56ded4e5c 100644 --- a/apps/spotrem/ChangeLog +++ b/apps/spotrem/ChangeLog @@ -6,4 +6,6 @@ 0.06: Make compatible with Fastload Utils app. 0.07: Remove just the specific listeners to not interfere with Quick Launch when fastloading. -0.08: Issue newline before GB commands (solves issue with console.log and ignored commands) \ No newline at end of file +0.08: Issue newline before GB commands (solves issue with console.log and ignored commands) +0.09: Don't send the gadgetbridge wake command twice. Once should do since we + issue newline before GB commands. diff --git a/apps/spotrem/app.js b/apps/spotrem/app.js index b3fa76f86..48274df44 100644 --- a/apps/spotrem/app.js +++ b/apps/spotrem/app.js @@ -64,7 +64,6 @@ let touchHandler = function(_, xy) { } else if ((R.x-1{ - if (e.y=0) && i { + g.reset().clearRect(R).setClipRect(R.x,R.y,R.x2,R.y2); + var a = YtoIdx(R.y); + var b = Math.min(YtoIdx(R.y2),options.c-1); + for (var i=a;i<=b;i++) + options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h}); + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); }; Bangle.setUI({ mode : "custom", back : options.back, remove : options.remove, + redraw : draw, swipe : (_,UD)=>{ pixels = 120; var dy = UD*pixels; @@ -45,13 +45,13 @@ Bangle.setUI({ rScroll = s.scroll &~1; dy = oldScroll-rScroll; if (!dy || options.c<=3) return; //options.c<=3 should maybe be dynamic, so 3 would be replaced by a variable dependent on R=Bangle.appRect. It's here so we don't try to scroll if all entries fit in the app rectangle. - g.reset().setClipRect(R.x,R.y,R.x2,R.y2); - g.scroll(0,dy); + g.reset().setClipRect(R.x,R.y,R.x2,R.y2).scroll(0,dy); var d = UD*pixels; if (d < 0) { - g.setClipRect(R.x,R.y2-(1-d),R.x2,R.y2); - let i = YtoIdx(R.y2-(1-d)); - let y = idxToY(i); + let y = Math.max(R.y2-(1-d), R.y); + g.setClipRect(R.x,y,R.x2,R.y2); + let i = YtoIdx(y); + y = idxToY(i); //print(i, options.c, options.c-i); //debugging info while (y < R.y2 - (options.h*((options.c-i)<=0)) ) { //- (options.h*((options.c-i)<=0)) makes sure we don't go beyond the menu entries in the menu object "options". This has to do with "dy = s.scroll - menuScrollMax-8" above. options.draw(i, {x:R.x,y:y,w:R.w,h:options.h}); @@ -59,10 +59,10 @@ Bangle.setUI({ y += options.h; } } else { // d>0 - g.setClipRect(R.x,R.y,R.x2,R.y+d); - let i = YtoIdx(R.y+d); - let y = idxToY(i); - //print(i, options.c, options.c-i); //debugging info + let y = Math.min(R.y+d, R.y2); + g.setClipRect(R.x,R.y,R.x2,y); + let i = YtoIdx(y); + y = idxToY(i); while (y > R.y-options.h) { options.draw(i, {x:R.x,y:y,w:R.w,h:options.h}); y -= options.h; @@ -70,7 +70,15 @@ Bangle.setUI({ } } g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); - }, touch : touchHandler + }, touch : (_,e)=>{ + if (e.y=0) && i { - g.reset().clearRect(R.x,R.y,R.x2,R.y2); - g.setClipRect(R.x,R.y,R.x2,R.y2); - var a = YtoIdx(R.y); - var b = Math.min(YtoIdx(R.y2),options.c-1); - for (var i=a;i<=b;i++) - options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h}); - g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); -}, drawItem : i => { - var y = idxToY(i); - g.reset().setClipRect(R.x,Math.max(y,R.y),R.x2,Math.min(y+options.h,R.y2)); - options.draw(i, {x:R.x,y:y,w:R.w,h:options.h}); - g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); - }, isActive : () => Bangle.touchHandler == touchHandler - }; + draw : draw, drawItem : i => { + var y = idxToY(i); + g.reset().setClipRect(R.x,Math.max(y,R.y),R.x2,Math.min(y+options.h,R.y2)); + options.draw(i, {x:R.x,y:y,w:R.w,h:options.h}); + g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); + }, isActive : () => Bangle.uiRedraw == draw +}; var rScroll = s.scroll&~1; // rendered menu scroll (we only shift by 2 because of dither) s.draw(); // draw the full scroller g.flip(); // force an update now to make this snappier diff --git a/apps/swscroll/metadata.json b/apps/swscroll/metadata.json index 3258b9b21..3c0717d14 100644 --- a/apps/swscroll/metadata.json +++ b/apps/swscroll/metadata.json @@ -1,7 +1,7 @@ { "id": "swscroll", "name": "Swipe menus", - "version": "0.04", + "version": "0.05", "description": "Replace built in E.showScroller to act on swipe instead of drag. Navigate menus in discrete steps instead of a continuous motion.", "readme": "README.md", "icon": "app.png", diff --git a/apps/timecal/metadata.json b/apps/timecal/metadata.json index 287dce0ae..9175d92a3 100644 --- a/apps/timecal/metadata.json +++ b/apps/timecal/metadata.json @@ -12,5 +12,6 @@ "storage": [ {"name":"timecal.app.js","url":"timecal.app.js"}, {"name":"timecal.settings.js","url":"timecal.settings.js"} - ] + ], + "data":[{"name":"timecal.settings.json"}] } diff --git a/apps/torch/metadata.json b/apps/torch/metadata.json index 6837dc904..414222587 100644 --- a/apps/torch/metadata.json +++ b/apps/torch/metadata.json @@ -13,5 +13,8 @@ {"name":"torch.wid.js","url":"widget.js","supports": ["BANGLEJS"]}, {"name":"torch.img","url":"app-icon.js","evaluate":true}, {"name":"torch.settings.js","url":"settings.js"} + ], + "data": [ + {"name":"torch.settings.json"} ] } diff --git a/apps/waypoint_editor/ChangeLog b/apps/waypoint_editor/ChangeLog index 5560f00bc..0ec5d2df8 100644 --- a/apps/waypoint_editor/ChangeLog +++ b/apps/waypoint_editor/ChangeLog @@ -1 +1,2 @@ 0.01: New App! +0.02: Display waypoint name instead of its index in remove menu and fix icon diff --git a/apps/waypoint_editor/app-icon.js b/apps/waypoint_editor/app-icon.js index 49232b838..6ab351afa 100644 --- a/apps/waypoint_editor/app-icon.js +++ b/apps/waypoint_editor/app-icon.js @@ -1 +1 @@ -require("heatshrink").decompress(atob("mEwwJC/AH4A/AH4AgA==")) +require("heatshrink").decompress(atob("mEwxH+AH4A/AA0QF1wABF94xrFwgvV63W5/PF1AsBAAQvBAAQviFggvHGRXWFwvWFigvKGQgPCFwwvMFxQvL54PDF9wuHF8wuIF7/OAYguFF7mBrtdFQWAvWA5+qvV6FxIvXrwfCGAQqBvXP0guLF6XV1awCmggCmgvB1Wk1QEBRpQvTJ4OAAgM6F4oAEFwulF66zBvXV62sMAM0M4IuKOYOlwGrF6nP0oXD54tGRpDLCO4TvXDwIAHXhB3BSQYvfdZPVSQIvsUILWBF8xeEX4OkF8wQFd7mlDgPP52q5wvDAYISF1elBAYvWwBNB54DBGYIAEO5YvSFAInB1asBQAQzBF5HQxGsF64rBvWlX4fO0nOSQgvD6E0TANeL65XCd4ySFCYWCe4ZhDF6GIxHQ6vVGgQAESQfOTwQvZnQWBmgXDF4qSC5+kGYOr62sR4U0R6WsI4eCF5AzETwYYBwWC6AvlX4gAIR553DR5Ivhd4OCFwYvpAAwv/F7wMBF6ouLF5APHF54sMF44SNF5IsPF4gUSGQQXVAH4A/AH4A/ADY")) diff --git a/apps/waypoint_editor/app.js b/apps/waypoint_editor/app.js index 34b3d8ef4..48a956d82 100644 --- a/apps/waypoint_editor/app.js +++ b/apps/waypoint_editor/app.js @@ -159,26 +159,28 @@ function removeCard() { "< Back" : mainMenu }; if (Object.keys(wp).length==0) Object.assign(menu, {"NO CARDS":""}); - else for (let c in wp) { - let card=c; - menu[c]=()=>{ - E.showMenu(); - var confirmRemove = new Layout ( - {type:"v", c: [ - {type:"txt", font:"15%", pad:1, fillx:1, filly:1, label:"Delete"}, - {type:"txt", font:"15%", pad:1, fillx:1, filly:1, label:card+"?"}, - {type:"h", c: [ - {type:"btn", font:"15%", pad:1, fillx:1, filly:1, label: "YES", cb:l=>{ - delete wp[card]; - writeWP(); - mainMenu(); - }}, - {type:"btn", font:"15%", pad:1, fillx:1, filly:1, label: " NO", cb:l=>{mainMenu();}} - ]} - ], lazy:true}); - g.clear(); - confirmRemove.render(); - }; + else { + wp.forEach((val, card) => { + const name = wp[card].name; + menu[name]=()=>{ + E.showMenu(); + var confirmRemove = new Layout ( + {type:"v", c: [ + {type:"txt", font:"15%", pad:1, fillx:1, filly:1, label:"Delete"}, + {type:"txt", font:"15%", pad:1, fillx:1, filly:1, label:name}, + {type:"h", c: [ + {type:"btn", font:"15%", pad:1, fillx:1, filly:1, label: "YES", cb:l=>{ + wp.splice(card, 1); + writeWP(); + mainMenu(); + }}, + {type:"btn", font:"15%", pad:1, fillx:1, filly:1, label: " NO", cb:l=>{mainMenu();}} + ]} + ], lazy:true}); + g.clear(); + confirmRemove.render(); + }; + }); } E.showMenu(menu); } diff --git a/apps/waypoint_editor/metadata.json b/apps/waypoint_editor/metadata.json index f48721732..12ff6e095 100644 --- a/apps/waypoint_editor/metadata.json +++ b/apps/waypoint_editor/metadata.json @@ -1,6 +1,6 @@ { "id": "waypoint_editor", "name": "Waypoint editor", - "version":"0.01", + "version":"0.02", "description": "Allows editing waypoints on device", "icon": "app.png", "readme": "README.md", diff --git a/apps/widpedom/metadata.json b/apps/widpedom/metadata.json index f49d3ba5b..480519a29 100644 --- a/apps/widpedom/metadata.json +++ b/apps/widpedom/metadata.json @@ -10,5 +10,6 @@ "storage": [ {"name":"widpedom.wid.js","url":"widget.js"}, {"name":"widpedom.settings.js","url":"settings.js"} - ] + ], + "data":[{"name":"wpedom.json"}] } diff --git a/apps/widswatchbeats/metadata.json b/apps/widswatchbeats/metadata.json new file mode 100644 index 000000000..5c8229966 --- /dev/null +++ b/apps/widswatchbeats/metadata.json @@ -0,0 +1,13 @@ +{ + "id": "widswatchbeats", + "name": "Swatch Internet Time Widget", + "icon": "widget-icon.png", + "type": "widget", + "version": "0.01", + "description": "Displays the current .beat (e.g. @500 for midday)", + "tags": "widget,time,swatch,internet,beat,.beat,clock", + "supports": ["BANGLEJS","BANGLEJS2"], + "storage": [ + {"name": "widswatchbeats.wid.js","url": "widget.js"} + ] +} diff --git a/apps/widswatchbeats/widget-icon.png b/apps/widswatchbeats/widget-icon.png new file mode 100644 index 000000000..726a57825 Binary files /dev/null and b/apps/widswatchbeats/widget-icon.png differ diff --git a/apps/widswatchbeats/widget.js b/apps/widswatchbeats/widget.js new file mode 100644 index 000000000..7f2427c74 --- /dev/null +++ b/apps/widswatchbeats/widget.js @@ -0,0 +1,41 @@ +(function() { + const WIDTH = 50; + const SEC_PER_BEAT = 86.4; + + let drawTimeout; + + function getSecondsSinceMidnight() { + const now = new Date(); + return now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds(); + } + + function queueDraw() { + if (drawTimeout) clearTimeout(drawTimeout); + const nextSecond = SEC_PER_BEAT - (getSecondsSinceMidnight() % SEC_PER_BEAT); + drawTimeout = setTimeout(function() { + drawTimeout = undefined; + WIDGETS.widswatchbeats.draw(); + }, nextSecond * 1000 + 1); // Add one ms to ensure we're past the beat + } + + function draw() { + const now = new Date(); + const seconds = now.getHours() * 3600 + now.getMinutes() * 60 + now.getSeconds(); + const beats = Math.floor(seconds / SEC_PER_BEAT); + const beatsString = '@' + beats.toString().padStart(3, '0'); + + g.reset(); + g.setFontAlign(0, 0); + g.clearRect(this.x, this.y, this.x + WIDTH, this.y+22); + g.setFont("6x8", 2); + g.drawString(beatsString, this.x+WIDTH/2, this.y+12); + queueDraw(); + } + + WIDGETS.widswatchbeats = { + area: "tl", + width: WIDTH, + draw + }; + +})(); diff --git a/apps/wohrm/metadata.json b/apps/wohrm/metadata.json index 1e2ac2cb0..a5411c113 100644 --- a/apps/wohrm/metadata.json +++ b/apps/wohrm/metadata.json @@ -14,5 +14,6 @@ {"name":"wohrm.app.js","url":"app.js"}, {"name":"wohrm.settings.js","url":"settings.js"}, {"name":"wohrm.img","url":"app-icon.js","evaluate":true} - ] + ], + "data":[{"name":"wohrm.setting.json"}] } diff --git a/bin/sanitycheck.js b/bin/sanitycheck.js index ecba1876e..d5c755443 100755 --- a/bin/sanitycheck.js +++ b/bin/sanitycheck.js @@ -92,9 +92,12 @@ const INTERNAL_FILES_IN_APP_TYPE = { // list of app types and files they SHOULD }; /* These are warnings we know about but don't want in our output */ var KNOWN_WARNINGS = [ -"App gpsrec data file wildcard .gpsrc? does not include app ID", -"App owmweather data file weather.json is also listed as data file for app weather", + "App gpsrec data file wildcard .gpsrc? does not include app ID", + "App owmweather data file weather.json is also listed as data file for app weather", "App messagegui storage file messagegui is also listed as storage file for app messagelist", + "App carcrazy has a setting file but no corresponding data entry (add `\"data\":[{\"name\":\"carcrazy.settings.json\"}]`)", + "App loadingscreen has a setting file but no corresponding data entry (add `\"data\":[{\"name\":\"loadingscreen.settings.json\"}]`)", + "App trex has a setting file but no corresponding data entry (add `\"data\":[{\"name\":\"trex.settings.json\"}]`)", ]; function globToRegex(pattern) { @@ -116,7 +119,7 @@ apps.forEach((app,appIdx) => { if (!app.id) ERROR(`App ${appIdx} has no id`); var appDirRelative = APPSDIR_RELATIVE+app.id+"/"; var appDir = APPSDIR+app.id+"/"; - var metadataFile = appDirRelative+"metadata.json"; + var metadataFile = appDirRelative+"metadata.json"; if (existingApps.includes(app.id)) ERROR(`Duplicate app '${app.id}'`, {file:metadataFile}); existingApps.push(app.id); //console.log(`Checking ${app.id}...`); @@ -164,11 +167,11 @@ apps.forEach((app,appIdx) => { }); } if (app.readme) { - if (!fs.existsSync(appDir+app.readme)) + if (!fs.existsSync(appDir+app.readme)) ERROR(`App ${app.id} README file doesn't exist`, {file:metadataFile}); } else { let readme = fs.readdirSync(appDir).find(f => f.toLowerCase().includes("readme")); - if (readme) + if (readme) ERROR(`App ${app.id} has a README in the directory (${readme}) but it's not linked`, {file:metadataFile}); } if (app.custom && !fs.existsSync(appDir+app.custom)) ERROR(`App ${app.id} custom HTML doesn't exist`, {file:metadataFile}); @@ -253,6 +256,10 @@ apps.forEach((app,appIdx) => { if (a>=0 && b>=0 && a !d.name || !d.name.endsWith(".json")))) { + WARN(`App ${app.id} has a setting file but no corresponding data entry (add \`"data":[{"name":"${app.id}.settings.json"}]\`)`, {file:appDirRelative+file.url}); + } } for (const key in file) { if (!STORAGE_KEYS.includes(key)) ERROR(`App ${app.id} file ${file.name} has unknown key ${key}`, {file:appDirRelative+file.url}); diff --git a/core b/core index 127c90aaa..e521afd72 160000 --- a/core +++ b/core @@ -1 +1 @@ -Subproject commit 127c90aaa9e3d23f8853807e1ad17451a37dc3c1 +Subproject commit e521afd722c46689007e62fdb5e370991f249823 diff --git a/modules/Layout.md b/modules/Layout.md index 95bf116dc..96790a41a 100644 --- a/modules/Layout.md +++ b/modules/Layout.md @@ -30,7 +30,7 @@ g.clear(); layout.render(); ``` -`layoutObject` has: +`layoutObject` (first argument) has: - A `type` field of: - `undefined` - blank, can be used for padding @@ -53,7 +53,12 @@ layout.render(); - A `pad` integer field to set pixels padding - A `fillx` int to choose if the object should fill available space in x. 0=no, 1=yes, 2=2x more space - A `filly` int to choose if the object should fill available space in y. 0=no, 1=yes, 2=2x more space -- `width` and `height` fields to optionally specify minimum size options is an object containing: +- `width` and `height` fields to optionally specify minimum size + + + `options` (second argument) is an object containing: + + - `lazy` - a boolean specifying whether to enable automatic lazy rendering - `btns` - array of objects containing: - `label` - the text on the button diff --git a/modules/date_utils.js b/modules/date_utils.js index 7239d4f1f..76ff0a393 100644 --- a/modules/date_utils.js +++ b/modules/date_utils.js @@ -8,7 +8,7 @@ // - 0/undefined --> Sunday // - 1 --> Monday // but you can start the week from any day if you need it. -// +// // Some functions have an "abbreviated" parameter. // It supports the following 3 values: // - 0/undefined --> get the full value, without abbreviation (eg.: "Monday", "January", etc.) @@ -22,12 +22,12 @@ * @returns The localized name of the i-th day of the week */ exports.dow = (i, abbreviated) => { - var dow = require("locale").dow(new Date(((i || 0) + 3.5) * 86400000), abbreviated).slice(0, (abbreviated == 2) ? 1 : 100); + var dow = require("locale").dow({getDay:()=>(i|0)%7}, abbreviated).slice(0, (abbreviated == 2) ? 1 : 100); return abbreviated == 2 ? dow.toUpperCase() : dow; } /** - * @param {int} firstDayOfWeek 0/undefined -> Sunday, + * @param {int} firstDayOfWeek 0/undefined -> Sunday, * 1 -> Monday * @param {int} abbreviated * @returns All 7 days of the week (localized) as an array @@ -47,7 +47,7 @@ exports.dows = (firstDayOfWeek, abbreviated) => { * @returns The localized name of the i-th month */ exports.month = (i, abbreviated) => { - var month = require("locale").month(new Date((i - 0.5) * 2628000000), abbreviated).slice(0, (abbreviated == 2) ? 1 : 100); + var month = require("locale").month({getMonth:()=>(11+(i|0))%12}, abbreviated).slice(0, (abbreviated == 2) ? 1 : 100); return abbreviated == 2 ? month.toUpperCase() : month; } @@ -58,8 +58,7 @@ exports.month = (i, abbreviated) => { exports.months = (abbreviated) => { var months = []; var locale = require("locale"); - for (var i = 1; i <= 12; i++) { - months.push(locale.month(new Date((i - 0.5) * 2628000000), abbreviated).slice(0, (abbreviated == 2) ? 1 : 100)); - } + for (var i = 0; i < 12; i++) + months.push(locale.month({getMonth:()=>i}, abbreviated).slice(0, (abbreviated == 2) ? 1 : 100)); return abbreviated == 2 ? months.map(month => month.toUpperCase()) : months; }; diff --git a/typescript/types/layout.d.ts b/typescript/types/layout.d.ts index 8c5706d0b..167ede29d 100644 --- a/typescript/types/layout.d.ts +++ b/typescript/types/layout.d.ts @@ -1,6 +1,6 @@ type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; -type ExtractIds = +type ExtractIds = [Depth] extends [never] ? never : (T extends { id: infer Id extends string } @@ -8,23 +8,23 @@ type ExtractIds = : never) | ( - T extends { c: Array } + T extends { c: Array } ? ExtractIds : never ); -declare module Layout_ { +declare module Layout { type Layouter = ExtractIds & { // these actually change T - render(l?: T): void; - layout(l: T): void; + render(l?: Hierarchy): void; + layout(l: Hierarchy): void; - debug(l?: T, c?: ColorResolvable): void; + debug(l?: Hierarchy, c?: ColorResolvable): void; update(): void; // changes layoutObject into a RenderedHierarchy - clear(obj?: T): void; + clear(obj?: Hierarchy): void; forgetLazyState(): void; @@ -51,7 +51,7 @@ declare module Layout_ { type Image = string; - type Fill = 0 | 1 | 2; // 0=no, 1=yes, 2=2x more space + type Fill = number; // fill a proportion of space, relative to sibling `filly`s type RenderedHierarchy = Hierarchy & { @@ -78,6 +78,10 @@ declare module Layout_ { filly?: Fill, width?: number, height?: number, + + // technically only on children of a h/v + halign?: Align, // children of a v + valign?: Align, // children of a h } & ( { r?: number, // 0: 0°, 1: 90°, 2: 180°, 3: 270°. @@ -86,17 +90,28 @@ declare module Layout_ { } ); - type Align = -1 | 0 | 1; + const enum Align { + Left = -1, + Top = -1, + Center = 0, + Right = 1, + Bottom = -1, + } + + const enum Rotation { + None = 0, + Deg90 = 1, + Deg180 = 2, + Deg270 = 3, + } type HierarchyParts = { type: "v", c: Hierarchy[], - halign?: Align, } | { type: "h" c: Hierarchy[], - valign?: Align, } | { type: "txt", label: string, @@ -108,18 +123,23 @@ declare module Layout_ { type: "btn", src: Image, cb: () => void, + r?: Rotation, + btnBorder?: ColorResolvable, } | { type: "btn", cb: () => void, label: string, font?: FontNameWithScaleFactor, scale?: number, + r?: Rotation, + btnBorder?: ColorResolvable, } ) | { type: "img", src: Image | (() => Image), + r?: Rotation, } | { type: "custom", - render: (h: Hierarchy) => void, + render: (h: RenderedHierarchy) => void, }; } diff --git a/typescript/types/modules.d.ts b/typescript/types/modules.d.ts index 5c65548fc..ad3612117 100644 --- a/typescript/types/modules.d.ts +++ b/typescript/types/modules.d.ts @@ -4,4 +4,4 @@ declare function require(moduleName: "sched"): typeof Sched; declare function require(moduleName: "ClockFace"): typeof ClockFace_.ClockFace; declare function require(moduleName: "clock_info"): typeof ClockInfo; -declare function require(moduleName: "Layout"): typeof Layout_.Layout; +declare function require(moduleName: "Layout"): typeof Layout.Layout;