diff --git a/.eslintignore b/.eslintignore index 1e3abd9ff..a82960313 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,6 +1,8 @@ # Needs to be ignored because it uses ESM export/import apps/gipy/pkg/gps.js +apps/gipy/pkg/gps.d.ts +apps/gipy/pkg/gps_bg.wasm.d.ts # Needs to be ignored because it includes broken JS apps/health/chart.min.js diff --git a/apps/aviatorclk/ChangeLog b/apps/aviatorclk/ChangeLog index 929ee8387..c086f765a 100644 --- a/apps/aviatorclk/ChangeLog +++ b/apps/aviatorclk/ChangeLog @@ -1,2 +1,7 @@ 1.00: initial release 1.01: added tap event to scroll METAR and toggle seconds display +1.02: continue showing METAR during AVWX updates (show update status next to time) + re-try GPS fix if it takes too long + bug fix + don't attempt to update METAR if Bluetooth is NOT connected + toggle seconds display on front double-taps (if un-locked) to avoid accidential enabling diff --git a/apps/aviatorclk/aviatorclk.app.js b/apps/aviatorclk/aviatorclk.app.js index 33d671bc7..cee83813f 100644 --- a/apps/aviatorclk/aviatorclk.app.js +++ b/apps/aviatorclk/aviatorclk.app.js @@ -19,6 +19,7 @@ const APP_NAME = 'aviatorclk'; const horizontalCenter = g.getWidth()/2; const mainTimeHeight = 38; const secondaryFontHeight = 22; +require("Font8x16").add(Graphics); // tertiary font const dateColour = ( g.theme.dark ? COLOUR_YELLOW : COLOUR_BLUE ); const UTCColour = ( g.theme.dark ? COLOUR_LIGHT_CYAN : COLOUR_DARK_CYAN ); const separatorColour = ( g.theme.dark ? COLOUR_LIGHT_GREY : COLOUR_DARK_GREY ); @@ -37,6 +38,7 @@ var settings = Object.assign({ var drawTimeout; var secondsInterval; var avwxTimeout; +var gpsTimeout; var AVWXrequest; var METAR = ''; @@ -92,16 +94,51 @@ function drawAVWX() { if (! avwxTimeout) { avwxTimeout = setTimeout(updateAVWX, 5 * 60000); } } +// show AVWX update status +function showUpdateAVWXstatus(status) { + let y = Bangle.appRect.y + 10; + g.setBgColor(g.theme.bg); + g.clearRect(0, y, horizontalCenter - 54, y + 16); + if (status) { + g.setFontAlign(0, -1).setFont("8x16").setColor( g.theme.dark ? COLOUR_ORANGE : COLOUR_DARK_YELLOW ); + g.drawString(status, horizontalCenter - 71, y, true); + } +} + +// re-try if the GPS doesn't return a fix in time +function GPStookTooLong() { + Bangle.setGPSPower(false, APP_NAME); + if (gpsTimeout) clearTimeout(gpsTimeout); + gpsTimeout = undefined; + + showUpdateAVWXstatus('X'); + + if (! avwxTimeout) { avwxTimeout = setTimeout(updateAVWX, 5 * 60000); } +} + // update the METAR info function updateAVWX() { if (avwxTimeout) clearTimeout(avwxTimeout); avwxTimeout = undefined; + if (gpsTimeout) clearTimeout(gpsTimeout); + gpsTimeout = undefined; - METAR = '\nGetting GPS fix'; - METARlinesCount = 0; METARscollLines = 0; - METARts = undefined; + if (! NRF.getSecurityStatus().connected) { + // if Bluetooth is NOT connected, try again in 5min + showUpdateAVWXstatus('X'); + avwxTimeout = setTimeout(updateAVWX, 5 * 60000); + return; + } + + showUpdateAVWXstatus('GPS'); + if (! METAR) { + METAR = '\nUpdating METAR'; + METARlinesCount = 0; METARscollLines = 0; + METARts = undefined; + } drawAVWX(); + gpsTimeout = setTimeout(GPStookTooLong, 30 * 60000); Bangle.setGPSPower(true, APP_NAME); Bangle.on('GPS', fix => { // prevent multiple, simultaneous requests @@ -109,12 +146,18 @@ function updateAVWX() { if ('fix' in fix && fix.fix != 0 && fix.satellites >= 4) { Bangle.setGPSPower(false, APP_NAME); + if (gpsTimeout) clearTimeout(gpsTimeout); + gpsTimeout = undefined; + let lat = fix.lat; let lon = fix.lon; - METAR = '\nRequesting METAR'; - METARlinesCount = 0; METARscollLines = 0; - METARts = undefined; + showUpdateAVWXstatus('AVWX'); + if (! METAR) { + METAR = '\nUpdating METAR'; + METARlinesCount = 0; METARscollLines = 0; + METARts = undefined; + } drawAVWX(); // get latest METAR from nearest airport (via AVWX API) @@ -146,6 +189,7 @@ function updateAVWX() { METARts = undefined; } + showUpdateAVWXstatus(''); drawAVWX(); AVWXrequest = undefined; @@ -155,6 +199,7 @@ function updateAVWX() { METAR = 'ERR: ' + error; METARlinesCount = 0; METARscollLines = 0; METARts = undefined; + showUpdateAVWXstatus(''); drawAVWX(); AVWXrequest = undefined; }); @@ -268,10 +313,10 @@ Bangle.on('tap', data => { case 'bottom': scrollAVWX(1); break; - case 'left': - case 'right': - // toggle seconds display on double taps left or right - if (data.double) { + case 'front': + // toggle seconds display on double tap on front/watch-face + // (if watch is un-locked) + if (data.double && ! Bangle.isLocked()) { if (settings.showSeconds) { clearInterval(secondsInterval); let y = Bangle.appRect.y + mainTimeHeight - 3; @@ -295,7 +340,7 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); // draw static separator line -y = Bangle.appRect.y + mainTimeHeight + secondaryFontHeight; +let y = Bangle.appRect.y + mainTimeHeight + secondaryFontHeight; g.setColor(separatorColour); g.drawLine(0, y, g.getWidth(), y); diff --git a/apps/aviatorclk/metadata.json b/apps/aviatorclk/metadata.json index 9d2b0beef..54f539c1e 100644 --- a/apps/aviatorclk/metadata.json +++ b/apps/aviatorclk/metadata.json @@ -2,7 +2,7 @@ "id": "aviatorclk", "name": "Aviator Clock", "shortName":"AV8R Clock", - "version":"1.01", + "version":"1.02", "description": "A clock for aviators, with local time and UTC - and the latest METAR for the nearest airport", "icon": "aviatorclk.png", "screenshots": [{ "url": "screenshot.png" }, { "url": "screenshot2.png" }], diff --git a/apps/boxclk/ChangeLog b/apps/boxclk/ChangeLog index b78eba44c..591d880b8 100644 --- a/apps/boxclk/ChangeLog +++ b/apps/boxclk/ChangeLog @@ -3,4 +3,13 @@ 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 -0.06 Added clockbackground app functionality +0.06: Added clockbackground app functionality +0.07: Allow custom backgrounds per boxclk config and from the clockbg module +0.08: Improves performance, responsiveness, and bug fixes +- [+] Added box size caching to reduce calculations +- [+] Improved step count with real-time updates +- [+] Improved battery level update logic to reduce unnecessary refreshes +- [+] Fixed optional seconds not displaying in time +- [+] Fixed drag handler by adding E.stopEventPropagation() +- [+] General code optimization and cleanup +0.09: Revised event handler code \ No newline at end of file diff --git a/apps/boxclk/app.js b/apps/boxclk/app.js index 548062349..87bbcef77 100644 --- a/apps/boxclk/app.js +++ b/apps/boxclk/app.js @@ -1,58 +1,123 @@ { - /** - * --------------------------------------------------------------- - * 1. Module dependencies and initial configurations - * --------------------------------------------------------------- - */ - + // 1. Module dependencies and initial configurations let background = require("clockbg"); let storage = require("Storage"); let locale = require("locale"); let widgets = require("widget_utils"); - let date = new Date(); + let bgImage; let configNumber = (storage.readJSON("boxclk.json", 1) || {}).selectedConfig || 0; let fileName = 'boxclk' + (configNumber > 0 ? `-${configNumber}` : '') + '.json'; - // Add a condition to check if the file exists, if it does not, default to 'boxclk.json' if (!storage.read(fileName)) { fileName = 'boxclk.json'; } let boxesConfig = storage.readJSON(fileName, 1) || {}; let boxes = {}; - let boxPos = {}; - let isDragging = {}; - let wasDragging = {}; + let isDragging = false; let doubleTapTimer = null; let g_setColor; let saveIcon = require("heatshrink").decompress(atob("mEwwkEogA/AHdP/4AK+gWVDBQWNAAIuVGBAIB+UQdhMfGBAHBCxUAgIXHIwPyCxQwEJAgXB+MAl/zBwQGBn8ggQjBGAQXG+EA/4XI/8gBIQXTGAMPC6n/C6HzkREBC6YACC6QAFC57aHCYIXOOgLsEn4XPABIX/C6vykQAEl6/WgCQBC5imFAAT2BC5gCBI4oUCC5x0IC/4X/C4K8Bl4XJ+TCCC4wKBABkvC4tEEoMQCxcBB4IWEC4XyDBUBFwIXGJAIAOIwowDABoWGGB4uHDBwWJAH4AzA")); - /** - * --------------------------------------------------------------- - * 2. Graphical and visual configurations - * --------------------------------------------------------------- - */ - + // 2. Graphical and visual configurations let w = g.getWidth(); let h = g.getHeight(); - let totalWidth, totalHeight; let drawTimeout; - /** - * --------------------------------------------------------------- - * 3. Touchscreen Handlers - * --------------------------------------------------------------- - */ - - let touchHandler; - let dragHandler; - let movementDistance = 0; - - /** - * --------------------------------------------------------------- - * 4. Font loading function - * --------------------------------------------------------------- - */ + // 3. Event handlers + let touchHandler = function(zone, e) { + let boxTouched = false; + let touchedBox = null; + + for (let boxKey in boxes) { + if (touchInText(e, boxes[boxKey])) { + touchedBox = boxKey; + boxTouched = true; + break; + } + } + + if (boxTouched) { + // Toggle the selected state of the touched box + boxes[touchedBox].selected = !boxes[touchedBox].selected; + + // Update isDragging based on whether any box is selected + isDragging = Object.values(boxes).some(box => box.selected); + + if (isDragging) { + widgets.hide(); + } else { + deselectAllBoxes(); + } + } else { + // If tapped outside any box, deselect all boxes + deselectAllBoxes(); + } + + // Always redraw after a touch event + draw(); + + // Handle double tap for saving + if (!boxTouched && !isDragging) { + if (doubleTapTimer) { + clearTimeout(doubleTapTimer); + doubleTapTimer = null; + for (let boxKey in boxes) { + boxesConfig[boxKey].boxPos.x = (boxes[boxKey].pos.x / w).toFixed(3); + boxesConfig[boxKey].boxPos.y = (boxes[boxKey].pos.y / h).toFixed(3); + } + storage.write(fileName, JSON.stringify(boxesConfig)); + displaySaveIcon(); + return; + } + + doubleTapTimer = setTimeout(() => { + doubleTapTimer = null; + }, 500); + } + }; + + let dragHandler = function(e) { + if (!isDragging) return; + + // Stop propagation of the drag event to prevent other handlers + E.stopEventPropagation(); + + for (let key in boxes) { + if (boxes[key].selected) { + let boxItem = boxes[key]; + calcBoxSize(boxItem); + let newX = boxItem.pos.x + e.dx; + let newY = boxItem.pos.y + e.dy; + + if (newX - boxItem.cachedSize.width / 2 >= 0 && + newX + boxItem.cachedSize.width / 2 <= w && + newY - boxItem.cachedSize.height / 2 >= 0 && + newY + boxItem.cachedSize.height / 2 <= h) { + boxItem.pos.x = newX; + boxItem.pos.y = newY; + } + } + } + + draw(); + }; + + let stepHandler = function(up) { + if (boxes.step && !isDragging) { + boxes.step.string = formatStr(boxes.step, Bangle.getHealthStatus("day").steps); + boxes.step.cachedSize = null; + draw(); + } + }; + + let lockHandler = function(isLocked) { + if (isLocked) { + deselectAllBoxes(); + draw(); + } + }; + // 4. Font loading function let loadCustomFont = function() { Graphics.prototype.setFontBrunoAce = function() { // Actual height 23 (24 - 2) @@ -60,42 +125,43 @@ E.toString(require('heatshrink').decompress(atob('ABMHwADBh4DKg4bKgIPDAYUfAYV/AYX/AQMD/gmC+ADBn/AByE/GIU8AYUwLxcfAYX/8AnB//4JIP/FgMP4F+CQQBBjwJBFYRbBAd43DHoJpBh/g/xPEK4ZfDgEEORKDDAY8////wADLfZrTCgITBnhEBAYJMBAYMPw4DCM4QDjhwDCjwDBn0+AYMf/gDBh/4AYMH+ADBLpc4ToK/NGYZfnAYcfL4U/x5fBW4LvB/7vC+LvBgHAsBfIn76Cn4WBcYQDFEgJ+CQQYDyH4L/BAZbHLNYjjCAZc8ngDunycBZ4KkBa4KwBnEHY4UB+BfMgf/ZgMH/4XBc4cf4F/gE+ZgRjwAYcfj5jBM4U4M4RQBM4UA8BjIngDFEYJ8BAYUDAYQvCM4ZxBC4V+AYQvBnkBQ4M8gabBJQPAI4WAAYM/GYQaBAYJKCnqyCn5OCn4aBAYIaBAYJPCU4IABnBhIuDXCFAMD+Z/BY4IDBQwOPwEfv6TDAYUPAcwrDAYQ7BAYY/BI4cD8bLCK4RfEAA0BRYTeDcwIrFn0Pw43Bg4DugYDBjxBBU4SvDMYMH/5QBgP/LAQAP8EHN4UPwADHB4YAHA'))), 46, atob("CBEdChgYGhgaGBsaCQ=="), - 32|65536 + 32 | 65536 ); }; }; - /** - * --------------------------------------------------------------- - * 5. Initial settings of boxes and their positions - * --------------------------------------------------------------- + // 5. Initial settings of boxes and their positions + let isBool = (val, defaultVal) => val !== undefined ? Boolean(val) : defaultVal; + + for (let key in boxesConfig) { + if (key === 'bg' && boxesConfig[key].img) { + bgImage = storage.read(boxesConfig[key].img); + } else if (key !== 'selectedConfig') { + boxes[key] = Object.assign({}, boxesConfig[key]); + // Set default values for short, shortMonth, and disableSuffix + boxes[key].short = isBool(boxes[key].short, true); + boxes[key].shortMonth = isBool(boxes[key].shortMonth, true); + boxes[key].disableSuffix = isBool(boxes[key].disableSuffix, false); + + // Set box position + boxes[key].pos = { + x: w * boxes[key].boxPos.x, + y: h * boxes[key].boxPos.y + }; + // Cache box size + boxes[key].cachedSize = null; + } + } + + // 6. Text and drawing functions + + /* + Overwrite the setColor function to allow the + use of (x) in g.theme.x as a string + in your JSON config ("fg", "bg", "fg2", "bg2", "fgH", "bgH") */ - - let boxKeys = Object.keys(boxes); - - boxKeys.forEach((key) => { - let boxConfig = boxes[key]; - boxPos[key] = { - x: w * boxConfig.boxPos.x, - y: h * boxConfig.boxPos.y - }; - isDragging[key] = false; - wasDragging[key] = false; - }); - - /** - * --------------------------------------------------------------- - * 6. Text and drawing functions - * --------------------------------------------------------------- - */ - - // Overwrite the setColor function to allow the - // use of (x) in g.theme.x as a string - // in your JSON config ("fg", "bg", "fg2", "bg2", "fgH", "bgH") let modSetColor = function() { - // Save the original setColor function g_setColor = g.setColor; - // Overwrite setColor with the new function g.setColor = function(color) { if (typeof color === "string" && color in g.theme) { g_setColor.call(g, g.theme[color]); @@ -106,7 +172,6 @@ }; let restoreSetColor = function() { - // Restore the original setColor function if (g_setColor) { g.setColor = g_setColor; } @@ -130,25 +195,6 @@ } }; - let calcBoxSize = function(boxItem) { - g.reset(); - g.setFontAlign(0,0); - g.setFont(boxItem.font, boxItem.fontSize); - let strWidth = g.stringWidth(boxItem.string) + 2 * boxItem.outline; - let fontHeight = g.getFontHeight() + 2 * boxItem.outline; - totalWidth = strWidth + 2 * boxItem.xPadding; - totalHeight = fontHeight + 2 * boxItem.yPadding; - }; - - let calcBoxPos = function(boxKey) { - return { - x1: boxPos[boxKey].x - totalWidth / 2, - y1: boxPos[boxKey].y - totalHeight / 2, - x2: boxPos[boxKey].x + totalWidth / 2, - y2: boxPos[boxKey].y + totalHeight / 2 - }; - }; - let displaySaveIcon = function() { draw(boxes); g.drawImage(saveIcon, w / 2 - 24, h / 2 - 24); @@ -159,33 +205,15 @@ }, 2000); }; - /** - * --------------------------------------------------------------- - * 7. String forming helper functions - * --------------------------------------------------------------- - */ - - let isBool = function(val, defaultVal) { - return typeof val !== 'undefined' ? Boolean(val) : defaultVal; - }; - + // 7. String forming helper functions let getDate = function(short, shortMonth, disableSuffix) { const date = new Date(); const dayOfMonth = date.getDate(); const month = shortMonth ? locale.month(date, 1) : locale.month(date, 0); const year = date.getFullYear(); - let suffix; - if ([1, 21, 31].includes(dayOfMonth)) { - suffix = "st"; - } else if ([2, 22].includes(dayOfMonth)) { - suffix = "nd"; - } else if ([3, 23].includes(dayOfMonth)) { - suffix = "rd"; - } else { - suffix = "th"; - } - let dayOfMonthStr = disableSuffix ? dayOfMonth : dayOfMonth + suffix; - return month + " " + dayOfMonthStr + (short ? '' : (", " + year)); // not including year for short version + let suffix = ["st", "nd", "rd"][(dayOfMonth - 1) % 10] || "th"; + let dayOfMonthStr = disableSuffix ? dayOfMonth : `${dayOfMonth}${suffix}`; + return `${month} ${dayOfMonthStr}${short ? '' : `, ${year}`}`; }; let getDayOfWeek = function(date, short) { @@ -198,187 +226,215 @@ return short ? meridian[0] : meridian; }; - let modString = function(boxItem, data) { - let prefix = boxItem.prefix || ''; - let suffix = boxItem.suffix || ''; - return prefix + data + suffix; + let formatStr = function(boxItem, data) { + return `${boxItem.prefix || ''}${data}${boxItem.suffix || ''}`; }; - /** - * --------------------------------------------------------------- - * 8. Main draw function - * --------------------------------------------------------------- - */ + // 8. Main draw function and update logic + let lastDay = -1; + const BATTERY_UPDATE_INTERVAL = 300000; - let draw = (function() { - let updatePerMinute = true; // variable to track the state of time display + let updateBoxData = function() { + let date = new Date(); + let currentDay = date.getDate(); + let now = Date.now(); - return function(boxes) { - date = new Date(); - g.clear(); - background.fillRect(Bangle.appRect); + if (boxes.time || boxes.meridian || boxes.date || boxes.dow) { if (boxes.time) { - boxes.time.string = modString(boxes.time, locale.time(date, isBool(boxes.time.short, true) ? 1 : 0)); - updatePerMinute = isBool(boxes.time.short, true); - } - if (boxes.meridian) { - 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.shortMonth, true), - isBool(boxes.date.disableSuffix, false) - ))); - } - if (boxes.dow) { - boxes.dow.string = modString(boxes.dow, getDayOfWeek(date, isBool(boxes.dow.short, true))); - } - if (boxes.batt) { - boxes.batt.string = modString(boxes.batt, E.getBattery()); - } - if (boxes.step) { - boxes.step.string = modString(boxes.step, Bangle.getHealthStatus("day").steps); - } - boxKeys.forEach((boxKey) => { - let boxItem = boxes[boxKey]; - calcBoxSize(boxItem); - const pos = calcBoxPos(boxKey); - if (isDragging[boxKey]) { - g.setColor(boxItem.border); - g.drawRect(pos.x1, pos.y1, pos.x2, pos.y2); + let showSeconds = !boxes.time.short; + let timeString = locale.time(date, 1).trim(); + if (showSeconds) { + let seconds = date.getSeconds().toString().padStart(2, '0'); + timeString += ':' + seconds; + } + let newTimeString = formatStr(boxes.time, timeString); + if (newTimeString !== boxes.time.string) { + boxes.time.string = newTimeString; + boxes.time.cachedSize = null; } - g.drawString( - boxItem, - boxItem.string, - boxPos[boxKey].x + boxItem.xOffset, - boxPos[boxKey].y + boxItem.yOffset - ); - }); - if (!Object.values(isDragging).some(Boolean)) { - if (drawTimeout) clearTimeout(drawTimeout); - let interval = updatePerMinute ? 60000 - (Date.now() % 60000) : 1000; - drawTimeout = setTimeout(() => draw(boxes), interval); } - }; - })(); - /** - * --------------------------------------------------------------- - * 9. Helper function for touch event - * --------------------------------------------------------------- - */ + if (boxes.meridian) { + let newMeridianString = formatStr(boxes.meridian, locale.meridian(date, boxes.meridian.short)); + if (newMeridianString !== boxes.meridian.string) { + boxes.meridian.string = newMeridianString; + boxes.meridian.cachedSize = null; + } + } - let touchInText = function(e, boxItem, boxKey) { + if (boxes.date && currentDay !== lastDay) { + let newDateString = formatStr(boxes.date, + getDate(boxes.date.short, + boxes.date.shortMonth, + boxes.date.noSuffix) + ); + if (newDateString !== boxes.date.string) { + boxes.date.string = newDateString; + boxes.date.cachedSize = null; + } + } + + if (boxes.dow) { + let newDowString = formatStr(boxes.dow, getDayOfWeek(date, boxes.dow.short)); + if (newDowString !== boxes.dow.string) { + boxes.dow.string = newDowString; + boxes.dow.cachedSize = null; + } + } + + lastDay = currentDay; + } + + if (boxes.step) { + let newStepCount = Bangle.getHealthStatus("day").steps; + let newStepString = formatStr(boxes.step, newStepCount); + if (newStepString !== boxes.step.string) { + boxes.step.string = newStepString; + boxes.step.cachedSize = null; + } + } + + if (boxes.batt) { + if (!boxes.batt.lastUpdate || now - boxes.batt.lastUpdate >= BATTERY_UPDATE_INTERVAL) { + let currentLevel = E.getBattery(); + if (currentLevel !== boxes.batt.lastLevel) { + let newBattString = formatStr(boxes.batt, currentLevel); + if (newBattString !== boxes.batt.string) { + boxes.batt.string = newBattString; + boxes.batt.cachedSize = null; + boxes.batt.lastLevel = currentLevel; + } + } + boxes.batt.lastUpdate = now; + } + } + }; + + let draw = function() { + g.clear(); + + // Always draw backgrounds full screen + if (bgImage) { // Check for bg in boxclk config + g.drawImage(bgImage, 0, 0); + } else { // Otherwise use clockbg module + background.fillRect(0, 0, g.getWidth(), g.getHeight()); + } + + if (!isDragging) { + updateBoxData(); + } + + for (let boxKey in boxes) { + let boxItem = boxes[boxKey]; + + // Set font and alignment for each box individually + g.setFont(boxItem.font, boxItem.fontSize); + g.setFontAlign(0, 0); + + calcBoxSize(boxItem); + + const pos = calcBoxPos(boxItem); + + if (boxItem.selected) { + g.setColor(boxItem.border); + g.drawRect(pos.x1, pos.y1, pos.x2, pos.y2); + } + + g.drawString( + boxItem, + boxItem.string, + boxItem.pos.x + boxItem.xOffset, + boxItem.pos.y + boxItem.yOffset + ); + } + + if (!isDragging) { + if (drawTimeout) clearTimeout(drawTimeout); + let updateInterval = boxes.time && !isBool(boxes.time.short, true) ? 1000 : 60000 - (Date.now() % 60000); + drawTimeout = setTimeout(draw, updateInterval); + } + }; + + // 9. Helper function for touch event + let calcBoxPos = function(boxItem) { calcBoxSize(boxItem); - const pos = calcBoxPos(boxKey); + return { + x1: boxItem.pos.x - boxItem.cachedSize.width / 2, + y1: boxItem.pos.y - boxItem.cachedSize.height / 2, + x2: boxItem.pos.x + boxItem.cachedSize.width / 2, + y2: boxItem.pos.y + boxItem.cachedSize.height / 2 + }; + }; + + // Use cached size if available, otherwise calculate and cache + let calcBoxSize = function(boxItem) { + if (boxItem.cachedSize) { + return boxItem.cachedSize; + } + + g.setFont(boxItem.font, boxItem.fontSize); + g.setFontAlign(0, 0); + + let strWidth = g.stringWidth(boxItem.string) + 2 * boxItem.outline; + let fontHeight = g.getFontHeight() + 2 * boxItem.outline; + let totalWidth = strWidth + 2 * boxItem.xPadding; + let totalHeight = fontHeight + 2 * boxItem.yPadding; + + boxItem.cachedSize = { + width: totalWidth, + height: totalHeight + }; + + return boxItem.cachedSize; + }; + + let touchInText = function(e, boxItem) { + calcBoxSize(boxItem); + const pos = calcBoxPos(boxItem); return e.x >= pos.x1 && - e.x <= pos.x2 && - e.y >= pos.y1 && - e.y <= pos.y2; + e.x <= pos.x2 && + e.y >= pos.y1 && + e.y <= pos.y2; }; let deselectAllBoxes = function() { - Object.keys(isDragging).forEach((boxKey) => { - isDragging[boxKey] = false; - }); + isDragging = false; + for (let boxKey in boxes) { + boxes[boxKey].selected = false; + } restoreSetColor(); widgets.show(); widgets.swipeOn(); modSetColor(); }; - /** - * --------------------------------------------------------------- - * 10. Setup function to configure event handlers - * --------------------------------------------------------------- - */ - + // 10. Setup function to configure event handlers let setup = function() { - // ------------------------------------ - // Define the touchHandler function - // ------------------------------------ - touchHandler = function(zone, e) { - wasDragging = Object.assign({}, isDragging); - let boxTouched = false; - boxKeys.forEach((boxKey) => { - if (touchInText(e, boxes[boxKey], boxKey)) { - isDragging[boxKey] = true; - wasDragging[boxKey] = true; - boxTouched = true; - } - }); - if (!boxTouched) { - if (!Object.values(isDragging).some(Boolean)) { // check if no boxes are being dragged - deselectAllBoxes(); - if (doubleTapTimer) { - clearTimeout(doubleTapTimer); - doubleTapTimer = null; - // Save boxesConfig on double tap outside of any box and when no boxes are being dragged - Object.keys(boxPos).forEach((boxKey) => { - boxesConfig[boxKey].boxPos.x = (boxPos[boxKey].x / w).toFixed(3); - boxesConfig[boxKey].boxPos.y = (boxPos[boxKey].y / h).toFixed(3); - }); - storage.write(fileName, JSON.stringify(boxesConfig)); - displaySaveIcon(); - return; - } - } else { - // if any box is being dragged, just deselect all without saving - deselectAllBoxes(); - } - } - if (Object.values(wasDragging).some(Boolean) || !boxTouched) { - draw(boxes); - } - doubleTapTimer = setTimeout(() => { - doubleTapTimer = null; - }, 500); // Increase or decrease this value based on the desired double tap timing - movementDistance = 0; - }; - - // ------------------------------------ - // 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 - if (movementDistance > 1) { - boxKeys.forEach((boxKey) => { - if (isDragging[boxKey]) { - widgets.hide(); - let boxItem = boxes[boxKey]; - calcBoxSize(boxItem); - let newX = boxPos[boxKey].x + e.dx; - let newY = boxPos[boxKey].y + e.dy; - if (newX - totalWidth / 2 >= 0 && - newX + totalWidth / 2 <= w && - newY - totalHeight / 2 >= 0 && - newY + totalHeight / 2 <= h ) { - boxPos[boxKey].x = newX; - boxPos[boxKey].y = newY; - } - const pos = calcBoxPos(boxKey); - g.clearRect(pos.x1, pos.y1, pos.x2, pos.y2); - } - }); - draw(boxes); - } - }; - + Bangle.on('lock', lockHandler); Bangle.on('touch', touchHandler); Bangle.on('drag', dragHandler); - + + if (boxes.step) { + boxes.step.string = formatStr(boxes.step, Bangle.getHealthStatus("day").steps); + Bangle.on('step', stepHandler); + } + + if (boxes.batt) { + boxes.batt.lastLevel = E.getBattery(); + boxes.batt.string = formatStr(boxes.batt, boxes.batt.lastLevel); + boxes.batt.lastUpdate = Date.now(); + } + Bangle.setUI({ - mode : "clock", - remove : function() { - // Remove event handlers, stop draw timer, remove custom font if used + mode: "clock", + remove: function() { + // Remove event handlers, stop draw timer, remove custom font Bangle.removeListener('touch', touchHandler); Bangle.removeListener('drag', dragHandler); + Bangle.removeListener('lock', lockHandler); + if (boxes.step) { + Bangle.removeListener('step', stepHandler); + } if (drawTimeout) clearTimeout(drawTimeout); drawTimeout = undefined; delete Graphics.prototype.setFontBrunoAce; @@ -388,16 +444,12 @@ widgets.show(); } }); + loadCustomFont(); - draw(boxes); + draw(); }; - /** - * --------------------------------------------------------------- - * 11. Main execution part - * --------------------------------------------------------------- - */ - + // 11. Main execution Bangle.loadWidgets(); widgets.swipeOn(); modSetColor(); diff --git a/apps/boxclk/boxclk-2.json b/apps/boxclk/boxclk-2.json index 64b842f1c..dde1da97e 100644 --- a/apps/boxclk/boxclk-2.json +++ b/apps/boxclk/boxclk-2.json @@ -11,15 +11,15 @@ "xOffset": 3, "yOffset": 0, "boxPos": { - "x": "0.5", - "y": "0.33" + "x": "0.494", + "y": "0.739" } }, "dow": { "font": "6x8", "fontSize": 3, "outline": 1, - "color": "#5ccd73", + "color": "bgH", "outlineColor": "fg", "border": "#f0f", "xPadding": -1, @@ -27,8 +27,8 @@ "xOffset": 2, "yOffset": 0, "boxPos": { - "x": "0.5", - "y": "0.57" + "x": "0.421", + "y": "0.201" }, "short": false }, @@ -36,7 +36,7 @@ "font": "6x8", "fontSize": 2, "outline": 1, - "color": "#5ccd73", + "color": "bgH", "outlineColor": "fg", "border": "#f0f", "xPadding": -0.5, @@ -44,8 +44,8 @@ "xOffset": 1, "yOffset": 0, "boxPos": { - "x": "0.5", - "y": "0.75" + "x": "0.454", + "y": "0.074" }, "shortMonth": false, "disableSuffix": true @@ -62,8 +62,8 @@ "xOffset": 2, "yOffset": 1, "boxPos": { - "x": "0.5", - "y": "0.92" + "x": "0.494", + "y": "0.926" }, "prefix": "Steps: " }, @@ -79,8 +79,8 @@ "xOffset": 2, "yOffset": 2, "boxPos": { - "x": "0.85", - "y": "0.08" + "x": "0.805", + "y": "0.427" }, "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 79b4c3019..c8790fe7f 100644 --- a/apps/boxclk/metadata.json +++ b/apps/boxclk/metadata.json @@ -1,7 +1,7 @@ { "id": "boxclk", "name": "Box Clock", - "version": "0.05", + "version": "0.09", "description": "A customizable clock with configurable text boxes that can be positioned to show your favorite background", "icon": "app.png", "dependencies" : { "clockbg":"module" }, @@ -24,4 +24,4 @@ "data": [ {"name":"boxclk.json","url":"boxclk.json"} ] -} +} \ No newline at end of file diff --git a/apps/boxclk/screenshot-2.png b/apps/boxclk/screenshot-2.png index b7a73d66a..568a310b9 100644 Binary files a/apps/boxclk/screenshot-2.png and b/apps/boxclk/screenshot-2.png differ diff --git a/apps/calculator/ChangeLog b/apps/calculator/ChangeLog index 2e1ace7bf..7b47d3a4c 100644 --- a/apps/calculator/ChangeLog +++ b/apps/calculator/ChangeLog @@ -5,3 +5,4 @@ 0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2) 0.06: Bangle.js 2: Exit with a short press of the physical button 0.07: Bangle.js 2: Exit by pressing upper left corner of the screen +0.08: truncate long numbers (and append '...' to displayed value) diff --git a/apps/calculator/app.js b/apps/calculator/app.js index 465291d13..5f4e77a47 100644 --- a/apps/calculator/app.js +++ b/apps/calculator/app.js @@ -10,9 +10,9 @@ g.clear(); require("Font7x11Numeric7Seg").add(Graphics); -var DEFAULT_SELECTION_NUMBERS = '5', DEFAULT_SELECTION_OPERATORS = '=', DEFAULT_SELECTION_SPECIALS = 'R'; -var RIGHT_MARGIN = 20; +var DEFAULT_SELECTION_NUMBERS = '5'; var RESULT_HEIGHT = 40; +var RESULT_MAX_LEN = Math.floor((g.getWidth() - 20) / 14); var COLORS = { // [normal, selected] DEFAULT: ['#7F8183', '#A6A6A7'], @@ -88,28 +88,11 @@ function prepareScreen(screen, grid, defaultColor) { } function drawKey(name, k, selected) { - var rMargin = 0; - var bMargin = 0; var color = k.color || COLORS.DEFAULT; g.setColor(color[selected ? 1 : 0]); g.setFont('Vector', 20).setFontAlign(0,0); g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]); g.setColor(-1); - // correct margins to center the texts - if (name == '0') { - rMargin = (RIGHT_MARGIN * 2) - 7; - } else if (name === '/') { - rMargin = 5; - } else if (name === '*') { - bMargin = 5; - rMargin = 3; - } else if (name === '-') { - rMargin = 3; - } else if (name === 'R' || name === 'N') { - rMargin = k.val === 'C' ? 0 : -9; - } else if (name === '%') { - rMargin = -3; - } g.drawString(k.val || name, (k.xy[0] + k.xy[2])/2, (k.xy[1] + k.xy[3])/2); } @@ -138,29 +121,21 @@ function drawGlobal() { screen[k] = specials[k]; } drawKeys(); - var selected = DEFAULT_SELECTION_NUMBERS; - var prevSelected = DEFAULT_SELECTION_NUMBERS; } function drawNumbers() { screen = numbers; screenColor = COLORS.DEFAULT; drawKeys(); - var selected = DEFAULT_SELECTION_NUMBERS; - var prevSelected = DEFAULT_SELECTION_NUMBERS; } function drawOperators() { screen = operators; screenColor =COLORS.OPERATOR; drawKeys(); - var selected = DEFAULT_SELECTION_OPERATORS; - var prevSelected = DEFAULT_SELECTION_OPERATORS; } function drawSpecials() { screen = specials; screenColor = COLORS.SPECIAL; drawKeys(); - var selected = DEFAULT_SELECTION_SPECIALS; - var prevSelected = DEFAULT_SELECTION_SPECIALS; } function getIntWithPrecision(x) { @@ -218,8 +193,6 @@ function doMath(x, y, operator) { } function displayOutput(num) { - var len; - var minusMarge = 0; g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT-1); g.setColor(-1); if (num === Infinity || num === -Infinity || isNaN(num)) { @@ -230,9 +203,7 @@ function displayOutput(num) { num = '-INFINITY'; } else { num = 'NOT A NUMBER'; - minusMarge = -25; } - len = (num + '').length; currNumber = null; results = null; isDecimal = false; @@ -261,6 +232,9 @@ function displayOutput(num) { num = num.toString(); num = num.replace("-","- "); // fix padding for '-' g.setFont('7x11Numeric7Seg', 2); + if (num.length > RESULT_MAX_LEN) { + num = num.substr(0, RESULT_MAX_LEN - 1)+'...'; + } } g.setFontAlign(1,0); g.drawString(num, g.getWidth()-20, RESULT_HEIGHT/2); diff --git a/apps/calculator/metadata.json b/apps/calculator/metadata.json index 1674b7843..a88444e11 100644 --- a/apps/calculator/metadata.json +++ b/apps/calculator/metadata.json @@ -2,7 +2,7 @@ "id": "calculator", "name": "Calculator", "shortName": "Calculator", - "version": "0.07", + "version": "0.08", "description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.", "icon": "calculator.png", "screenshots": [{"url":"screenshot_calculator.png"}], diff --git a/apps/dutchclock/ChangeLog b/apps/dutchclock/ChangeLog new file mode 100644 index 000000000..8efcb9edb --- /dev/null +++ b/apps/dutchclock/ChangeLog @@ -0,0 +1 @@ +0.20: First release \ No newline at end of file diff --git a/apps/dutchclock/README.md b/apps/dutchclock/README.md new file mode 100644 index 000000000..787bcce1b --- /dev/null +++ b/apps/dutchclock/README.md @@ -0,0 +1,22 @@ +# Dutch Clock +This clock shows the time, in words, the way a Dutch person might respond when asked what time it is. Useful when learning Dutch and/or pretending to know Dutch. + +Dedicated to my wife, who will sometimes insist I tell her exactly what time it says on the watch and not just an approximation. + +## Options +- Three modes: + - exact time ("zeven voor half zes / twee voor tien") + - approximate time, rounded to the nearest 5-minute mark ("bijna vijf voor half zes / tegen tienen") (the default) + - hybrid mode, rounded when close to the quarter marks and exact otherwise ("zeven voor half zes / tegen tienen") +- Option to turn top widgets on/off (on by default) +- Option to show digital time at the bottom (off by default) +- Option to show the date at the bottom (on by default) + +The app respects top and bottom widgets, but it gets a bit crowded when you add the time/date and you also have bottom widgets turned on. + +When you turn widgets off, you can still see the top widgets by swiping down from the top. + +## Screenshots +![](screenshotbangle1-2.png) +![](screenshotbangle2.png) +![](screenshotbangle1.png) \ No newline at end of file diff --git a/apps/dutchclock/app-icon.js b/apps/dutchclock/app-icon.js new file mode 100644 index 000000000..7d6e655e8 --- /dev/null +++ b/apps/dutchclock/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwgP/AE0/Ao/4sccAoX79NtAofttIFD8dsAof3t1/GZ397oGE/YLE6IFDloFE1vbAoeNAondAon/z4FE356U/nNxhZC/drlpLDscNAoX4ue9C4f3L4oAKt4FEQ4qxE/0skIGDtg7DAoNtAocsAogAX94POA")) \ No newline at end of file diff --git a/apps/dutchclock/app.js b/apps/dutchclock/app.js new file mode 100644 index 000000000..588692a2b --- /dev/null +++ b/apps/dutchclock/app.js @@ -0,0 +1,260 @@ +// Load libraries +const storage = require("Storage"); +const locale = require('locale'); +const widget_utils = require('widget_utils'); + +// Define constants +const DATETIME_SPACING_HEIGHT = 5; +const TIME_HEIGHT = 8; +const DATE_HEIGHT = 8; +const BOTTOM_SPACING = 2; + +const MINS_IN_HOUR = 60; +const MINS_IN_DAY = 24 * MINS_IN_HOUR; + +const VARIANT_EXACT = 'exact'; +const VARIANT_APPROXIMATE = 'approximate'; +const VARIANT_HYBRID = 'hybrid'; + +const DEFAULTS_FILE = "dutchclock.default.json"; +const SETTINGS_FILE = "dutchclock.json"; + +// Load settings +const settings = Object.assign( + storage.readJSON(DEFAULTS_FILE, true) || {}, + storage.readJSON(SETTINGS_FILE, true) || {} +); + +// Define global variables +const textBox = {}; +let date, mins; + +// Define functions +function initialize() { + // Reset the state of the graphics library + g.clear(true); + + // Tell Bangle this is a clock + Bangle.setUI("clock"); + + // Load widgets + Bangle.loadWidgets(); + + // Show widgets, or not + if (settings.showWidgets) { + Bangle.drawWidgets(); + } else { + widget_utils.swipeOn(); + } + + const dateTimeHeight = (settings.showDate || settings.showTime ? DATETIME_SPACING_HEIGHT : 0) + + (settings.showDate ? DATE_HEIGHT : 0) + + (settings.showTime ? TIME_HEIGHT : 0); + + Object.assign(textBox, { + x: Bangle.appRect.x + Bangle.appRect.w / 2, + y: Bangle.appRect.y + (Bangle.appRect.h - dateTimeHeight) / 2, + w: Bangle.appRect.w - 2, + h: Bangle.appRect.h - dateTimeHeight + }); + + // draw immediately at first + tick(); + + // now check every second + let secondInterval = setInterval(tick, 1000); + + // Stop updates when LCD is off, restart when on + Bangle.on('lcdPower',on=>{ + if (secondInterval) clearInterval(secondInterval); + secondInterval = undefined; + if (on) { + secondInterval = setInterval(tick, 1000); + draw(); // draw immediately + } + }); +} + +function tick() { + date = new Date(); + const m = (date.getHours() * MINS_IN_HOUR + date.getMinutes()) % MINS_IN_DAY; + + if (m !== mins) { + mins = m; + draw(); + } +} + +function draw() { + // work out how to display the current time + const timeLines = getTimeLines(mins); + const bottomLines = getBottomLines(); + + g.reset().clearRect(Bangle.appRect); + + // draw the current time (4x size 7 segment) + setFont(timeLines); + + g.setFontAlign(0,0); // align center top + g.drawString(timeLines.join("\n"), textBox.x, textBox.y, false); + + if (bottomLines.length) { + // draw the time and/or date, in a normal font + g.setFont("6x8"); + g.setFontAlign(0,1); // align center bottom + // pad the date - this clears the background if the date were to change length + g.drawString(bottomLines.join('\n'), Bangle.appRect.w / 2, Bangle.appRect.y2 - BOTTOM_SPACING, false); + } +} + +function setFont(timeLines) { + const size = textBox.h / timeLines.length; + + g.setFont("Vector", size); + + let width = g.stringWidth(timeLines.join('\n')); + + if (width > textBox.w) { + g.setFont("Vector", Math.floor(size * (textBox.w / width))); + } +} + +function getBottomLines() { + const lines = []; + + if (settings.showTime) { + lines.push(locale.time(date, 1)); + } + + if (settings.showDate) { + lines.push(locale.date(date)); + } + + return lines; + } + +function getTimeLines(m) { + switch (settings.variant) { + case VARIANT_EXACT: + return getExactTimeLines(m); + case VARIANT_APPROXIMATE: + return getApproximateTimeLines(m); + case VARIANT_HYBRID: + return distanceFromNearest(15)(m) < 3 + ? getApproximateTimeLines(m) + : getExactTimeLines(m); + default: + console.warn(`Error in settings: unknown variant "${settings.variant}"`); + return getExactTimeLines(m); + } +} + +function getExactTimeLines(m) { + if (m === 0) { + return ['middernacht']; + } + + const hour = getHour(m); + const minutes = getMinutes(hour.offset); + + const lines = minutes.concat(hour.lines); + if (lines.length === 1) { + lines.push('uur'); + } + + return lines; +} + +function getApproximateTimeLines(m) { + const roundMinutes = getRoundMinutes(m); + + const lines = getExactTimeLines(roundMinutes.minutes); + + return addApproximateDescription(lines, roundMinutes.offset); +} + +function getHour(minutes) { + const hours = ['twaalf', 'een', 'twee', 'drie', 'vier', 'vijf', 'zes', 'zeven', 'acht', 'negen', 'tien', 'elf']; + + const h = Math.floor(minutes / MINS_IN_HOUR), m = minutes % MINS_IN_HOUR; + + if (m <= 15) { + return {lines: [hours[h % 12]], offset: m}; + } + + if (m > 15 && m < 45) { + return { + lines: ['half', hours[(h + 1) % 12]], + offset: m - (MINS_IN_HOUR / 2) + }; + } + + return {lines: [hours[(h + 1) % 12]], offset: m - MINS_IN_HOUR}; +} + +function getMinutes(m) { + const minutes = ['', 'een', 'twee', 'drie', 'vier', 'vijf', 'zes', 'zeven', 'acht', 'negen', 'tien', 'elf', 'twaalf', 'dertien', 'veertien', 'kwart']; + + if (m === 0) { + return []; + } + + return [minutes[Math.abs(m)], m > 0 ? 'over' : 'voor']; +} + +function getRoundMinutes(m) { + const nearest = roundTo(5)(m); + + return { + minutes: nearest % MINS_IN_DAY, + offset: m - nearest + }; +} + +function addApproximateDescription(lines, offset) { + if (offset === 0) { + return lines; + } + + if (lines.length === 1 || lines[1] === 'uur') { + const singular = lines[0]; + const plural = getPlural(singular); + return { + '-2': ['tegen', plural], + '-1': ['iets voor', singular], + '1': ['iets na', plural], + '2': ['even na', plural] + }[`${offset}`]; + } + + return { + '-2': ['bijna'].concat(lines), + '-1': ['rond'].concat(lines), + '1': ['iets na'].concat(lines), + '2': lines.concat(['geweest']) + }[`${offset}`]; +} + +function getPlural(h) { + return { + middernacht: 'middernacht', + een: 'enen', + twee: 'tweeën', + drie: 'drieën', + vijf: 'vijven', + zes: 'zessen', + elf: 'elven', + twaalf: 'twaalven' + }[h] || `${h}en`; +} + +function distanceFromNearest(x) { + return n => Math.abs(n - roundTo(x)(n)); +} + +function roundTo(x) { + return n => Math.round(n / x) * x; +} + +// Let's go +initialize(); \ No newline at end of file diff --git a/apps/dutchclock/app.png b/apps/dutchclock/app.png new file mode 100644 index 000000000..94d35b0c5 Binary files /dev/null and b/apps/dutchclock/app.png differ diff --git a/apps/dutchclock/default.json b/apps/dutchclock/default.json new file mode 100644 index 000000000..cfe5d34a4 --- /dev/null +++ b/apps/dutchclock/default.json @@ -0,0 +1,6 @@ +{ + "variant": "approximate", + "showWidgets": true, + "showTime": false, + "showDate": true +} \ No newline at end of file diff --git a/apps/dutchclock/metadata.json b/apps/dutchclock/metadata.json new file mode 100644 index 000000000..d336023f8 --- /dev/null +++ b/apps/dutchclock/metadata.json @@ -0,0 +1,28 @@ +{ + "id": "dutchclock", + "name": "Dutch Clock", + "shortName":"Dutch Clock", + "icon": "app.png", + "version":"0.20", + "description": "A clock that displays the time the way a Dutch person would respond when asked what time it is.", + "type": "clock", + "tags": "clock,dutch,text", + "supports": ["BANGLEJS", "BANGLEJS2"], + "allow_emulator": true, + "screenshots": [ + {"url":"screenshotbangle1-2.png"}, + {"url":"screenshotbangle2.png"}, + {"url":"screenshotbangle1.png"} + ], + "storage": [ + {"name":"dutchclock.app.js","url":"app.js"}, + {"name":"dutchclock.settings.js","url":"settings.js"}, + {"name":"dutchclock.default.json","url":"default.json"}, + {"name":"dutchclock.img","url":"app-icon.js","evaluate":true} + ], + "data": [ + {"name":"dutchclock.json"} + ], + "readme":"README.md" +} + \ No newline at end of file diff --git a/apps/dutchclock/screenshotbangle1-2.png b/apps/dutchclock/screenshotbangle1-2.png new file mode 100644 index 000000000..08bf31939 Binary files /dev/null and b/apps/dutchclock/screenshotbangle1-2.png differ diff --git a/apps/dutchclock/screenshotbangle1.png b/apps/dutchclock/screenshotbangle1.png new file mode 100644 index 000000000..49ba895f4 Binary files /dev/null and b/apps/dutchclock/screenshotbangle1.png differ diff --git a/apps/dutchclock/screenshotbangle2.png b/apps/dutchclock/screenshotbangle2.png new file mode 100644 index 000000000..48b3fd501 Binary files /dev/null and b/apps/dutchclock/screenshotbangle2.png differ diff --git a/apps/dutchclock/settings.js b/apps/dutchclock/settings.js new file mode 100644 index 000000000..146df5395 --- /dev/null +++ b/apps/dutchclock/settings.js @@ -0,0 +1,73 @@ +(function(back) { + const storage = require("Storage"); + + const VARIANT_EXACT = 'exact'; + const VARIANT_APPROXIMATE = 'approximate'; + const VARIANT_HYBRID = 'hybrid'; + + const DEFAULTS_FILE = "dutchclock.default.json"; + const SETTINGS_FILE = "dutchclock.json"; + + // Load settings + const settings = Object.assign( + storage.readJSON(DEFAULTS_FILE, true) || {}, + storage.readJSON(SETTINGS_FILE, true) || {} + ); + + function writeSettings() { + require('Storage').writeJSON(SETTINGS_FILE, settings); + } + + function writeSetting(setting, value) { + settings[setting] = value; + writeSettings(); + } + + function writeOption(setting, value) { + writeSetting(setting, value); + showMainMenu(); + } + + function getOption(label, setting, value) { + return { + title: label, + value: settings[setting] === value, + onchange: () => { + writeOption(setting, value); + } + }; + } + + // Show the menu + function showMainMenu() { + const mainMenu = [ + getOption('Exact', 'variant', VARIANT_EXACT), + getOption('Approximate', 'variant', VARIANT_APPROXIMATE), + getOption('Hybrid', 'variant', VARIANT_HYBRID), + { + title: 'Show widgets?', + value: settings.showWidgets, + onchange: v => writeSetting('showWidgets', v) + }, + { + title: 'Show time?', + value: settings.showTime, + onchange: v => writeSetting('showTime', v) + }, + { + title: 'Show date?', + value: settings.showDate, + onchange: v => writeSetting('showDate', v) + } + ]; + + mainMenu[""] = { + title : "Dutch Clock", + back: back + }; + + E.showMenu(mainMenu); + } + + showMainMenu(); + }) \ No newline at end of file diff --git a/apps/elapsed_t/ChangeLog b/apps/elapsed_t/ChangeLog index 6a72c2590..26fbf5ff0 100644 --- a/apps/elapsed_t/ChangeLog +++ b/apps/elapsed_t/ChangeLog @@ -1,3 +1,4 @@ 0.01: New App! 0.02: Handle AM/PM time in the "set target" menu. Add yesterday/today/tomorrow when showing target date to improve readability. 0.03: Add option to set clock as default, handle DST in day/month/year mode +0.04: Use new pickers from the more_pickers library, add settings to display seconds never/unlocked/always diff --git a/apps/elapsed_t/README.md b/apps/elapsed_t/README.md index dc2173409..9e361be59 100644 --- a/apps/elapsed_t/README.md +++ b/apps/elapsed_t/README.md @@ -1,7 +1,10 @@ # Elapsed Time Clock A clock that calculates the time difference between now (in blue/cyan) and any given target date (in red/orange). -The results is show in years, months, days, hours, minutes, seconds. To save battery life, the seconds are shown only when the watch is unlocked, or can be disabled entirely. +The results is show in years, months, days, hours, minutes, seconds. The seconds can be shown: +- always +- when the watch is unlocked +- never. The time difference is positive if the target date is in the past and negative if it is in the future. diff --git a/apps/elapsed_t/app.js b/apps/elapsed_t/app.js index 13fbca2cd..910ff85f3 100644 --- a/apps/elapsed_t/app.js +++ b/apps/elapsed_t/app.js @@ -24,13 +24,20 @@ var now = new Date(); var settings = Object.assign({ // default values - displaySeconds: true, + displaySeconds: 1, displayMonthsYears: true, dateFormat: 0, time24: true }, require('Storage').readJSON(APP_NAME + ".settings.json", true) || {}); -var temp_displaySeconds = settings.displaySeconds; +function writeSettings() { + require('Storage').writeJSON(APP_NAME + ".settings.json", settings); +} + +if (typeof settings.displaySeconds === 'boolean') { + settings.displaySeconds = 1; + writeSettings(); +} var data = Object.assign({ // default values @@ -49,17 +56,12 @@ function writeData() { require('Storage').writeJSON(APP_NAME + ".data.json", data); } -function writeSettings() { - require('Storage').writeJSON(APP_NAME + ".settings.json", settings); - temp_displaySeconds = settings.temp_displaySeconds; -} - let inMenu = false; Bangle.on('touch', function (zone, e) { if (!inMenu && e.y > 24) { if (drawTimeout) clearTimeout(drawTimeout); - E.showMenu(menu); + showMainMenu(); inMenu = true; } }); @@ -112,115 +114,151 @@ function formatDateTime(date, dateFormat, time24, showSeconds) { return formattedDateTime; } -function formatHourToAMPM(h){ +function formatHourToAMPM(h) { var ampm = (h >= 12 ? 'PM' : 'AM'); var h_ampm = h % 12; h_ampm = (h_ampm == 0 ? 12 : h_ampm); - return `${h_ampm} ${ampm}` + return `${h_ampm}\n${ampm}`; } -function howManyDaysInMonth(month, year) { - return new Date(year, month, 0).getDate(); -} +function getDatePickerObject() { + switch (settings.dateFormat) { + case 0: + return { + back: showMainMenu, + title: "Date", + separator_1: "/", + separator_2: "/", -function handleExceedingDay() { - var maxDays = howManyDaysInMonth(data.target.M, data.target.Y); - menu.Day.max = maxDays; - if (data.target.D > maxDays) { - menu.Day.value = maxDays; - data.target.D = maxDays; + value_1: data.target.D, + min_1: 1, max_1: 31, step_1: 1, wrap_1: true, + + value_2: data.target.M, + min_2: 1, max_2: 12, step_2: 1, wrap_2: true, + + value_3: data.target.Y, + min_3: 1900, max_3: 2100, step_3: 1, wrap_3: true, + + format_1: function (v_1) { return (pad2(v_1)); }, + format_2: function (v_2) { return (pad2(v_2)); }, + onchange: function (v_1, v_2, v_3) { data.target.D = v_1; data.target.M = v_2; data.target.Y = v_3; setTarget(true); } + }; + + case 1: + return { + back: showMainMenu, + title: "Date", + separator_1: "/", + separator_2: "/", + + value_1: data.target.M, + min_1: 1, max_1: 12, step_1: 1, wrap_1: true, + + value_2: data.target.D, + min_2: 1, max_2: 31, step_2: 1, wrap_2: true, + + value_3: data.target.Y, + min_3: 1900, max_3: 2100, step_3: 1, wrap_3: true, + + format_1: function (v_1) { return (pad2(v_1)); }, + format_2: function (v_2) { return (pad2(v_2)); }, + onchange: function (v_1, v_2, v_3) { data.target.M = v_1; data.target.D = v_2; data.target.Y = v_3; setTarget(true); } + }; + + case 2: + return { + back: showMainMenu, + title: "Date", + separator_1: "-", + separator_2: "-", + + value_1: data.target.Y, + min_1: 1900, max_1: 2100, step_1: 1, wrap_1: true, + + value_2: data.target.M, + min_2: 1, max_2: 12, step_2: 1, wrap_2: true, + + value_3: data.target.D, + min_3: 1, max_3: 31, step_3: 1, wrap_3: true, + + format_1: function (v_1) { return (pad2(v_1)); }, + format_2: function (v_2) { return (pad2(v_2)); }, + onchange: function (v_1, v_2, v_3) { data.target.Y = v_1; data.target.M = v_2; data.target.D = v_3; setTarget(true); } + }; } } -var menu = { - "": { - "title": "Set target", - back: function () { - E.showMenu(); - Bangle.setUI("clock"); - inMenu = false; - draw(); - } - }, - 'Day': { - value: data.target.D, - min: 1, max: 31, wrap: true, - onchange: v => { - data.target.D = v; - } - }, - 'Month': { - value: data.target.M, - min: 1, max: 12, noList: true, wrap: true, - onchange: v => { - data.target.M = v; - handleExceedingDay(); - } - }, - 'Year': { - value: data.target.Y, - min: 1900, max: 2100, - onchange: v => { - data.target.Y = v; - handleExceedingDay(); - } - }, - 'Hours': { - value: data.target.h, - min: 0, max: 23, wrap: true, - onchange: v => { - data.target.h = v; - }, - format: function (v) {return(settings.time24 ? pad2(v) : formatHourToAMPM(v))} - }, - 'Minutes': { - value: data.target.m, - min: 0, max: 59, wrap: true, - onchange: v => { - data.target.m = v; - }, - format: function (v) { return pad2(v); } - }, - 'Seconds': { - value: data.target.s, - min: 0, max: 59, wrap: true, - onchange: v => { - data.target.s = v; - }, - format: function (v) { return pad2(v); } - }, - 'Save': function () { - E.showMenu(); - inMenu = false; - Bangle.setUI("clock"); - setTarget(true); - writeSettings(); - temp_displaySeconds = settings.displaySeconds; - updateQueueMillis(settings.displaySeconds); - draw(); - }, - 'Reset': function () { - E.showMenu(); - inMenu = false; - Bangle.setUI("clock"); - setTarget(false); - updateQueueMillis(settings.displaySeconds); - draw(); - }, - 'Set clock as default': function () { - setClockAsDefault(); - E.showAlert("Elapsed Time was set as default").then(function() { - E.showMenu(); - inMenu = false; - Bangle.setUI("clock"); - draw(); - }); - } -}; +function getTimePickerObject() { + var timePickerObject = { + back: showMainMenu, + title: "Time", + separator_1: ":", + separator_2: ":", -function setClockAsDefault(){ + value_1: data.target.h, + min_1: 0, max_1: 23, step_1: 1, wrap_1: true, + + value_2: data.target.m, + min_2: 0, max_2: 59, step_2: 1, wrap_2: true, + + value_3: data.target.s, + min_3: 0, max_3: 59, step_3: 1, wrap_3: true, + + format_2: function (v_2) { return (pad2(v_2)); }, + format_3: function (v_3) { return (pad2(v_3)); }, + onchange: function (v_1, v_2, v_3) { data.target.h = v_1; data.target.m = v_2; data.target.s = v_3; setTarget(true); }, + }; + + if (settings.time24) { + timePickerObject.format_1 = function (v_1) { return (pad2(v_1)); }; + } else { + timePickerObject.format_1 = function (v_1) { return (formatHourToAMPM(v_1)); }; + } + + return timePickerObject; +} + +function showMainMenu() { + E.showMenu({ + "": { + "title": "Set target", + back: function () { + E.showMenu(); + Bangle.setUI("clock"); + inMenu = false; + draw(); + } + }, + 'Date': { + value: formatDateTime(target, settings.dateFormat, settings.time24, true).date, + onchange: function () { require("more_pickers").triplePicker(getDatePickerObject()); } + }, + 'Time': { + value: formatDateTime(target, settings.dateFormat, settings.time24, true).time, + onchange: function () { require("more_pickers").triplePicker(getTimePickerObject()); } + }, + 'Reset': function () { + E.showMenu(); + inMenu = false; + Bangle.setUI("clock"); + setTarget(false); + draw(); + }, + 'Set clock as default': function () { + setClockAsDefault(); + E.showAlert("Elapsed Time was set as default").then(function () { + E.showMenu(); + inMenu = false; + Bangle.setUI("clock"); + draw(); + }); + } + }); +} + +function setClockAsDefault() { let storage = require('Storage'); - let settings = storage.readJSON('setting.json',true)||{clock:null}; + let settings = storage.readJSON('setting.json', true) || { clock: null }; settings.clock = "elapsed_t.app.js"; storage.writeJSON('setting.json', settings); } @@ -238,26 +276,21 @@ function setTarget(set) { data.target.isSet = true; } else { target = new Date(); + target.setSeconds(0); Object.assign( data, { target: { isSet: false, - Y: now.getFullYear(), - M: now.getMonth() + 1, // Month is zero-based, so add 1 - D: now.getDate(), - h: now.getHours(), - m: now.getMinutes(), + Y: target.getFullYear(), + M: target.getMonth() + 1, // Month is zero-based, so add 1 + D: target.getDate(), + h: target.getHours(), + m: target.getMinutes(), s: 0 } } ); - menu.Day.value = data.target.D; - menu.Month.value = data.target.M; - menu.Year.value = data.target.Y; - menu.Hours.value = data.target.h; - menu.Minutes.value = data.target.m; - menu.Seconds.value = 0; } writeData(); @@ -267,8 +300,8 @@ var target; setTarget(data.target.isSet); var drawTimeout; -var queueMillis = 1000; - +var temp_displaySeconds; +var queueMillis; function queueDraw() { if (drawTimeout) clearTimeout(drawTimeout); @@ -283,27 +316,25 @@ function queueDraw() { }, delay); } -function updateQueueMillis(displaySeconds) { +function updateQueueMillisAndDraw(displaySeconds) { + temp_displaySeconds = displaySeconds; if (displaySeconds) { queueMillis = 1000; } else { queueMillis = 60000; } + draw(); } Bangle.on('lock', function (on, reason) { - if (inMenu) { // if already in a menu, nothing to do + if (inMenu || settings.displaySeconds == 0 || settings.displaySeconds == 2) { // if already in a menu, or always/never show seconds, nothing to do return; } if (on) { // screen is locked - temp_displaySeconds = false; - updateQueueMillis(false); - draw(); + updateQueueMillisAndDraw(false); } else { // screen is unlocked - temp_displaySeconds = settings.displaySeconds; - updateQueueMillis(temp_displaySeconds); - draw(); + updateQueueMillisAndDraw(true); } }); @@ -335,18 +366,21 @@ function diffToTarget() { var end; if (now > target) { - start = target; - end = now; + start = new Date(target.getTime()); + end = new Date(now.getTime()); } else { - start = now; - end = target; + start = new Date(now.getTime()); + end = new Date(target.getTime()); } + // Adjust for DST + end.setMinutes(end.getMinutes() + end.getTimezoneOffset() - start.getTimezoneOffset()); + diff.Y = end.getFullYear() - start.getFullYear(); diff.M = end.getMonth() - start.getMonth(); diff.D = end.getDate() - start.getDate(); diff.hh = end.getHours() - start.getHours(); - diff.mm = end.getMinutes() - start.getMinutes() + end.getTimezoneOffset() - start.getTimezoneOffset(); + diff.mm = end.getMinutes() - start.getMinutes(); diff.ss = end.getSeconds() - start.getSeconds(); // Adjust negative differences @@ -372,7 +406,6 @@ function diffToTarget() { diff.Y--; } - } else { var timeDifference = target - now; timeDifference = Math.abs(timeDifference); @@ -491,4 +524,14 @@ Bangle.loadWidgets(); Bangle.drawWidgets(); Bangle.setUI("clock"); -draw(); +switch (settings.displaySeconds) { + case 0: // never + updateQueueMillisAndDraw(false); + break; + case 1: // unlocked + updateQueueMillisAndDraw(Bangle.isBacklightOn()); + break; + case 2: // always + updateQueueMillisAndDraw(true); + break; +} diff --git a/apps/elapsed_t/metadata.json b/apps/elapsed_t/metadata.json index fa0674e0b..2515e0e79 100644 --- a/apps/elapsed_t/metadata.json +++ b/apps/elapsed_t/metadata.json @@ -3,7 +3,7 @@ "name": "Elapsed Time Clock", "shortName": "Elapsed Time", "type": "clock", - "version":"0.03", + "version":"0.04", "description": "A clock that calculates the time difference between now and any given target date.", "tags": "clock,tool", "supports": ["BANGLEJS2"], diff --git a/apps/elapsed_t/settings.js b/apps/elapsed_t/settings.js index d3a7cb357..4726516d5 100644 --- a/apps/elapsed_t/settings.js +++ b/apps/elapsed_t/settings.js @@ -4,7 +4,7 @@ // Load settings var settings = Object.assign({ // default values - displaySeconds: true, + displaySeconds: 1, displayMonthsYears: true, dateFormat: 0, time24: true @@ -14,18 +14,26 @@ require('Storage').writeJSON(FILE, settings); } + if (typeof settings.displaySeconds === 'boolean') { + settings.displaySeconds = 1; + writeSettings(); + } + var dateFormats = ["DD/MM/YYYY", "MM/DD/YYYY", "YYYY-MM-DD"]; + var displaySecondsFormats = ["Never", "Unlocked", "Always"]; // Show the menu E.showMenu({ "" : { "title" : "Elapsed Time" }, "< Back" : () => back(), 'Show\nseconds': { - value: !!settings.displaySeconds, + value: settings.displaySeconds, + min: 0, max: 2, wrap: true, onchange: v => { settings.displaySeconds = v; writeSettings(); - } + }, + format: function (v) {return displaySecondsFormats[v];} }, 'Show months/\nyears': { value: !!settings.displayMonthsYears, diff --git a/apps/gbdiscon/ChangeLog b/apps/gbdiscon/ChangeLog new file mode 100644 index 000000000..5560f00bc --- /dev/null +++ b/apps/gbdiscon/ChangeLog @@ -0,0 +1 @@ +0.01: New App! diff --git a/apps/gbdiscon/app-icon.js b/apps/gbdiscon/app-icon.js new file mode 100644 index 000000000..81bf14884 --- /dev/null +++ b/apps/gbdiscon/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwwYHEgMkyVAkmQDJYREyQRRoARQpARQpIRRkARNggRBkgRNgARCwARNiQRBSRIREgQRBSRIREgARCSRARFhKSKCIoFCSRAjISQ0BAQJZHCI6ZBTwKPEI44tBTIMSYoZ9IBIYyEWZCHEKwbXIDwZ6MBghjBWBR7DIQbmJAAJ7BexYRHGZZHEchRrGNJYRIRpARJWI7XDCIrVHLIeACIpuIgKwBR4RcQyDLFCJbLGCJcAZZgLEiRcLCIkCZZYvFCKAjDI6BZOPqD+PWaUJa6ARCTxARICBQRFPRIRHPRIRHBg4A=")) diff --git a/apps/gbdiscon/app.js b/apps/gbdiscon/app.js new file mode 100644 index 000000000..1652ac506 --- /dev/null +++ b/apps/gbdiscon/app.js @@ -0,0 +1,7 @@ +{ +Bangle.setUI({mode:"custom",remove:()=>{}});"Bangle.loadWidgets"; // Allow fastloading. + +Bluetooth.println(JSON.stringify({t:"intent", action:"nodomain.freeyourgadget.gadgetbridge.BLUETOOTH_DISCONNECT", extra:{EXTRA_DEVICE_ADDRESS:NRF.getAddress()}})); + +Bangle.showClock(); +} diff --git a/apps/gbdiscon/app.png b/apps/gbdiscon/app.png new file mode 100644 index 000000000..f942c9ba0 Binary files /dev/null and b/apps/gbdiscon/app.png differ diff --git a/apps/gbdiscon/metadata.json b/apps/gbdiscon/metadata.json new file mode 100644 index 000000000..ecc92d01c --- /dev/null +++ b/apps/gbdiscon/metadata.json @@ -0,0 +1,13 @@ +{ "id": "gbdiscon", + "name": "Disconnect from Gadgetbridge", + "shortName":"Disconnect Gadgetbridge", + "version":"0.01", + "description": "Disconnect from your android device by running this app. The app will forward you to your clock face immediately after triggering the command. (Gadgetbridge nightly required until version 82 is released)", + "icon": "app.png", + "tags": "android, gadgetbridge, bluetooth, bt", + "supports" : ["BANGLEJS", "BANGLEJS2"], + "storage": [ + {"name":"gbdiscon.app.js","url":"app.js"}, + {"name":"gbdiscon.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/gbmusic/ChangeLog b/apps/gbmusic/ChangeLog index 90e5ed857..0275542fb 100644 --- a/apps/gbmusic/ChangeLog +++ b/apps/gbmusic/ChangeLog @@ -10,4 +10,5 @@ 0.10: Simplify touch events Remove date+time 0.11: Use default Bangle formatter for booleans -0.12: Issue newline before GB commands (solves issue with console.log and ignored commands) \ No newline at end of file +0.12: Issue newline before GB commands (solves issue with console.log and ignored commands) +0.13: Upgrade to new translation system diff --git a/apps/gbmusic/metadata.json b/apps/gbmusic/metadata.json index 0c73548cb..0024a1708 100644 --- a/apps/gbmusic/metadata.json +++ b/apps/gbmusic/metadata.json @@ -2,7 +2,7 @@ "id": "gbmusic", "name": "Gadgetbridge Music Controls", "shortName": "Music Controls", - "version": "0.12", + "version": "0.13", "description": "Control the music on your Gadgetbridge-connected phone", "icon": "icon.png", "screenshots": [{"url":"screenshot_v1_d.png"},{"url":"screenshot_v1_l.png"}, diff --git a/apps/gbmusic/settings.js b/apps/gbmusic/settings.js index 6619eab1c..9b8d35be9 100644 --- a/apps/gbmusic/settings.js +++ b/apps/gbmusic/settings.js @@ -3,8 +3,7 @@ */ (function(back) { const SETTINGS_FILE = "gbmusic.json", - storage = require("Storage"), - translate = require("locale").translate; + storage = require("Storage"); // initialize with default settings... let s = { @@ -28,12 +27,12 @@ let menu = { "": {"title": "Music Control"}, }; - menu[translate("< Back")] = back; - menu[translate("Auto start")] = { + menu["< Back"] = back; + menu[/*LANG*/"Auto start"] = { value: !!s.autoStart, onchange: save("autoStart"), }; - menu[translate("Simple button")] = { + menu[/*LANG*/"Simple button"] = { value: !!s.simpleButton, onchange: save("simpleButton"), }; diff --git a/apps/gipy/ChangeLog b/apps/gipy/ChangeLog index 79b3a7615..b0445c161 100644 --- a/apps/gipy/ChangeLog +++ b/apps/gipy/ChangeLog @@ -135,5 +135,9 @@ * Fix for files converted from maps2gpx : path was not reduced in size correctly * Experimental ski mode : have a ski slopes map * Fix for path projection display when lost and zoomed out + 0.25: Minor code improvements + 0.26: Add option to plot openstmap if installed + +0.27: Support for large paths (grid sizes > 65k) diff --git a/apps/gipy/app.js b/apps/gipy/app.js index 4d0838282..659e12d98 100644 --- a/apps/gipy/app.js +++ b/apps/gipy/app.js @@ -95,7 +95,7 @@ function compute_eta(hour, minutes, approximate_speed, remaining_distance) { } class TilesOffsets { - constructor(filename, offset) { + constructor(filename, offset, bytes_per_tile_index) { let header = E.toArrayBuffer(s.read(filename, offset, 4)); let type_size = Uint8Array(header, 0, 1)[0]; offset += 1; @@ -105,26 +105,30 @@ class TilesOffsets { offset += 2; let bytes = (type_size==24)?3:2; - let buffer = E.toArrayBuffer(s.read(filename, offset, 2*non_empty_tiles_number+bytes*non_empty_tiles_number)); - this.non_empty_tiles = Uint16Array(buffer, 0, non_empty_tiles_number); - offset += 2 * non_empty_tiles_number; + let buffer = E.toArrayBuffer(s.read(filename, offset, bytes_per_tile_index*non_empty_tiles_number)); + if (bytes_per_tile_index == 2) { + this.non_empty_tiles = Uint16Array(buffer, 0, non_empty_tiles_number); + } else { + this.non_empty_tiles = Uint24Array(buffer, 0, non_empty_tiles_number); + } + offset += bytes_per_tile_index * non_empty_tiles_number; + let tile_buffer = E.toArrayBuffer(s.read(filename, offset, bytes*non_empty_tiles_number)); if (type_size == 24) { this.non_empty_tiles_ends = Uint24Array( - buffer, - 2*non_empty_tiles_number, + tile_buffer, + 0, non_empty_tiles_number ); - offset += 3 * non_empty_tiles_number; } else if (type_size == 16) { this.non_empty_tiles_ends = Uint16Array( - buffer, - 2*non_empty_tiles_number, + tile_buffer, + 0, non_empty_tiles_number ); - offset += 2 * non_empty_tiles_number; } else { throw "unknown size"; } + offset += bytes * non_empty_tiles_number; return [this, offset]; } tile_start_offset(tile_index) { @@ -179,7 +183,8 @@ class Map { offset += 8; // tiles offsets - let res = new TilesOffsets(filename, offset); + let bytes_per_tile_index = (this.grid_size[0] * this.grid_size[1] > 65536)?3:2; + let res = new TilesOffsets(filename, offset, bytes_per_tile_index); this.tiles_offsets = res[0]; offset = res[1]; @@ -314,7 +319,8 @@ class Interests { this.side = side_array[0]; offset += 8; - let res = new TilesOffsets(filename, offset); + let bytes_per_tile_index = (this.grid_size[0] * this.grid_size[1] > 65536)?3:2; + let res = new TilesOffsets(filename, offset, bytes_per_tile_index); offset = res[1]; this.offsets = res[0]; let end = this.offsets.end_offset(); diff --git a/apps/gipy/metadata.json b/apps/gipy/metadata.json index 5819f2d4c..91e371c16 100644 --- a/apps/gipy/metadata.json +++ b/apps/gipy/metadata.json @@ -2,7 +2,7 @@ "id": "gipy", "name": "Gipy", "shortName": "Gipy", - "version": "0.26", + "version": "0.27", "description": "Follow gpx files using the gps. Don't get lost in your bike trips and hikes.", "allow_emulator":false, "icon": "gipy.png", diff --git a/apps/gipy/pkg/gps.d.ts b/apps/gipy/pkg/gps.d.ts index 6e2c14f5a..ebf3c8456 100644 --- a/apps/gipy/pkg/gps.d.ts +++ b/apps/gipy/pkg/gps.d.ts @@ -1,5 +1,5 @@ /* tslint:disable */ - +/* eslint-disable */ /** * @param {Gps} gps */ @@ -80,11 +80,11 @@ export interface InitOutput { readonly __wbindgen_malloc: (a: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number) => number; readonly __wbindgen_export_2: WebAssembly.Table; - readonly wasm_bindgen__convert__closures__invoke1_mut__hc18aa489d857d6a0: (a: number, b: number, c: number) => void; + readonly wasm_bindgen__convert__closures__invoke1_mut__h175ee3b9ff4e5b4c: (a: number, b: number, c: number) => void; readonly __wbindgen_add_to_stack_pointer: (a: number) => number; readonly __wbindgen_free: (a: number, b: number) => void; readonly __wbindgen_exn_store: (a: number) => void; - readonly wasm_bindgen__convert__closures__invoke2_mut__h41c3b5af183df3b2: (a: number, b: number, c: number, d: number) => void; + readonly wasm_bindgen__convert__closures__invoke2_mut__h41622a4cb7018e76: (a: number, b: number, c: number, d: number) => void; } export type SyncInitInput = BufferSource | WebAssembly.Module; diff --git a/apps/gipy/pkg/gps.js b/apps/gipy/pkg/gps.js index 0f8b74804..d98a5c05b 100644 --- a/apps/gipy/pkg/gps.js +++ b/apps/gipy/pkg/gps.js @@ -98,6 +98,14 @@ function takeObject(idx) { return ret; } +const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); + +cachedTextDecoder.decode(); + +function getStringFromWasm0(ptr, len) { + return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); +} + function addHeapObject(obj) { if (heap_next === heap.length) heap.push(heap.length + 1); const idx = heap_next; @@ -107,14 +115,6 @@ function addHeapObject(obj) { return idx; } -const cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }); - -cachedTextDecoder.decode(); - -function getStringFromWasm0(ptr, len) { - return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len)); -} - function debugString(val) { // primitive types const type = typeof val; @@ -205,7 +205,7 @@ function makeMutClosure(arg0, arg1, dtor, f) { return real; } function __wbg_adapter_24(arg0, arg1, arg2) { - wasm.wasm_bindgen__convert__closures__invoke1_mut__hc18aa489d857d6a0(arg0, arg1, addHeapObject(arg2)); + wasm.wasm_bindgen__convert__closures__invoke1_mut__h175ee3b9ff4e5b4c(arg0, arg1, addHeapObject(arg2)); } function _assertClass(instance, klass) { @@ -389,7 +389,7 @@ function handleError(f, args) { } } function __wbg_adapter_86(arg0, arg1, arg2, arg3) { - wasm.wasm_bindgen__convert__closures__invoke2_mut__h41c3b5af183df3b2(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); + wasm.wasm_bindgen__convert__closures__invoke2_mut__h41622a4cb7018e76(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); } /** @@ -464,10 +464,6 @@ function getImports() { imports.wbg.__wbindgen_object_drop_ref = function(arg0) { takeObject(arg0); }; - imports.wbg.__wbindgen_object_clone_ref = function(arg0) { - const ret = getObject(arg0); - return addHeapObject(ret); - }; imports.wbg.__wbindgen_string_new = function(arg0, arg1) { const ret = getStringFromWasm0(arg0, arg1); return addHeapObject(ret); @@ -476,6 +472,10 @@ function getImports() { const ret = fetch(getObject(arg0)); return addHeapObject(ret); }; + imports.wbg.__wbindgen_object_clone_ref = function(arg0) { + const ret = getObject(arg0); + return addHeapObject(ret); + }; imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) { const ret = getObject(arg0).signal; return addHeapObject(ret); @@ -695,8 +695,8 @@ function getImports() { const ret = wasm.memory; return addHeapObject(ret); }; - imports.wbg.__wbindgen_closure_wrapper2356 = function(arg0, arg1, arg2) { - const ret = makeMutClosure(arg0, arg1, 293, __wbg_adapter_24); + imports.wbg.__wbindgen_closure_wrapper2375 = function(arg0, arg1, arg2) { + const ret = makeMutClosure(arg0, arg1, 301, __wbg_adapter_24); return addHeapObject(ret); }; diff --git a/apps/gipy/pkg/gps_bg.wasm b/apps/gipy/pkg/gps_bg.wasm index c9e212a13..cfdf4be5f 100644 Binary files a/apps/gipy/pkg/gps_bg.wasm and b/apps/gipy/pkg/gps_bg.wasm differ diff --git a/apps/gipy/pkg/gps_bg.wasm.d.ts b/apps/gipy/pkg/gps_bg.wasm.d.ts index 5b84a9229..b6f04ad71 100644 --- a/apps/gipy/pkg/gps_bg.wasm.d.ts +++ b/apps/gipy/pkg/gps_bg.wasm.d.ts @@ -1,5 +1,5 @@ /* tslint:disable */ - +/* eslint-disable */ export const memory: WebAssembly.Memory; export function __wbg_gps_free(a: number): void; export function disable_elevation(a: number): void; @@ -14,8 +14,8 @@ export function gps_from_area(a: number, b: number, c: number, d: number, e: num export function __wbindgen_malloc(a: number): number; export function __wbindgen_realloc(a: number, b: number, c: number): number; export const __wbindgen_export_2: WebAssembly.Table; -export function wasm_bindgen__convert__closures__invoke1_mut__hc18aa489d857d6a0(a: number, b: number, c: number): void; +export function wasm_bindgen__convert__closures__invoke1_mut__h175ee3b9ff4e5b4c(a: number, b: number, c: number): void; export function __wbindgen_add_to_stack_pointer(a: number): number; export function __wbindgen_free(a: number, b: number): void; export function __wbindgen_exn_store(a: number): void; -export function wasm_bindgen__convert__closures__invoke2_mut__h41c3b5af183df3b2(a: number, b: number, c: number, d: number): void; +export function wasm_bindgen__convert__closures__invoke2_mut__h41622a4cb7018e76(a: number, b: number, c: number, d: number): void; diff --git a/apps/locale/ChangeLog b/apps/locale/ChangeLog index 982103cd1..64e477529 100644 --- a/apps/locale/ChangeLog +++ b/apps/locale/ChangeLog @@ -21,3 +21,4 @@ 0.17: Fix regression where long month names were 'undefined' (fix #1641) 0.18: Fix lint warnings, change anv->janv for fr_BE and fr_CH 0.19: Deprecate currency information +0.20: Improve support for meridians diff --git a/apps/locale/locale.html b/apps/locale/locale.html index b198b1c4b..71bfecea9 100644 --- a/apps/locale/locale.html +++ b/apps/locale/locale.html @@ -201,11 +201,14 @@ function round(n, dp) { var p = Math.max(0,Math.min(dp,dp - Math.floor(Math.log(n)/Math.log(10)))); return n.toFixed(p); } -var is12; +var _is12Hours; +function is12Hours() { + if (_is12Hours === undefined) _is12Hours = ${isLocal ? "false" : `(require('Storage').readJSON('setting.json', 1) || {})["12hour"]`}; + return _is12Hours; +} function getHours(d) { var h = d.getHours(); - if (is12 === undefined) is12 = ${isLocal ? "false" : `(require('Storage').readJSON('setting.json', 1) || {})["12hour"]`}; - if (!is12) return ('0' + h).slice(-2); + if (!is12Hours()) return ('0' + h).slice(-2); return ((h % 12 == 0) ? 12 : h % 12).toString(); } exports = { @@ -234,7 +237,8 @@ exports = { translate: s => ${locale.trans?`{var t=${js(locale.trans)};s=''+s;return t[s]||t[s.toLowerCase()]||s;}`:`s`}, date: (d,short) => short ? \`${dateS}\` : \`${dateN}\`, time: (d,short) => short ? \`${timeS}\` : \`${timeN}\`, - meridian: d => d.getHours() < 12 ? ${js(locale.ampm[0])}:${js(locale.ampm[1])}, + meridian: (d,force) => (force||is12Hours()) ? d.getHours() < 12 ? ${js(locale.ampm[0])}:${js(locale.ampm[1])} : "", + is12Hours, }; `.trim() }; @@ -306,8 +310,8 @@ ${customizeLocale ? `Time format `; document.getElementById("examples").innerHTML = ` Meridian - ${exports.meridian(new Date(0))} / - ${exports.meridian(new Date(43200000))} + ${exports.meridian(new Date(0), true)} / + ${exports.meridian(new Date(43200000), true)} ${customizeLocale ? `Meridian names diff --git a/apps/locale/metadata.json b/apps/locale/metadata.json index 36cb2eae7..c2f76ca8a 100644 --- a/apps/locale/metadata.json +++ b/apps/locale/metadata.json @@ -1,7 +1,7 @@ { "id": "locale", "name": "Languages", - "version": "0.19", + "version": "0.20", "description": "Translations for different countries", "icon": "locale.png", "type": "locale", diff --git a/apps/messagegui/ChangeLog b/apps/messagegui/ChangeLog index 7320d8ec6..bb3ac519e 100644 --- a/apps/messagegui/ChangeLog +++ b/apps/messagegui/ChangeLog @@ -106,3 +106,4 @@ 0.77: Messages can now use international fonts if they are installed 0.78: Fix: When user taps on a new message, clear the unread timeout 0.79: Fix: Reset the unread timeout each time a new message is shown. When the message is read from user input, do not set an unread timeout. +0.80: Add ability to reply to messages if a reply library is installed and the message can be replied to \ No newline at end of file diff --git a/apps/messagegui/app.js b/apps/messagegui/app.js index 9172bcad7..3b03adeaf 100644 --- a/apps/messagegui/app.js +++ b/apps/messagegui/app.js @@ -24,6 +24,8 @@ require("messages").pushMessage({"t":"add","id":"call","src":"Phone","title":"Bo var Layout = require("Layout"); var layout; // global var containing the layout for the currently displayed message var settings = require('Storage').readJSON("messages.settings.json", true) || {}; +var reply; +try { reply = require("reply"); } catch (e) {} var fontSmall = "6x8"; var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2"; var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2"; @@ -45,6 +47,7 @@ if (Graphics.prototype.setFontIntl) { var active; // active screen (undefined/"list"/"music"/"map"/"message"/"scroller"/"settings") var openMusic = false; // go back to music screen after we handle something else? +var replying = false; // If we're replying to a message, don't interrupt // hack for 2v10 firmware's lack of ':size' font handling try { g.setFont("6x8:2"); @@ -267,11 +270,31 @@ function showMessageSettings(msg) { /*LANG*/"View Message" : () => { showMessageScroller(msg); }, + }; + + if (msg.reply && reply) { + menu[/*LANG*/"Reply"] = () => { + replying = true; + reply.reply({msg: msg}) + .then(result => { + Bluetooth.println(JSON.stringify(result)); + replying = false; + showMessage(msg.id); + }) + .catch(() => { + replying = false; + showMessage(msg.id); + }); + }; + } + + menu = Object.assign(menu, { /*LANG*/"Delete" : () => { MESSAGES = MESSAGES.filter(m=>m.id!=msg.id); checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); }, - }; + }); + if (Bangle.messageIgnore && msg.src) menu[/*LANG*/"Ignore"] = () => { E.showPrompt(/*LANG*/"Ignore all messages from "+E.toJS(msg.src)+"?", {title:/*LANG*/"Ignore"}).then(isYes => { @@ -305,6 +328,7 @@ function showMessageSettings(msg) { } function showMessage(msgid, persist) { + if (replying) { return; } if(!persist) resetReloadTimeout(); let idx = MESSAGES.findIndex(m=>m.id==msgid); var msg = MESSAGES[idx]; @@ -374,15 +398,32 @@ function showMessage(msgid, persist) { }; footer.push({type:"img",src:atob("PhAB4A8AAAAAAAPAfAMAAAAAD4PwHAAAAAA/H4DwAAAAAH78B8AAAAAA/+A/AAAAAAH/Af//////w/gP//////8P4D///////H/Af//////z/4D8AAAAAB+/AfAAAAAA/H4DwAAAAAPg/AcAAAAADwHwDAAAAAA4A8AAAAAAAA=="),col:"#f00",cb:negHandler}); } footer.push({fillx:1}); // push images to left/right - if (msg.positive) { + if (msg.reply && reply) { + posHandler = ()=>{ + replying = true; + msg.new = false; + cancelReloadTimeout(); // don't auto-reload to clock now + reply.reply({msg: msg}) + .then(result => { + Bluetooth.println(JSON.stringify(result)); + replying = false; + layout.render(); + checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic}); + }) + .catch(() => { + replying = false; + layout.render(); + showMessage(msg.id); + }); + }; footer.push({type:"img",src:atob("QRABAAAAAAAH//+AAAAABgP//8AAAAADgf//4AAAAAHg4ABwAAAAAPh8APgAAAAAfj+B////////geHv///////hf+f///////GPw///////8cGBwAAAAAPx/gDgAAAAAfD/gHAAAAAA8DngOAAAAABwDHP8AAAAADACGf4AAAAAAAAM/w=="),col:"#0f0", cb:posHandler}); + } + else if (msg.positive) { posHandler = ()=>{ msg.new = false; cancelReloadTimeout(); // don't auto-reload to clock now Bangle.messageResponse(msg,true); checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic}); - }; - footer.push({type:"img",src:atob("QRABAAAAAAAAAAOAAAAABgAAA8AAAAADgAAD4AAAAAHgAAPgAAAAAPgAA+AAAAAAfgAD4///////gAPh///////gA+D///////AD4H//////8cPgAAAAAAPw8+AAAAAAAfB/4AAAAAAA8B/gAAAAAABwB+AAAAAAADAB4AAAAAAAAABgAA=="),col:"#0f0",cb:posHandler}); - + }; footer.push({type:"img",src:atob("QRABAAAAAAAAAAOAAAAABgAAA8AAAAADgAAD4AAAAAHgAAPgAAAAAPgAA+AAAAAAfgAD4///////gAPh///////gA+D///////AD4H//////8cPgAAAAAAPw8+AAAAAAAfB/4AAAAAAA8B/gAAAAAABwB+AAAAAAADAB4AAAAAAAAABgAA=="),col:"#0f0",cb:posHandler}); } layout = new Layout({ type:"v", c: [ diff --git a/apps/messagegui/metadata.json b/apps/messagegui/metadata.json index 83056fc0c..1e6913aac 100644 --- a/apps/messagegui/metadata.json +++ b/apps/messagegui/metadata.json @@ -2,13 +2,13 @@ "id": "messagegui", "name": "Message UI", "shortName": "Messages", - "version": "0.79", + "version": "0.80", "description": "Default app to display notifications from iOS and Gadgetbridge/Android", "icon": "app.png", "type": "app", "tags": "tool,system", "supports": ["BANGLEJS","BANGLEJS2"], - "dependencies" : { "messageicons":"module" }, + "dependencies" : { "messageicons":"module", "reply": "module" }, "provides_modules": ["messagegui"], "default": true, "readme": "README.md", diff --git a/apps/messageicons/ChangeLog b/apps/messageicons/ChangeLog index 4e8972f7a..ede169914 100644 --- a/apps/messageicons/ChangeLog +++ b/apps/messageicons/ChangeLog @@ -6,3 +6,4 @@ 0.05: Add message icon for 'jira' 0.06: Add message icon for 'molly' and 'threema libre' 0.07: Minor code improvements +0.08: Add more icons including GMail, Google Messages, Google Agenda \ No newline at end of file diff --git a/apps/messageicons/icons.img b/apps/messageicons/icons.img index 66ecb53f8..bb74d05d9 100644 Binary files a/apps/messageicons/icons.img and b/apps/messageicons/icons.img differ diff --git a/apps/messageicons/icons/agenda.png b/apps/messageicons/icons/agenda.png new file mode 100644 index 000000000..f9328facc Binary files /dev/null and b/apps/messageicons/icons/agenda.png differ diff --git a/apps/messageicons/icons/generate.js b/apps/messageicons/icons/generate.js index 854e04143..a07b7af44 100755 --- a/apps/messageicons/icons/generate.js +++ b/apps/messageicons/icons/generate.js @@ -91,6 +91,7 @@ exports.getColor = function(msg,options) { const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase(); return { /* generic colors, using B2-safe colors */ ${ /* DO NOT USE BLACK OR WHITE HERE, just leave the declaration out and then the theme's fg color will be used */"" } + "agenda": "#206cd5", "airbnb": "#ff385c", // https://news.airbnb.com/media-assets/category/brand/ "mail": "#ff0", "music": "#f0f", @@ -111,8 +112,10 @@ exports.getColor = function(msg,options) { // "home assistant": "#41bdf5", // ha-blue is #41bdf5, but that's the background "instagram": "#ff0069", // https://about.instagram.com/brand/gradient "jira": "#0052cc", //https://atlassian.design/resources/logo-library + "leboncoin": "#fa7321", "lieferando": "#ff8000", "linkedin": "#0a66c2", // https://brand.linkedin.com/ + "messages": "#0a5cce", "messenger": "#0078ff", "mastodon": "#563acc", // https://www.joinmastodon.org/branding "mattermost": "#00f", diff --git a/apps/messageicons/icons/gmail.png b/apps/messageicons/icons/gmail.png new file mode 100644 index 000000000..b96465db1 Binary files /dev/null and b/apps/messageicons/icons/gmail.png differ diff --git a/apps/messageicons/icons/icon_names.json b/apps/messageicons/icons/icon_names.json index de8be9e98..89f81be08 100644 --- a/apps/messageicons/icons/icon_names.json +++ b/apps/messageicons/icons/icon_names.json @@ -1,6 +1,7 @@ [ { "app":"default", "icon":"default.png" }, { "app":"airbnb", "icon":"airbnb.png" }, + { "app":"agenda", "icon":"agenda.png" }, { "app":"alarm", "icon":"alarm.png" }, { "app":"alarmclockreceiver", "icon":"alarm.png" }, { "app":"amazon shopping", "icon":"amazon.png" }, @@ -36,6 +37,7 @@ { "app":"aurora droid", "icon":"security.png" }, { "app":"github", "icon":"github.png" }, { "app":"gitlab", "icon":"gitlab.png" }, + { "app":"gmail", "icon":"gmail.png" }, { "app":"gmx", "icon":"gmx.png" }, { "app":"google", "icon":"google.png" }, { "app":"google home", "icon":"google home.png" }, @@ -45,6 +47,7 @@ { "app":"jira", "icon":"jira.png" }, { "app":"kalender", "icon":"kalender.png" }, { "app":"keep notes", "icon":"google keep.png" }, + { "app":"leboncoin", "icon":"leboncoin.png" }, { "app":"lieferando", "icon":"lieferando.png" }, { "app":"linkedin", "icon":"linkedin.png" }, { "app":"maps", "icon":"map.png" }, @@ -55,6 +58,7 @@ { "app":"tooot", "icon":"mastodon.png" }, { "app":"tusky", "icon":"mastodon.png" }, { "app":"mattermost", "icon":"mattermost.png" }, + { "app":"messages", "icon":"messages.png" }, { "app":"n26", "icon":"n26.png" }, { "app":"netflix", "icon":"netflix.png" }, { "app":"news", "icon":"news.png" }, @@ -110,6 +114,5 @@ { "app":"meet", "icon":"videoconf.png" }, { "app":"music", "icon":"music.png" }, { "app":"sms message", "icon":"default.png" }, - { "app":"mail", "icon":"default.png" }, - { "app":"gmail", "icon":"default.png" } + { "app":"mail", "icon":"default.png" } ] diff --git a/apps/messageicons/icons/leboncoin.png b/apps/messageicons/icons/leboncoin.png new file mode 100644 index 000000000..fb41bf03f Binary files /dev/null and b/apps/messageicons/icons/leboncoin.png differ diff --git a/apps/messageicons/icons/messages.png b/apps/messageicons/icons/messages.png new file mode 100644 index 000000000..9d89da001 Binary files /dev/null and b/apps/messageicons/icons/messages.png differ diff --git a/apps/messageicons/lib.js b/apps/messageicons/lib.js index b3ff2d9ff..9c070a03e 100644 --- a/apps/messageicons/lib.js +++ b/apps/messageicons/lib.js @@ -4,7 +4,7 @@ exports.getImage = function(msg) { if (msg.img) return atob(msg.img); let s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase(); if (msg.id=="music") s="music"; - let match = ",default|0,airbnb|1,alarm|2,alarmclockreceiver|2,amazon shopping|3,bibel|4,bitwarden|5,1password|5,lastpass|5,dashlane|5,bring|6,calendar|7,etar|7,chat|8,chrome|9,clock|2,corona-warn|10,bmo|11,desjardins|11,rbc mobile|11,nbc|11,rabobank|11,scotiabank|11,td (canada)|11,discord|12,drive|13,element|14,facebook|15,messenger|16,firefox|17,firefox beta|17,firefox nightly|17,f-droid|5,neo store|5,aurora droid|5,github|18,gitlab|19,gmx|20,google|21,google home|22,google play store|23,home assistant|24,instagram|25,jira|26,kalender|27,keep notes|28,lieferando|29,linkedin|30,maps|31,organic maps|31,osmand|31,mastodon|32,fedilab|32,tooot|32,tusky|32,mattermost|33,n26|34,netflix|35,news|36,cbc news|36,rc info|36,reuters|36,ap news|36,la presse|36,nbc news|36,nextbike|37,nina|38,outlook mail|39,paypal|40,phone|41,plex|42,pocket|43,post & dhl|44,proton mail|45,reddit|46,sync pro|46,sync dev|46,boost|46,infinity|46,slide|46,signal|47,molly|47,skype|48,slack|49,snapchat|50,starbucks|51,steam|52,teams|53,telegram|54,telegram foss|54,threema|55,threema libre|55,tiktok|56,to do|57,opentasks|57,tasks|57,transit|58,twitch|59,twitter|60,uber|61,lyft|61,vlc|62,warnapp|63,whatsapp|64,wordfeud|65,youtube|66,newpipe|66,zoom|67,meet|67,music|68,sms message|0,mail|0,gmail|0,".match(new RegExp(`,${s}\\|(\\d+)`)) + let match = ",default|0,airbnb|1,agenda|2,alarm|3,alarmclockreceiver|3,amazon shopping|4,bibel|5,bitwarden|6,1password|6,lastpass|6,dashlane|6,bring|7,calendar|8,etar|8,chat|9,chrome|10,clock|3,corona-warn|11,bmo|12,desjardins|12,rbc mobile|12,nbc|12,rabobank|12,scotiabank|12,td (canada)|12,discord|13,drive|14,element|15,facebook|16,messenger|17,firefox|18,firefox beta|18,firefox nightly|18,f-droid|6,neo store|6,aurora droid|6,github|19,gitlab|20,gmail|21,gmx|22,google|23,google home|24,google play store|25,home assistant|26,instagram|27,jira|28,kalender|29,keep notes|30,leboncoin|31,lieferando|32,linkedin|33,maps|34,organic maps|34,osmand|34,mastodon|35,fedilab|35,tooot|35,tusky|35,mattermost|36,messages|37,n26|38,netflix|39,news|40,cbc news|40,rc info|40,reuters|40,ap news|40,la presse|40,nbc news|40,nextbike|41,nina|42,outlook mail|43,paypal|44,phone|45,plex|46,pocket|47,post & dhl|48,proton mail|49,reddit|50,sync pro|50,sync dev|50,boost|50,infinity|50,slide|50,signal|51,molly|51,skype|52,slack|53,snapchat|54,starbucks|55,steam|56,teams|57,telegram|58,telegram foss|58,threema|59,threema libre|59,tiktok|60,to do|61,opentasks|61,tasks|61,transit|62,twitch|63,twitter|64,uber|65,lyft|65,vlc|66,warnapp|67,whatsapp|68,wordfeud|69,youtube|70,newpipe|70,zoom|71,meet|71,music|72,sms message|0,mail|0,".match(new RegExp(`,${s}\\|(\\d+)`)) return require("Storage").read("messageicons.img", (match===null)?0:match[1]*76, 76); }; @@ -16,6 +16,7 @@ exports.getColor = function(msg,options) { const s = (("string"=== typeof msg) ? msg : (msg.src || "")).toLowerCase(); return { /* generic colors, using B2-safe colors */ + "agenda": "#206cd5", "airbnb": "#ff385c", // https://news.airbnb.com/media-assets/category/brand/ "mail": "#ff0", "music": "#f0f", @@ -33,8 +34,10 @@ exports.getColor = function(msg,options) { // "home assistant": "#41bdf5", // ha-blue is #41bdf5, but that's the background "instagram": "#ff0069", // https://about.instagram.com/brand/gradient "jira": "#0052cc", //https://atlassian.design/resources/logo-library + "leboncoin": "#fa7321", "lieferando": "#ff8000", "linkedin": "#0a66c2", // https://brand.linkedin.com/ + "messages": "#0a5cce", "messenger": "#0078ff", "mastodon": "#563acc", // https://www.joinmastodon.org/branding "mattermost": "#00f", diff --git a/apps/messageicons/metadata.json b/apps/messageicons/metadata.json index 13ae5f232..356eeba79 100644 --- a/apps/messageicons/metadata.json +++ b/apps/messageicons/metadata.json @@ -1,7 +1,7 @@ { "id": "messageicons", "name": "Message Icons", - "version": "0.07", + "version": "0.08", "description": "Library containing a list of icons and colors for apps", "icon": "app.png", "type": "module", diff --git a/apps/openlocatebeacon/ChangeLog b/apps/openlocatebeacon/ChangeLog new file mode 100644 index 000000000..4a396d83f --- /dev/null +++ b/apps/openlocatebeacon/ChangeLog @@ -0,0 +1,4 @@ +0.01: New App! +0.02: Corrected NaN test for GPS +0.03: Removed remaining invalid references to Number.isFinite +0.04: Improved menu/display interaction diff --git a/apps/openlocatebeacon/README.md b/apps/openlocatebeacon/README.md new file mode 100644 index 000000000..944bb51bf --- /dev/null +++ b/apps/openlocatebeacon/README.md @@ -0,0 +1,19 @@ +# OpenLocate Beacon + +Collect geolocation sensor data from the Bangle.js 2's GPS and barometer, display the live readings on-screen, and broadcast in Bluetooth Low Energy (BLE) OpenLocate Beacon packets (LCI over BLE) to any listening devices in range. + +## Usage + +The advertising packets will be recognised by [Pareto Anywhere](https://www.reelyactive.com/pareto/anywhere/) open source middleware and any other program which observes the standard packet types. See our [Bangle.js Development Guide](https://reelyactive.github.io/diy/banglejs-dev/) for details. + +## Features + +Advertises packets with the OpenLocate Beacon geolocation element when a GPS fix is available, and packets with the name "Bangle.js" otherwise. + +## Requests + +[Contact reelyActive](https://www.reelyactive.com/contact/) for support/updates. + +## Creator + +Developed by [jeffyactive](https://github.com/jeffyactive) of [reelyActive](https://www.reelyactive.com) diff --git a/apps/openlocatebeacon/metadata.json b/apps/openlocatebeacon/metadata.json new file mode 100644 index 000000000..85f7e2fee --- /dev/null +++ b/apps/openlocatebeacon/metadata.json @@ -0,0 +1,19 @@ +{ + "id": "openlocatebeacon", + "name": "OpenLocate Beacon", + "shortName": "OpenLocate Beacon", + "version": "0.04", + "description": "Advertise GPS geolocation data using the OpenLocate Beacon packet specification.", + "icon": "openlocatebeacon.png", + "screenshots": [], + "type": "app", + "tags": "tool,sensors,bluetooth", + "supports" : [ "BANGLEJS2" ], + "allow_emulator": true, + "readme": "README.md", + "storage": [ + { "name": "openlocatebeacon.app.js", "url": "openlocatebeacon.js" }, + { "name": "openlocatebeacon.img", "url": "openlocatebeacon-icon.js", + "evaluate": true } + ] +} \ No newline at end of file diff --git a/apps/openlocatebeacon/openlocatebeacon-icon.js b/apps/openlocatebeacon/openlocatebeacon-icon.js new file mode 100644 index 000000000..7e838aaa0 --- /dev/null +++ b/apps/openlocatebeacon/openlocatebeacon-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwhHXAB+sr1dnMzmcPh4DBnNdr2sDyAsOropCABczroyaxE5FhoAFnOIFqutFqgxE1ouSrwdHnWIxGz2YPBAYIHBnQTHrwuQLgwsBURwyGnIWN2bmFnR4S1oxFmZyCFx0zLY2r0md4AACzuk1ZjGDoowKCAk6JwuBFggAFzuBOApiEmYuIBwjSE2ddvOcFxIABzl5rpWErxQJN4QuIPIN5FpYADvKlFGAivGHZAuSGBCDEFwg6EOoZnBFyQwCK4mzQg4IECIYuBqzqKehVWDwxWFLwc5M4czLxPB5HP5/I4JgJmYfDnJgFEwJeFG4MyLw4tB6wPB6wxBMA8yRAhgDHAOtY44FBq2cFw3Qa4nX6AwGziQBEIwAB1o1DHoyOG4PPFwoAB56SGSAKBGA4SVDBgc6AwKOG45eGMAXHSAwbBnSQGnIvD1psFF43IXgQAF6yQGF4SQDXQczdwezF5pfJF5uzF4bED1gACF5KPSKoMzEY0PgAAJuQdFMAIvH5wQGuQkKABVOzhgG57BE63PLw2cpwvVkgvGGAPI5/Q6HP5AuGF4MkF6pgIAAPB4/H4ILHLxsrMBbBHABlyLxcrF5ZgKABJeOqyRMMCVyEBlWwLyNywuPyzsNwPXeS6NTAAPX67AMSKCNNXwIvBSBqRBGBlyRpqOCAAIRNSJiNPRwQABqwwPF5IuPqwvD1gUOSJKNPgGsF4bBPgEqSI2clQYOXoYADlbCUXiErFwyRQgCREuQVPRoqRTkmWFwOWXh6NHGCaRBRqAuLGCIAQFxowgFx70ClYtZlbqJMUZcRAA1WFqdWFq5jESp0rLbAyJq0rGggFBqwsSA==")) \ No newline at end of file diff --git a/apps/openlocatebeacon/openlocatebeacon.js b/apps/openlocatebeacon/openlocatebeacon.js new file mode 100644 index 000000000..0fa60419e --- /dev/null +++ b/apps/openlocatebeacon/openlocatebeacon.js @@ -0,0 +1,121 @@ +/** + * Copyright reelyActive 2024 + * We believe in an open Internet of Things + */ + + +// Non-user-configurable constants +const APP_ID = 'openlocatebeacon'; +const ADVERTISING_OPTIONS = { showName: false, interval: 5000 }; + + +// Global variables +let bar, gps; +let sequenceNumber = 0; + + +// Menus +let mainMenu = { + "": { "title": "OpenLocateBcn" }, + "Lat": { value: null }, + "Lon": { value: null }, + "Altitude": { value: null }, + "Satellites": { value: null } +}; + + +// Encode the OpenLocate geo location element advertising packet +function encodeGeoLocationElement() { + let lci = new Uint8Array(16); + let seqFrag = ((sequenceNumber++ & 0x0f) << 4) + 0x01; + let rfc6225lat = toRfc6225Coordinate(gps.lat); + let rfc6225lon = toRfc6225Coordinate(gps.lon); + let rfc6225alt = toRfc6225Altitude(bar.altitude); + lci[0] = rfc6225lat.integer >> 7; + lci[1] = ((rfc6225lat.integer & 0xff) << 1) + (rfc6225lat.fraction >> 24); + lci[2] = (rfc6225lat.fraction >> 16) & 0xff; + lci[3] = (rfc6225lat.fraction >> 8) & 0xff; + lci[4] = rfc6225lat.fraction & 0xff; + lci[5] = rfc6225lon.integer >> 7; + lci[6] = ((rfc6225lon.integer & 0xff) << 1) + (rfc6225lon.fraction >> 24); + lci[7] = (rfc6225lon.fraction >> 16) & 0xff; + lci[8] = (rfc6225lon.fraction >> 8) & 0xff; + lci[9] = rfc6225lon.fraction & 0xff; + lci[10] = bar.altitude ? 0x10 : 0x00; + lci[11] = (rfc6225alt.integer >> 16) & 0xff; + lci[12] = (rfc6225alt.integer >> 8) & 0xff; + lci[13] = rfc6225alt.integer & 0xff; + lci[14] = rfc6225alt.fraction & 0xff; + lci[15] = 0x41; + + return [ + 0x02, 0x01, 0x06, // Flags + 0x16, 0x16, 0x94, 0xfd, 0x09, seqFrag, 0x30, lci[0], lci[1], lci[2], + lci[3], lci[4], lci[5], lci[6], lci[7], lci[8], lci[9], lci[10], lci[11], + lci[12], lci[13], lci[14], lci[15] + ]; +} + + +// Convert a latitude or longitude coordinate to RFC6225 +function toRfc6225Coordinate(coordinate) { + let integer = Math.floor(coordinate); + let fraction = Math.round((coordinate - integer) * 0x1ffffff); + + if(integer < 0) { + integer += 0x1ff + 1; + } + + return { integer: integer, fraction: fraction }; +} + + +// Convert altitude to RFC6225 +function toRfc6225Altitude(altitude) { + if(!altitude) { + return { integer: 0, fraction: 0 }; + } + + let integer = Math.floor(altitude); + let fraction = Math.round((altitude - integer) * 0xff); + + if(integer < 0) { + integer += 0x3fffff + 1; + } + + return { integer: integer, fraction: fraction }; +} + + +// Update barometer +Bangle.on('pressure', (newBar) => { + bar = newBar; + + mainMenu.Altitude.value = bar.altitude.toFixed(1) + 'm'; + E.showMenu(mainMenu); +}); + + +// Update GPS +Bangle.on('GPS', (newGps) => { + gps = newGps; + + mainMenu.Lat.value = gps.lat.toFixed(4); + mainMenu.Lon.value = gps.lon.toFixed(4); + mainMenu.Satellites.value = gps.satellites; + E.showMenu(mainMenu); + + if(!isNaN(gps.lat) && !isNaN(gps.lon)) { + NRF.setAdvertising(encodeGeoLocationElement(), ADVERTISING_OPTIONS); + } + else { + NRF.setAdvertising({}, { name: "Bangle.js" }); + } +}); + + +// On start: enable sensors and display main menu +g.clear(); +Bangle.setGPSPower(true, APP_ID); +Bangle.setBarometerPower(true, APP_ID); +E.showMenu(mainMenu); \ No newline at end of file diff --git a/apps/openlocatebeacon/openlocatebeacon.png b/apps/openlocatebeacon/openlocatebeacon.png new file mode 100644 index 000000000..b3294dae0 Binary files /dev/null and b/apps/openlocatebeacon/openlocatebeacon.png differ diff --git a/apps/pomodo/README.md b/apps/pomodo/README.md new file mode 100644 index 000000000..5c33e5231 --- /dev/null +++ b/apps/pomodo/README.md @@ -0,0 +1,14 @@ +# Pomodoro + +> The Pomodoro Technique is a time management method developed by Francesco Cirillo in the late 1980s. It uses a kitchen timer to break work into intervals, typically 25 minutes in length, separated by short breaks. Each interval is known as a pomodoro, from the Italian word for tomato, after the tomato-shaped kitchen timer Cirillo used as a university student. +> +> The original technique has six steps: +> +> Decide on the task to be done. +> Set the Pomodoro timer (typically for 25 minutes). +> Work on the task. +> End work when the timer rings and take a short break (typically 5–10 minutes). +> Go back to Step 2 and repeat until you complete four pomodori. +> After four pomodori are done, take a long break (typically 20 to 30 minutes) instead of a short break. Once the long break is finished, return to step 2. + +*Description gathered from https://en.wikipedia.org/wiki/Pomodoro_Technique* diff --git a/apps/pomodo/metadata.json b/apps/pomodo/metadata.json index a60009555..509e83fcc 100644 --- a/apps/pomodo/metadata.json +++ b/apps/pomodo/metadata.json @@ -7,6 +7,7 @@ "type": "app", "tags": "pomodoro,cooking,tools", "supports": ["BANGLEJS", "BANGLEJS2"], + "readme": "README.md", "allow_emulator": true, "screenshots": [{"url":"bangle2-pomodoro-screenshot.png"}], "storage": [ diff --git a/apps/pomoplus/ChangeLog b/apps/pomoplus/ChangeLog index 96104469b..327ce07c2 100644 --- a/apps/pomoplus/ChangeLog +++ b/apps/pomoplus/ChangeLog @@ -2,3 +2,6 @@ 0.02-0.04: Bug fixes 0.05: Submitted to the app loader 0.06: Added setting to show clock after start/resume +0.07: Make fonts and buttons larger for legibility and ease of use. Hide + buttons when screen is locked. Toggle the graphical presentation when + pressing the (middle) hardware button. diff --git a/apps/pomoplus/README.md b/apps/pomoplus/README.md new file mode 100644 index 000000000..98642debf --- /dev/null +++ b/apps/pomoplus/README.md @@ -0,0 +1,42 @@ +# Pomodoro Plus + +> The Pomodoro Technique is a time management method developed by Francesco Cirillo in the late 1980s. It uses a kitchen timer to break work into intervals, typically 25 minutes in length, separated by short breaks. Each interval is known as a pomodoro, from the Italian word for tomato, after the tomato-shaped kitchen timer Cirillo used as a university student. +> +> The original technique has six steps: +> +> Decide on the task to be done. +> Set the Pomodoro timer (typically for 25 minutes). +> Work on the task. +> End work when the timer rings and take a short break (typically 5–10 minutes). +> Go back to Step 2 and repeat until you complete four pomodori. +> After four pomodori are done, take a long break (typically 20 to 30 minutes) instead of a short break. Once the long break is finished, return to step 2. + +*Description gathered from https://en.wikipedia.org/wiki/Pomodoro_Technique* + +## Usage + +- Click the play button to start a pomodoro and get to work! +- Click the pause button if you're interrupted with something urgent. +- Click the cross button if you need to end your work session. +- Click the skip button if you forgot to start the pomodoro after the urgent interruption and ended up working for a long time! (Good on ya'!) +- Press the (middle) hardware button to toggle visibility of widgets and software buttons. + +Configure the pomodori and break times in the settings. + +## Features + +Continues to run in the background if you exit the app while the pomodoro timer is running. + +The buttons and widgets hide automatically when the screen is locked. + +## Requests + +Open an issue on the espruino/BangleApps issue tracker. + +## Creator + +bruceblore + +## Contributors + +notEvil, thyttan diff --git a/apps/pomoplus/app.js b/apps/pomoplus/app.js index a9e21b98a..56275efb4 100644 --- a/apps/pomoplus/app.js +++ b/apps/pomoplus/app.js @@ -4,6 +4,7 @@ Bangle.POMOPLUS_ACTIVE = true; //Prevent the boot code from running. To avoid h const storage = require("Storage"); const common = require("pomoplus-com.js"); +const wu = require("widget_utils"); //Expire the state if necessary if ( @@ -14,37 +15,45 @@ if ( common.state = common.STATE_DEFAULT; } +const W = g.getWidth(); +const H = g.getHeight(); +const SCALING = W/176; // The UI was tweaked to look good on a Bangle.js 2 (176x176 px). SCALING automatically adapts so elements have the same proportions relative to the screen size on devices with other resolutions. +const BUTTON_HEIGHT = 56 * SCALING; +const BUTTON_TOP = H - BUTTON_HEIGHT; + function drawButtons() { - let w = g.getWidth(); - let h = g.getHeight(); //Draw the backdrop - const BAR_TOP = h - 24; - g.setColor(0, 0, 1).setFontAlign(0, -1) - .clearRect(0, BAR_TOP, w, h) - .fillRect(0, BAR_TOP, w, h) + const ICONS_SIZE = 24; + const ICONS_ANCHOR_Y = BUTTON_TOP + BUTTON_HEIGHT / 2 - ICONS_SIZE / 2; + g.setColor(0, 0, 1) + .fillRect({x:0, y:BUTTON_TOP, x2:W-1, y2:H-1,r:15*SCALING}) .setColor(1, 1, 1); if (!common.state.wasRunning) { //If the timer was never started, only show a play button - g.drawImage(common.BUTTON_ICONS.play, w / 2, BAR_TOP); + g.drawImage(common.BUTTON_ICONS.play, W / 2 - ICONS_SIZE / 2, ICONS_ANCHOR_Y); } else { - g.drawLine(w / 2, BAR_TOP, w / 2, h); + g.setColor(g.theme.bg) + .fillRect(W / 2 - 2, BUTTON_TOP, W / 2 + 2, H) + .setColor(1,1,1); if (common.state.running) { - g.drawImage(common.BUTTON_ICONS.pause, w / 4, BAR_TOP) - .drawImage(common.BUTTON_ICONS.skip, w * 3 / 4, BAR_TOP); + g.drawImage(common.BUTTON_ICONS.pause, W / 4 - ICONS_SIZE / 2, ICONS_ANCHOR_Y) + .drawImage(common.BUTTON_ICONS.skip, W * 3 / 4 - ICONS_SIZE / 2, ICONS_ANCHOR_Y); } else { - g.drawImage(common.BUTTON_ICONS.reset, w / 4, BAR_TOP) - .drawImage(common.BUTTON_ICONS.play, w * 3 / 4, BAR_TOP); + g.drawImage(common.BUTTON_ICONS.reset, W / 4 - ICONS_SIZE / 2, ICONS_ANCHOR_Y) + .drawImage(common.BUTTON_ICONS.play, W * 3 / 4 - ICONS_SIZE / 2, ICONS_ANCHOR_Y); } } } function drawTimerAndMessage() { - let w = g.getWidth(); - let h = g.getHeight(); + const ANCHOR_X = W / 2; + const ANCHOR_Y = H * 3 / 8; + const TIME_SIZE = 48 * SCALING; + const LABEL_SIZE = 18 * SCALING; g.reset() .setFontAlign(0, 0) - .setFont("Vector", 36) - .clearRect(w / 2 - 60, h / 2 - 34, w / 2 + 60, h / 2 + 34) + .setFont("Vector", TIME_SIZE) + .clearRect(0, ANCHOR_Y - TIME_SIZE / 2, W-1, ANCHOR_Y + TIME_SIZE / 2 + 1.2 * LABEL_SIZE) //Draw the timer .drawString((() => { @@ -59,17 +68,17 @@ function drawTimerAndMessage() { if (hours >= 1) return `${parseInt(hours)}:${pad(minutes)}:${pad(seconds)}`; else return `${parseInt(minutes)}:${pad(seconds)}`; - })(), w / 2, h / 2) + })(), ANCHOR_X, ANCHOR_Y) //Draw the phase label - .setFont("Vector", 12) + .setFont("Vector", LABEL_SIZE) .drawString(((currentPhase, numShortBreaks) => { if (!common.state.wasRunning) return "Not started"; else if (currentPhase == common.PHASE_WORKING) return `Work ${numShortBreaks + 1}/${common.settings.numShortBreaks + 1}` else if (currentPhase == common.PHASE_SHORT_BREAK) return `Short break ${numShortBreaks + 1}/${common.settings.numShortBreaks}`; else return "Long break!"; })(common.state.phase, common.state.numShortBreaks), - w / 2, h / 2 + 18); + ANCHOR_X, ANCHOR_Y + 2*LABEL_SIZE); //Update phase with vibation if needed if (common.getTimeLeft() <= 0) { @@ -77,11 +86,35 @@ function drawTimerAndMessage() { } } -drawButtons(); -Bangle.on("touch", (button, xy) => { +if (!Bangle.isLocked()) drawButtons(); + +let hideButtons = ()=>{ + g.clearRect(0,BUTTON_TOP,W-1,H-1); +} + +let graphicState = 0; // 0 - all is visible, 1 - widgets are hidden, 2 - widgets and buttons are hidden. +let onButtonSwitchGraphics = (n)=>{ + if (process.env.HWVERSION == 2) n=2; // Translate Bangle.js 2 button to Bangle.js 1 middle button. + if (n == 2) { + if (graphicState == 0) { + wu.hide(); + } + if (graphicState == 1) { + hideButtons(); + } + if (graphicState == 2) { + wu.show(); + drawButtons(); + } + graphicState = (graphicState+1) % 3; + } +} + +let onTouchSoftwareButtons = (button, xy) => { //If we support full touch and we're not touching the keys, ignore. //If we don't support full touch, we can't tell so just assume we are. - if (xy !== undefined && xy.y <= g.getHeight() - 24) return; + let isOutsideButtonArea = xy !== undefined && xy.y <= g.getHeight() - BUTTON_HEIGHT; + if (isOutsideButtonArea || graphicState == 2) return; if (!common.state.wasRunning) { //If we were never running, there is only one button: the start button @@ -137,7 +170,13 @@ Bangle.on("touch", (button, xy) => { if (common.settings.showClock) Bangle.showClock(); } } -}); +}; + +Bangle.setUI({ + mode: "custom", + touch: onTouchSoftwareButtons, + btn: onButtonSwitchGraphics +}) let timerInterval; @@ -156,6 +195,18 @@ if (common.state.running) { setupTimerInterval(); } +Bangle.on('lock', (on, reason) => { + if (graphicState==2) return; + if (on) { + hideButtons(); + wu.hide(); + } + if (!on) { + drawButtons(); + if (graphicState==0) wu.show(); + } +}); + //Save our state when the app is closed E.on('kill', () => { storage.writeJSON(common.STATE_PATH, common.state); @@ -163,3 +214,4 @@ E.on('kill', () => { Bangle.loadWidgets(); Bangle.drawWidgets(); +if (Bangle.isLocked()) wu.hide(); diff --git a/apps/pomoplus/metadata.json b/apps/pomoplus/metadata.json index 4f2fd6cbb..7b1efee2a 100644 --- a/apps/pomoplus/metadata.json +++ b/apps/pomoplus/metadata.json @@ -1,7 +1,7 @@ { "id": "pomoplus", "name": "Pomodoro Plus", - "version": "0.06", + "version": "0.07", "description": "A configurable pomodoro timer that runs in the background.", "icon": "icon.png", "type": "app", @@ -10,6 +10,7 @@ "BANGLEJS", "BANGLEJS2" ], + "readme": "README.md", "storage": [ { "name": "pomoplus.app.js", diff --git a/apps/reply/ChangeLog b/apps/reply/ChangeLog index f3c7b0d2c..a5a65b116 100644 --- a/apps/reply/ChangeLog +++ b/apps/reply/ChangeLog @@ -1 +1,2 @@ -0.01: New Library! \ No newline at end of file +0.01: New Library! +0.02: Minor bug fixes \ No newline at end of file diff --git a/apps/reply/lib.js b/apps/reply/lib.js index 4a040c557..cdf394bb4 100644 --- a/apps/reply/lib.js +++ b/apps/reply/lib.js @@ -6,7 +6,12 @@ exports.reply = function (options) { keyboard = null; } - function constructReply(msg, replyText, resolve) { + function constructReply(msg, replyText, resolve, reject) { + if (!replyText) { + reject(""); + return; + } + var responseMessage = {msg: replyText}; if (msg.id) { responseMessage = { t: "notify", id: msg.id, n: "REPLY", msg: replyText }; @@ -29,7 +34,10 @@ exports.reply = function (options) { }, // options /*LANG*/ "Compose": function () { keyboard.input().then((result) => { - constructReply(options.msg ?? {}, result, resolve); + if (result) + constructReply(options.msg ?? {}, result, resolve, reject); + else + E.showMenu(menu); }); }, }; @@ -40,7 +48,7 @@ exports.reply = function (options) { ) || []; replies.forEach((reply) => { menu = Object.defineProperty(menu, reply.text, { - value: () => constructReply(options.msg ?? {}, reply.text, resolve), + value: () => constructReply(options.msg ?? {}, reply.text, resolve, reject), }); }); if (!keyboard) delete menu[/*LANG*/ "Compose"]; @@ -60,10 +68,11 @@ exports.reply = function (options) { ); } else { keyboard.input().then((result) => { - constructReply(options.msg.id, result, resolve); + constructReply(options.msg, result, resolve, reject); }); } + } else{ + E.showMenu(menu); } - E.showMenu(menu); }); -}; +}; \ No newline at end of file diff --git a/apps/reply/metadata.json b/apps/reply/metadata.json index 34843edd4..c028ed053 100644 --- a/apps/reply/metadata.json +++ b/apps/reply/metadata.json @@ -1,6 +1,6 @@ { "id": "reply", "name": "Reply Library", - "version": "0.01", + "version": "0.02", "description": "A library for replying to text messages via predefined responses or keyboard", "icon": "app.png", "type": "module", diff --git a/apps/setuichange/ChangeLog b/apps/setuichange/ChangeLog index 397e4f509..89a3d1964 100644 --- a/apps/setuichange/ChangeLog +++ b/apps/setuichange/ChangeLog @@ -1,3 +1,5 @@ 0.01: New App! 0.02: Fix case where we tried to push to Bangle.btnWatches but it wasn't defined. +0.03: Throw exception if trying to add custom drag handler on mode updown and + leftright. diff --git a/apps/setuichange/boot.js b/apps/setuichange/boot.js index c9f7aa898..d9df3566f 100644 --- a/apps/setuichange/boot.js +++ b/apps/setuichange/boot.js @@ -39,6 +39,7 @@ Bangle.setUI = (function(mode, cb) { try{Bangle.buzz(30);}catch(e){} } if (mode=="updown") { + if (options.drag) throw new Error("Custom drag handler not supported in mode updown!") var dy = 0; Bangle.dragHandler = e=>{ dy += e.dy; @@ -55,6 +56,7 @@ Bangle.setUI = (function(mode, cb) { setWatch(function() { b();cb(); }, BTN1, {repeat:1, edge:"rising"}), ]; } else if (mode=="leftright") { + if (options.drag) throw new Error("Custom drag handler not supported in mode leftright!") var dx = 0; Bangle.dragHandler = e=>{ dx += e.dx; diff --git a/apps/setuichange/metadata.json b/apps/setuichange/metadata.json index 2d6cafc81..c5aad6929 100644 --- a/apps/setuichange/metadata.json +++ b/apps/setuichange/metadata.json @@ -1,6 +1,6 @@ { "id": "setuichange", "name": "SetUI Proposals preview", - "version":"0.02", + "version":"0.03", "description": "Try out potential future changes to `Bangle.setUI`. Makes hardware button interaction snappier. Makes it possible to set custom event handlers on any type/mode, not just `\"custom\"`. Please provide feedback - see `Read more...` below.", "icon": "app.png", "tags": "", diff --git a/apps/sixths/ChangeLog b/apps/sixths/ChangeLog index 510748fb3..bff499d30 100644 --- a/apps/sixths/ChangeLog +++ b/apps/sixths/ChangeLog @@ -3,3 +3,11 @@ 0.03: minor code improvements 0.04: make height auto-calibration useful and slow ticks to save power 0.05: add ability to navigate to waypoints, better documentation +0.10: lots of updates + acknowledge commands by vibration and message + ui tweaks -- bigger font, compressing uninteresting info + display meters up/down + display pressure trend + adjust GPS on/off algorithm for more reliable fix + display warnings when GPS altitude does not match baro + diff --git a/apps/sixths/README.md b/apps/sixths/README.md index 18aa85beb..9fd7631f9 100644 --- a/apps/sixths/README.md +++ b/apps/sixths/README.md @@ -26,16 +26,17 @@ Useful gestures: B -- "Battery", show/buzz battery info D -- "Down", previous waypoint -F -- "oFf", disable GPS. -G -- "Gps", enable GPS for 4 hours in low power mode. +F -- "turn oFf gps", disable GPS. +T -- "Turn on gps", enable GPS for 4 hours in low power mode. I -- "Info", toggle info display L -- "aLtimeter", load altimeter app M -- "Mark", create mark from current position N -- "Note", take a note and write it to the log. O -- "Orloj", run orloj app - R -- "Run", run "runplus" app + P -- "runPlus", run "runplus" app +R -- "Reset" daily statistics S -- "Speed", enable GPS for 30 minutes in high power mode. - T -- "Time", buzz current time + G -- "Get time", buzz current time U -- "Up", next waypoint Y -- "compass", reset compass @@ -44,6 +45,7 @@ to communicate back to the user. B -- battery low. E -- acknowledge, gesture understood. +I -- unknown gesture. T -- start of new hour. Three colored dots may appear on display. North is on the 12 o'clock @@ -73,27 +75,18 @@ Todo: *) only turn on compass when needed -*) only warn about battery low when it crosses thresholds, update -battery low message - -*) rename "show" to something else -- it collides with built-in - -*) adjust clock according to GPS - -*) show something more reasonable than (NOTEHERE). - -*) hide messages after timeout. - -*) show route lengths after the fact - *) implement longer recording than "G". -*) Probably T should be G. - -*) sum gps distances for a day - *) allow setting up home altitude, or at least disable auto-calibration *) show time-to-sunset / sunrise? -*) one-second updates when gps is active \ No newline at end of file +*) "myprofile" to read step length + +?) display gps alt + offset to baro + +?) start logging baro pressure + +*) compute climb/descent + +*) switch to compensated compass diff --git a/apps/sixths/metadata.json b/apps/sixths/metadata.json index 585f23170..03a9aa1d9 100644 --- a/apps/sixths/metadata.json +++ b/apps/sixths/metadata.json @@ -1,6 +1,6 @@ { "id": "sixths", "name": "Sixth sense", - "version": "0.05", + "version": "0.10", "description": "Clock for outdoor use with GPS support", "icon": "app.png", "readme": "README.md", diff --git a/apps/sixths/sixths.app.js b/apps/sixths/sixths.app.js index c5fb3b9cf..c8d2e668e 100644 --- a/apps/sixths/sixths.app.js +++ b/apps/sixths/sixths.app.js @@ -1,8 +1,222 @@ // Sixth sense /* eslint-disable no-unused-vars */ -// Options you'll want to edit -const rest_altitude = 354; +/* fmt library v0.2.2 */ +let fmt = { + icon_alt : "\0\x08\x1a\1\x00\x00\x00\x20\x30\x78\x7C\xFE\xFF\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_m : "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_km : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_kph : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\xFF\x00\xC3\xC3\xFF\xC3\xC3", + icon_c : "\0\x08\x1a\1\x00\x00\x60\x90\x90\x60\x00\x7F\xFF\xC0\xC0\xC0\xC0\xC0\xFF\x7F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + icon_hpa : "\x00\x08\x16\x01\x00\x80\xb0\xc8\x88\x88\x88\x00\xf0\x88\x84\x84\x88\xf0\x80\x8c\x92\x22\x25\x19\x00\x00", + icon_9 : "\x00\x08\x16\x01\x00\x00\x00\x00\x38\x44\x44\x4c\x34\x04\x04\x38\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + icon_10 : "\x00\x08\x16\x01\x00\x08\x18\x28\x08\x08\x08\x00\x00\x18\x24\x24\x24\x24\x18\x00\x00\x00\x00\x00\x00\x00", + + /* 0 .. DD.ddddd + 1 .. DD MM.mmm' + 2 .. DD MM'ss" + */ + geo_mode : 1, + + init: function() {}, + fmtDist: function(km) { + if (km >= 1.0) return km.toFixed(1) + this.icon_km; + return (km*1000).toFixed(0) + this.icon_m; + }, + fmtSteps: function(n) { return this.fmtDist(0.001 * 0.719 * n); }, + fmtAlt: function(m) { return m.toFixed(0) + this.icon_alt; }, + fmtTemp: function(c) { return c.toFixed(1) + this.icon_c; }, + fmtPress: function(p) { + if (p < 900 || p > 1100) + return p.toFixed(0) + this.icon_hpa; + if (p < 1000) { + p -= 900; + return this.icon_9 + this.add0(p.toFixed(0)) + this.icon_hpa; + } + p -= 1000; + return this.icon_10 + this.add0(p.toFixed(0)) + this.icon_hpa; + }, + draw_dot : 1, + add0: function(i) { + if (i > 9) { + return ""+i; + } else { + return "0"+i; + } + }, + fmtTOD: function(now) { + this.draw_dot = !this.draw_dot; + let dot = ":"; + if (!this.draw_dot) + dot = "."; + return now.getHours() + dot + this.add0(now.getMinutes()); + }, + fmtNow: function() { return this.fmtTOD(new Date()); }, + fmtTimeDiff: function(d) { + if (d < 180) + return ""+d.toFixed(0); + d = d/60; + return ""+d.toFixed(0)+"m"; + }, + fmtAngle: function(x) { + switch (this.geo_mode) { + case 0: + return "" + x; + case 1: { + let d = Math.floor(x); + let m = x - d; + m = m*60; + return "" + d + " " + m.toFixed(3) + "'"; + } + case 2: { + let d = Math.floor(x); + let m = x - d; + m = m*60; + let mf = Math.floor(m); + let s = m - mf; + s = s*60; + return "" + d + " " + mf + "'" + s.toFixed(0) + '"'; + } + } + return "bad mode?"; + }, + fmtPos: function(pos) { + let x = pos.lat; + let c = "N"; + if (x<0) { + c = "S"; + x = -x; + } + let s = c+this.fmtAngle(x) + "\n"; + c = "E"; + if (x<0) { + c = "W"; + x = -x; + } + return s + c + this.fmtAngle(x); + }, + fmtFix: function(fix, t) { + if (fix && fix.fix && fix.lat) { + return this.fmtSpeed(fix.speed) + " " + + this.fmtAlt(fix.alt); + } else { + return "N/FIX " + this.fmtTimeDiff(t); + } + }, + fmtSpeed: function(kph) { + return kph.toFixed(1) + this.icon_kph; + }, + radians: function(a) { return a*Math.PI/180; }, + degrees: function(a) { return a*180/Math.PI; }, + // distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km + // https://www.movable-type.co.uk/scripts/latlong.html + // (Equirectangular approximation) + // returns value in meters + distance: function(a,b) { + var x = this.radians(b.lon-a.lon) * Math.cos(this.radians((a.lat+b.lat)/2)); + var y = this.radians(b.lat-a.lat); + return Math.sqrt(x*x + y*y) * 6371000; + }, + // thanks to waypointer + bearing: function(a,b) { + var delta = this.radians(b.lon-a.lon); + var alat = this.radians(a.lat); + var blat = this.radians(b.lat); + var y = Math.sin(delta) * Math.cos(blat); + var x = Math.cos(alat) * Math.sin(blat) - + Math.sin(alat)*Math.cos(blat)*Math.cos(delta); + return Math.round(this.degrees(Math.atan2(y, x))); + }, +}; + +/* gps library v0.1.2 */ +let gps = { + emulator: -1, + init: function(x) { + this.emulator = (process.env.BOARD=="EMSCRIPTEN" + || process.env.BOARD=="EMSCRIPTEN2")?1:0; + }, + state: {}, + on_gps: function(f) { + let fix = this.getGPSFix(); + f(fix); + + /* + "lat": number, // Latitude in degrees + "lon": number, // Longitude in degrees + "alt": number, // altitude in M + "speed": number, // Speed in kph + "course": number, // Course in degrees + "time": Date, // Current Time (or undefined if not known) + "satellites": 7, // Number of satellites + "fix": 1 // NMEA Fix state - 0 is no fix + "hdop": number, // Horizontal Dilution of Precision + */ + this.state.timeout = setTimeout(this.on_gps, 1000, f); + }, + off_gps: function() { + clearTimeout(this.state.timeout); + }, + getGPSFix: function() { + if (!this.emulator) + return Bangle.getGPSFix(); + let fix = {}; + fix.fix = 1; + fix.lat = 50; + fix.lon = 14-(getTime()-this.gps_start) / 1000; /* Go West! */ + fix.alt = 200; + fix.speed = 5; + fix.course = 30; + fix.time = Date(); + fix.satellites = 5; + fix.hdop = 12; + return fix; + }, + gps_start : -1, + start_gps: function() { + Bangle.setGPSPower(1, "libgps"); + this.gps_start = getTime(); + }, + stop_gps: function() { + Bangle.setGPSPower(0, "libgps"); + }, +}; + +/* sun version 0.0.1 */ +let sun = { + SunCalc: null, + lat: 50, + lon: 14, + init: function() { + try { + this.SunCalc = require("suncalc"); // from modules folder + } catch (e) { + print("Require error", e); + } + + print("Have suncalc: ", this.SunCalc); + }, + get_sun_pos: function() { + let d = new Date(); + let sun = this.SunCalc.getPosition(d, this.lat, this.lon); + print(sun.azimuth, sun.altitude); + return sun; + }, + get_sun_time: function() { + let d = new Date(); + let sun = this.SunCalc.getTimes(d, this.lat, this.lon); + print(sun.sunrise, sun.sunset); + return sun; + }, +}; + +sun.init(); +sun.get_sun_pos(); +sun.get_sun_time(); +fmt.init(); +gps.init(); + +var location; const W = g.getWidth(); const H = g.getHeight(); @@ -12,19 +226,28 @@ var buzz = "", /* Set this to transmit morse via vibrations */ inm = "", l = "", /* For incoming morse handling */ in_str = "", note = "", - debug = "v0.04.1", debug2 = "(otherdb)", debug3 = "(short)"; + debug = "v0.5.11", debug2 = "(otherdb)", debug3 = "(short)"; +var note_limit = 0; var mode = 0, mode_time = 0; // 0 .. normal, 1 .. note, 2.. mark name var disp_mode = 0; // 0 .. normal, 1 .. small time +var state = { + gps_limit: 0, // timeout -- when to stop recording + gps_speed_limit: 0, + prev_fix: null, + gps_dist: 0, + + // Marks + cur_mark: null, +}; + // GPS handling var gps_on = 0, // time GPS was turned on last_fix = 0, // time of last fix - last_restart = 0, last_pause = 0, last_fstart = 0; // utime + last_restart = 0, last_pause = 0, // utime + last_fstart = 0; // utime, time of start of last fix var gps_needed = 0, // how long to wait for a fix - gps_limit = 0, // timeout -- when to stop recording - gps_speed_limit = 0; -var prev_fix = null; -var gps_dist = 0; + keep_fix_for = 30; var mark_heading = -1; @@ -34,19 +257,9 @@ var draw_dot = false; var is_level = false; // For altitude handling. -var cur_altitude = 0; +var cur_altitude = -1; var cur_temperature = 0; - -// Marks -var cur_mark = null; - -// Icons - -var icon_alt = "\0\x08\x1a\1\x00\x00\x00\x20\x30\x78\x7C\xFE\xFF\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00"; -//var icon_m = "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00"; -var icon_km = "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00"; -var icon_kph = "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\xFF\x00\xC3\xC3\xFF\xC3\xC3"; -var icon_c = "\0\x08\x1a\1\x00\x00\x60\x90\x90\x60\x00\x7F\xFF\xC0\xC0\xC0\xC0\xC0\xFF\x7F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; +var night_pressure = 0; function toMorse(x) { let r = ""; @@ -84,34 +297,28 @@ function gpsPause() { last_restart = 0; last_pause = getTime(); } +function gpsReset() { + state.prev_fix = null; + state.gps_dist = 0; +} function gpsOn() { gps_on = getTime(); gps_needed = 1000; last_fix = 0; - prev_fix = null; - gps_dist = 0; gpsRestart(); } function gpsOff() { Bangle.setGPSPower(0, "sixths"); gps_on = 0; } -function fmtDist(km) { return km.toFixed(1) + icon_km; } -function fmtSteps(n) { return fmtDist(0.001 * 0.719 * n); } -function fmtAlt(m) { return m.toFixed(0) + icon_alt; } -function fmtTimeDiff(d) { - if (d < 180) - return ""+d.toFixed(0); - d = d/60; - return ""+d.toFixed(0)+"m"; -} function gpsHandleFix(fix) { - if (!prev_fix) { - show("GPS acquired", 10); + if (!state.prev_fix) { + showMsg("GPS acquired", 10); doBuzz(" ."); - prev_fix = fix; + state.prev_fix = fix; } - if (1) { + if (0) { + /* Display error between GPS and system time */ let now1 = Date(); let now2 = fix.time; let n1 = now1.getMinutes() * 60 + now1.getSeconds(); @@ -119,42 +326,55 @@ function gpsHandleFix(fix) { debug2 = "te "+(n2-n1)+"s"; } loggps(fix); - let d = calcDistance(fix, prev_fix); + let d = fmt.distance(fix, state.prev_fix); if (d > 30) { - prev_fix = fix; - gps_dist += d/1000; + state.prev_fix = fix; + state.gps_dist += d/1000; } } function gpsHandle() { let msg = ""; + debug2 = "Ne" + gps_needed; + debug3 = "Ke" + keep_fix_for; if (!last_restart) { let d = (getTime()-last_pause); if (last_fix) - msg = "PL"+ fmtTimeDiff(getTime()-last_fix); + msg = "PL"+ fmt.fmtTimeDiff(getTime()-last_fix); else - msg = "PN"+ fmtTimeDiff(getTime()-gps_on); + msg = "PN"+ fmt.fmtTimeDiff(getTime()-gps_on); print("gps on, paused ", d, gps_needed); if (d > gps_needed * 2) { gpsRestart(); } } else { - let fix = Bangle.getGPSFix(); + let fix = gps.getGPSFix(); if (fix && fix.fix && fix.lat) { gpsHandleFix(fix); - msg = fix.speed.toFixed(1) + icon_kph; - print("GPS FIX", msg); + msg = ""; + if (Math.abs(fix.alt - cur_altitude) > 20) + msg += "!"; + if (Math.abs(fix.alt - cur_altitude) > 80) + msg += "!"; + if (Math.abs(fix.alt - cur_altitude) > 320) + msg += "!"; + msg += fmt.fmtSpeed(fix.speed); if (!last_fstart) last_fstart = getTime(); last_fix = getTime(); + keep_fix_for = (last_fstart - last_restart) / 1.5; + if (keep_fix_for < 20) + keep_fix_for = 20; + if (keep_fix_for > 6*60) + keep_fix_for = 6*60; gps_needed = 60; } else { if (last_fix) - msg = "L"+ fmtTimeDiff(getTime()-last_fix); + msg = "L"+ fmt.fmtTimeDiff(getTime()-last_fix); else { - msg = "N"+ fmtTimeDiff(getTime()-gps_on); - if (fix) { + msg = "N"+ fmt.fmtTimeDiff(getTime()-gps_on); + if (0 && fix) { msg += " " + fix.satellites + "sats"; } } @@ -162,50 +382,50 @@ function gpsHandle() { let d = (getTime()-last_restart); let d2 = (getTime()-last_fstart); - print("gps on, restarted ", d, gps_needed, d2, fix.lat); - if (getTime() > gps_speed_limit && - (d > gps_needed || (last_fstart && d2 > 10))) { + print("gps on, restarted ", d, gps_needed, d2); + if (getTime() > state.gps_speed_limit && + ((d > gps_needed && !last_fstart) || (last_fstart && d2 > keep_fix_for))) { gpsPause(); gps_needed = gps_needed * 1.5; print("Pausing, next try", gps_needed); } } - msg += " "+gps_dist.toFixed(1)+icon_km; + msg += " "+fmt.fmtDist(state.gps_dist); return msg; } function markNew() { let r = {}; r.time = getTime(); - r.fix = prev_fix; + r.fix = state.prev_fix; r.steps = Bangle.getHealthStatus("day").steps; - r.gps_dist = gps_dist; + r.gps_dist = state.gps_dist; r.altitude = cur_altitude; r.name = "auto"; return r; } function markHandle() { - let m = cur_mark; + let m = state.cur_mark; let msg = m.name + ">"; if (m.time) { - msg += fmtTimeDiff(getTime()- m.time); + msg += fmt.fmtTimeDiff(getTime()- m.time); } - if (prev_fix && prev_fix.fix && m.fix && m.fix.fix) { - let s = fmtDist(calcDistance(m.fix, prev_fix)/1000) + icon_km; + if (state.prev_fix && state.prev_fix.fix && m.fix && m.fix.fix) { + let s = fmt.fmtDist(fmt.distance(m.fix, state.prev_fix)/1000) + fmt.icon_km; msg += " " + s; debug = "wp>" + s; - mark_heading = 180 + calcBearing(m.fix, prev_fix); + mark_heading = 180 + fmt.bearing(m.fix, state.prev_fix); debug2 = "wp>" + mark_heading; } else { - msg += " w" + fmtDist(gps_dist - m.gps_dist); + msg += " w" + fmt.fmtDist(state.gps_dist - m.gps_dist); } return msg; } function entryDone() { - show(":" + in_str); + showMsg(":" + in_str); doBuzz(" ."); switch (mode) { case 1: logstamp(">" + in_str); break; - case 2: cur_mark.name = in_str; break; + case 2: state.cur_mark.name = in_str; break; } in_str = 0; mode = 0; @@ -225,18 +445,22 @@ function selectWP(i) { if (sel_wp >= waypoints.length) sel_wp = waypoints.length - 1; if (sel_wp < 0) { - show("No WPs", 60); + showMsg("No WPs", 60); } let wp = waypoints[sel_wp]; - cur_mark = {}; - cur_mark.name = wp.name; - cur_mark.gps_dist = 0; /* HACK */ - cur_mark.fix = {}; - cur_mark.fix.fix = 1; - cur_mark.fix.lat = wp.lat; - cur_mark.fix.lon = wp.lon; - show("WP:"+wp.name, 60); - print("Select waypoint: ", cur_mark); + state.cur_mark = {}; + state.cur_mark.name = wp.name; + state.cur_mark.gps_dist = 0; /* HACK */ + state.cur_mark.fix = {}; + state.cur_mark.fix.fix = 1; + state.cur_mark.fix.lat = wp.lat; + state.cur_mark.fix.lon = wp.lon; + showMsg("WP:"+wp.name, 60); + print("Select waypoint: ", state.cur_mark); +} +function ack(cmd) { + showMsg(cmd, 3); + doBuzz(' .'); } function inputHandler(s) { print("Ascii: ", s, s[0], s[1]); @@ -249,7 +473,7 @@ function inputHandler(s) { } if ((mode == 1) || (mode == 2)){ in_str = in_str + s; - show(">"+in_str, 10); + showMsg(">"+in_str, 10); mode_time = getTime(); return; } @@ -262,34 +486,37 @@ function inputHandler(s) { else s = s+(bat/5); doBuzz(toMorse(s)); - show("Bat "+bat+"%", 60); + showMsg("Bat "+bat+"%", 60); break; } - case 'D': selectWP(1); break; - case 'F': gpsOff(); show("GPS off", 3); break; - case 'G': gpsOn(); gps_limit = getTime() + 60*60*4; show("GPS on", 3); break; + case 'D': doBuzz(' .'); selectWP(1); break; + case 'F': gpsOff(); ack("GPS off"); break; + case 'T': gpsOn(); state.gps_limit = getTime() + 60*60*4; ack("GPS on"); break; case 'I': + doBuzz(' .'); disp_mode += 1; if (disp_mode == 2) { disp_mode = 0; } break; case 'L': aload("altimeter.app.js"); break; - case 'M': mode = 2; show("M>", 10); cur_mark = markNew(); mode_time = getTime(); break; - case 'N': mode = 1; show(">", 10); mode_time = getTime(); break; + case 'M': doBuzz(' .'); mode = 2; showMsg("M>", 10); state.cur_mark = markNew(); mode_time = getTime(); break; + case 'N': doBuzz(' .'); mode = 1; showMsg(">", 10); mode_time = getTime(); break; case 'O': aload("orloj.app.js"); break; - case 'R': aload("runplus.app.js"); break; - case 'S': gpsOn(); gps_limit = getTime() + 60*30; gps_speed_limit = gps_limit; show("GPS on", 3); break; - case 'T': { + case 'R': gpsReset(); ack("GPS reset"); break; + case 'P': aload("runplus.app.js"); break; + case 'S': gpsOn(); state.gps_limit = getTime() + 60*30; state.gps_speed_limit = state.gps_limit; ack("GPS speed"); break; + case 'G': { s = ' T'; let d = new Date(); s += d.getHours() % 10; - s += add0(d.getMinutes()); + s += fmt.add0(d.getMinutes()); doBuzz(toMorse(s)); break; } - case 'U': selectWP(-1); break; - case 'Y': doBuzz(buzz); Bangle.resetCompass(); break; + case 'U': doBuzz(' .'); selectWP(-1); break; + case 'Y': ack('Compass reset'); Bangle.resetCompass(); break; + default: doBuzz(' ..'); showMsg("Unknown "+s, 5); break; } } const morseDict = { @@ -389,19 +616,15 @@ function touchHandler(d) { //print(inm, "drag:", d); } -function add0(i) { - if (i > 9) { - return ""+i; - } else { - return "0"+i; - } -} var lastHour = -1, lastMin = -1; function logstamp(s) { - logfile.write("utime=" + getTime() + " " + s + "\n"); + logfile.write("utime=" + getTime() + + " bele=" + cur_altitude + + " batperc=" + E.getBattery() + + " " + s + "\n"); } function loggps(fix) { - logfile.write(fix.lat + " " + fix.lon + " "); + logfile.write(fix.lat + " " + fix.lon + " ele=" + fix.alt + " "); logstamp(""); } function hourly() { @@ -410,15 +633,19 @@ function hourly() { let bat = E.getBattery(); if (bat < 25) { s = ' B'; - show("Bat "+bat+"%", 60); + showMsg("Bat "+bat+"%", 60); } if (is_active) doBuzz(toMorse(s)); //logstamp(""); } -function show(msg, timeout) { +function showMsg(msg, timeout) { + note_limit = getTime() + timeout; note = msg; } + +var prev_step = -1, this_step = -1; + function fivemin() { print("fivemin"); let s = ' B'; @@ -429,7 +656,8 @@ function fivemin() { } catch (e) { print("Altimeter error", e); } - + prev_step = this_step; + this_step = Bangle.getStepCount(); } function every(now) { if ((mode > 0) && (getTime() - mode_time > 10)) { @@ -438,7 +666,7 @@ function every(now) { } mode = 0; } - if (gps_on && getTime() > gps_limit && getTime() > gps_speed_limit) { + if (gps_on && getTime() > state.gps_limit && getTime() > state.gps_speed_limit) { Bangle.setGPSPower(0, "sixths"); gps_on = 0; } @@ -447,38 +675,17 @@ function every(now) { lastHour = now.getHours(); hourly(); } - if (lastMin / 5 != now.getMinutes() / 5) { + if (lastMin / 5 != now.getMinutes() / 5) { // fixme, trunc? lastMin = now.getMinutes(); fivemin(); } - } -function radians(a) { return a*Math.PI/180; } -function degrees(a) { return a*180/Math.PI; } -// distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km -// https://www.movable-type.co.uk/scripts/latlong.html -// (Equirectangular approximation) -function calcDistance(a,b) { - var x = radians(b.lon-a.lon) * Math.cos(radians((a.lat+b.lat)/2)); - var y = radians(b.lat-a.lat); - return Math.sqrt(x*x + y*y) * 6371000; -} -// thanks to waypointer -function calcBearing(a,b){ - var delta = radians(b.lon-a.lon); - var alat = radians(a.lat); - var blat = radians(b.lat); - var y = Math.sin(delta) * Math.cos(blat); - var x = Math.cos(alat)*Math.sin(blat) - - Math.sin(alat)*Math.cos(blat)*Math.cos(delta); - return Math.round(degrees(Math.atan2(y, x))); -} function testBearing() { let p1 = {}, p2 = {}; p1.lat = 40; p2.lat = 50; p1.lon = 14; p2.lon = 14; - print("bearing = ", calcBearing(p1, p2)); + print("bearing = ", fmt.bearing(p1, p2)); } function radA(p) { return p*(Math.PI*2); } @@ -509,9 +716,9 @@ function drawBackground() { drawDot(h, 0.7, 10); } } - if (prev_fix && prev_fix.fix) { + if (state.prev_fix && state.prev_fix.fix) { g.setColor(0.5, 1, 0.5); - drawDot(prev_fix.course, 0.5, 6); + drawDot(state.prev_fix.course, 0.5, 6); } if (mark_heading != -1) { g.setColor(1, 0.5, 0.5); @@ -523,13 +730,66 @@ function drawTime(now) { g.setFont('Vector', 60); else g.setFont('Vector', 26); - g.setFontAlign(1, 1); + g.setFontAlign(1, -1); draw_dot = !draw_dot; let dot = ":"; if (!draw_dot) dot = "."; - g.drawString(now.getHours() + dot + add0(now.getMinutes()), W, 90); + let s = ""; + if (disp_mode == 1) + s = debug; + g.drawString(s + now.getHours() + dot + fmt.add0(now.getMinutes()), W, 28); } + +var base_alt = -1, ext_alt = -1, tot_down = 0, tot_up = 0; + +function walkHandle() { + let msg = ""; + let step = Bangle.getStepCount(); + let cur = cur_altitude; + if (base_alt == -1) { + base_alt = cur; + ext_alt = cur; + } + if (this_step - prev_step > 100 + || 1 + || step - this_step > 100) { + //msg += fmt.fmtSteps((this_step - prev_step) * 12); + + let dir = ext_alt > base_alt; /* 1.. climb */ + if (!dir) dir = -1; + let hyst = 2; + if (Math.abs(cur - base_alt) > hyst) { + if (cur*dir > ext_alt*dir) { + ext_alt = cur; + } + } + let diff = ext_alt - base_alt; + if (cur*dir < (ext_alt - hyst*dir)*dir) { + if (1 == dir) { + tot_up += diff; + } + if (-1 == dir) { + tot_down += -diff; + } + base_alt = ext_alt; + ext_alt = cur; + } + let tmp_down = tot_down, tmp_up = tot_up; + if (1 == dir) { + tmp_up += diff; + } + if (-1 == dir) { + tmp_down += -diff; + } + + msg += " " + fmt.fmtAlt(tmp_down) + " " + fmt.fmtAlt(tmp_up); + + return msg + "\n"; + } + return ""; +} + function draw() { if (disp_mode == 2) { draw_all(); @@ -542,16 +802,6 @@ function draw() { g.setColor(0.25, 1, 1); g.fillPoly([ W/2, 24, W, 80, 0, 80 ]); } - let msg = ""; - if (gps_on) { - msg = gpsHandle(); - } else { - let o = Bangle.getOptions(); - msg = o.seaLevelPressure.toFixed(1) + "hPa"; - if (note != "") { - msg = note; - } - } drawBackground(); let now = new Date(); @@ -562,43 +812,75 @@ function draw() { //let km = 0.001 * 0.719 * Bangle.getHealthStatus("day").steps; - g.setFontAlign(-1, 1); - g.setFont('Vector', 26); + // 33 still fits + g.setFont('Vector', 30); const weekday = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]; - g.drawString(weekday[now.getDay()] + "" + now.getDate() + ". " - + fmtSteps(Bangle.getHealthStatus("day").steps), 10, 115); + let msg = weekday[now.getDay()] + "" + now.getDate() + ". " + + fmt.fmtSteps(Bangle.getHealthStatus("day").steps) + "\n"; - g.drawString(msg, 10, 145); + if (gps_on) { + msg += gpsHandle() + "\n"; + } + + if (state.cur_mark) { + msg += markHandle() + "\n"; + } + + if (note != "") { + if (getTime() > note_limit) + note = ""; + else + msg += note + "\n"; + } + + msg += walkHandle(); if (getTime() - last_active > 15*60) { - let alt_adjust = cur_altitude - rest_altitude; + let alt_adjust = cur_altitude - location.alt; let abs = Math.abs(alt_adjust); print("adj", alt_adjust); let o = Bangle.getOptions(); if (abs > 10 && abs < 150) { let a = 0.01; // FIXME: draw is called often compared to alt reading - if (cur_altitude > rest_altitude) + if (cur_altitude > location.alt) a = -a; o.seaLevelPressure = o.seaLevelPressure + a; Bangle.setOptions(o); } - msg = o.seaLevelPressure.toFixed(1) + "hPa"; + let pr = o.seaLevelPressure; + if (pr) + msg += fmt.fmtPress(pr); + else + msg += "emu?"; } else { - msg = fmtAlt(cur_altitude); + msg += fmt.fmtAlt(cur_altitude); } - msg = msg + " " + cur_temperature.toFixed(1)+icon_c; - if (cur_mark) { - msg = markHandle(); - } - g.drawString(msg, 10, 175); - if (disp_mode == 1) { - g.drawString(debug, 10, 45); - g.drawString(debug2, 10, 65); - g.drawString(debug3, 10, 85); + msg = msg + " " + fmt.fmtTemp(cur_temperature) + "\n"; + + { + let o = Bangle.getOptions(); + let pr = o.seaLevelPressure; + + if (now.getHours() < 6) + night_pressure = pr; + if (night_pressure) + msg += (pr-night_pressure).toFixed(1) + fmt.icon_hpa + " "; + if (pr) + msg += fmt.fmtPress(pr) + "\n"; + } + g.setFontAlign(-1, -1); + if (disp_mode == 0) + g.drawString(msg, 10, 85); + else + g.drawString(msg, 10, 60); + + if (0 && disp_mode == 1) { + g.setFont('Vector', 21); + g.drawString(debug + "\n" + debug2 + "\n" + debug3, 10, 20); } queueDraw(); @@ -611,7 +893,7 @@ function draw_all() { g.setColor(1, 1, 1); g.setFontAlign(-1, 1); let now = new Date(); - g.drawString(now.getHours() + ":" + add0(now.getMinutes()) + ":" + add0(now.getSeconds()), 10, 40); + g.drawString(now.getHours() + ":" + fmt.add0(now.getMinutes()) + ":" + fmt.add0(now.getSeconds()), 10, 40); let acc = Bangle.getAccel(); let ax = 0 + acc.x, ay = 0.75 + acc.y, az = 0.75 + acc.y; @@ -727,7 +1009,8 @@ function lockHandler(locked) { function queueDraw() { let next; - if (getTime() - last_unlocked > 3*60) + if ((getTime() - last_unlocked > 3*60) && + (getTime() > state.gps_limit)) next = 60000; else next = 1000; @@ -754,6 +1037,8 @@ function start() { } draw(); + location = require("Storage").readJSON("mylocation.json",1)||{"lat":50,"lon":14.45,"alt":354,"location":"Woods"}; + state = require("Storage").readJSON("sixths.json",1)||state; loadWPs(); buzzTask(); if (0) diff --git a/apps/trail/ChangeLog b/apps/trail/ChangeLog new file mode 100644 index 000000000..4c4db83bc --- /dev/null +++ b/apps/trail/ChangeLog @@ -0,0 +1,2 @@ +0.01: New App! + diff --git a/apps/trail/README.md b/apps/trail/README.md new file mode 100644 index 000000000..24e06a3ba --- /dev/null +++ b/apps/trail/README.md @@ -0,0 +1,18 @@ +# Trail Rail ![](app.png) + +Simple app to follow GPX track + +Written by: [Pavel Machek](https://github.com/pavelmachek) + +After GPS fix is acquired, it displays familiar arrow with road in +front of you. It never stores whole track in memory, so it should work +with fairly large files. + +GPX files can be obtained from various services, www.mapy.cz is one of +them (actually uses openstreetmap data for most of the world). + +## Preparing data + +"gpx2egt.sh < file.gpx > t.name.egt" can be used to prepare data, then +upload it to watch. + diff --git a/apps/trail/app-icon.js b/apps/trail/app-icon.js new file mode 100644 index 000000000..511c43245 --- /dev/null +++ b/apps/trail/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEwhHXAH4AHgAqpvownEQ4wEMEYw8YMgw4F84wwMH4woeQQvlEwwvCGFgvqE4gvDGFAvHAFQv/AH74YElYvjF3Je/FzV9F8wuLF8QhHL34u/RqSOjFxYvpF2gvlRQwuoAAIvqFoQvJFsoupF9wtFF+CNuX34xfF1YwDF9oA/AH4AyA==")) diff --git a/apps/trail/app.png b/apps/trail/app.png new file mode 100644 index 000000000..3213f929a Binary files /dev/null and b/apps/trail/app.png differ diff --git a/apps/trail/gpx2egt.sh b/apps/trail/gpx2egt.sh new file mode 100755 index 000000000..a4c5b5d0a --- /dev/null +++ b/apps/trail/gpx2egt.sh @@ -0,0 +1,2 @@ +#!/bin/bash +grep "trkpt lat" | sed 's/.*trkpt.lat=.//' | sed 's/. lon=./ /' | sed 's/".$//' diff --git a/apps/trail/metadata.json b/apps/trail/metadata.json new file mode 100644 index 000000000..093d04567 --- /dev/null +++ b/apps/trail/metadata.json @@ -0,0 +1,13 @@ +{ "id": "trail", + "name": "Trail Rail", + "version":"0.01", + "description": "Follow a GPX track in car or on bike", + "icon": "app.png", + "readme": "README.md", + "supports" : ["BANGLEJS2"], + "tags": "outdoors,gps,osm", + "storage": [ + {"name":"trail.app.js","url":"trail.app.js"}, + {"name":"trail.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/apps/trail/trail.app.js b/apps/trail/trail.app.js new file mode 100644 index 000000000..17f2ffc88 --- /dev/null +++ b/apps/trail/trail.app.js @@ -0,0 +1,651 @@ +// "Rail trail"? "Trail rail"! + +/* fmt library v0.2.2 */ +let fmt = { + icon_alt : "\0\x08\x1a\1\x00\x00\x00\x20\x30\x78\x7C\xFE\xFF\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_m : "\0\x08\x1a\1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_km : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\x00\x00\x00\x00\x00\x00\x00", + icon_kph : "\0\x08\x1a\1\xC3\xC6\xCC\xD8\xF0\xD8\xCC\xC6\xC3\x00\xC3\xE7\xFF\xDB\xC3\xC3\xC3\xC3\x00\xFF\x00\xC3\xC3\xFF\xC3\xC3", + icon_c : "\0\x08\x1a\1\x00\x00\x60\x90\x90\x60\x00\x7F\xFF\xC0\xC0\xC0\xC0\xC0\xFF\x7F\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + icon_hpa : "\x00\x08\x16\x01\x00\x80\xb0\xc8\x88\x88\x88\x00\xf0\x88\x84\x84\x88\xf0\x80\x8c\x92\x22\x25\x19\x00\x00", + icon_9 : "\x00\x08\x16\x01\x00\x00\x00\x00\x38\x44\x44\x4c\x34\x04\x04\x38\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", + icon_10 : "\x00\x08\x16\x01\x00\x08\x18\x28\x08\x08\x08\x00\x00\x18\x24\x24\x24\x24\x18\x00\x00\x00\x00\x00\x00\x00", + + /* 0 .. DD.ddddd + 1 .. DD MM.mmm' + 2 .. DD MM'ss" + */ + geo_mode : 1, + + init: function() {}, + fmtDist: function(km) { + if (km >= 1.0) return km.toFixed(1) + this.icon_km; + return (km*1000).toFixed(0) + this.icon_m; + }, + fmtSteps: function(n) { return this.fmtDist(0.001 * 0.719 * n); }, + fmtAlt: function(m) { return m.toFixed(0) + this.icon_alt; }, + fmtTemp: function(c) { return c.toFixed(1) + this.icon_c; }, + fmtPress: function(p) { + if (p < 900 || p > 1100) + return p.toFixed(0) + this.icon_hpa; + if (p < 1000) { + p -= 900; + return this.icon_9 + this.add0(p.toFixed(0)) + this.icon_hpa; + } + p -= 1000; + return this.icon_10 + this.add0(p.toFixed(0)) + this.icon_hpa; + }, + draw_dot : 1, + add0: function(i) { + if (i > 9) { + return ""+i; + } else { + return "0"+i; + } + }, + fmtTOD: function(now) { + this.draw_dot = !this.draw_dot; + let dot = ":"; + if (!this.draw_dot) + dot = "."; + return now.getHours() + dot + this.add0(now.getMinutes()); + }, + fmtNow: function() { return this.fmtTOD(new Date()); }, + fmtTimeDiff: function(d) { + if (d < 180) + return ""+d.toFixed(0); + d = d/60; + return ""+d.toFixed(0)+"m"; + }, + fmtAngle: function(x) { + switch (this.geo_mode) { + case 0: + return "" + x; + case 1: { + let d = Math.floor(x); + let m = x - d; + m = m*60; + return "" + d + " " + m.toFixed(3) + "'"; + } + case 2: { + let d = Math.floor(x); + let m = x - d; + m = m*60; + let mf = Math.floor(m); + let s = m - mf; + s = s*60; + return "" + d + " " + mf + "'" + s.toFixed(0) + '"'; + } + } + return "bad mode?"; + }, + fmtPos: function(pos) { + let x = pos.lat; + let c = "N"; + if (x<0) { + c = "S"; + x = -x; + } + let s = c+this.fmtAngle(x) + "\n"; + c = "E"; + if (x<0) { + c = "W"; + x = -x; + } + return s + c + this.fmtAngle(x); + }, + fmtFix: function(fix, t) { + if (fix && fix.fix && fix.lat) { + return this.fmtSpeed(fix.speed) + " " + + this.fmtAlt(fix.alt); + } else { + return "N/FIX " + this.fmtTimeDiff(t); + } + }, + fmtSpeed: function(kph) { + return kph.toFixed(1) + this.icon_kph; + }, + radians: function(a) { return a*Math.PI/180; }, + degrees: function(a) { return a*180/Math.PI; }, + // distance between 2 lat and lons, in meters, Mean Earth Radius = 6371km + // https://www.movable-type.co.uk/scripts/latlong.html + // (Equirectangular approximation) + // returns value in meters + distance: function(a,b) { + var x = this.radians(b.lon-a.lon) * Math.cos(this.radians((a.lat+b.lat)/2)); + var y = this.radians(b.lat-a.lat); + return Math.sqrt(x*x + y*y) * 6371000; + }, + // thanks to waypointer + bearing: function(a,b) { + var delta = this.radians(b.lon-a.lon); + var alat = this.radians(a.lat); + var blat = this.radians(b.lat); + var y = Math.sin(delta) * Math.cos(blat); + var x = Math.cos(alat) * Math.sin(blat) - + Math.sin(alat)*Math.cos(blat)*Math.cos(delta); + return Math.round(this.degrees(Math.atan2(y, x))); + }, +}; + +/* gps library v0.1.2 */ +let gps = { + emulator: -1, + init: function(x) { + this.emulator = (process.env.BOARD=="EMSCRIPTEN" + || process.env.BOARD=="EMSCRIPTEN2")?1:0; + }, + state: {}, + on_gps: function(f) { + let fix = this.getGPSFix(); + f(fix); + + /* + "lat": number, // Latitude in degrees + "lon": number, // Longitude in degrees + "alt": number, // altitude in M + "speed": number, // Speed in kph + "course": number, // Course in degrees + "time": Date, // Current Time (or undefined if not known) + "satellites": 7, // Number of satellites + "fix": 1 // NMEA Fix state - 0 is no fix + "hdop": number, // Horizontal Dilution of Precision + */ + this.state.timeout = setTimeout(this.on_gps, 1000, f); + }, + off_gps: function() { + clearTimeout(this.state.timeout); + }, + getGPSFix: function() { + if (!this.emulator) + return Bangle.getGPSFix(); + let fix = {}; + fix.fix = 1; + fix.lat = 50; + fix.lon = 14-(getTime()-this.gps_start) / 1000; /* Go West! */ + fix.alt = 200; + fix.speed = 5; + fix.course = 30; + fix.time = Date(); + fix.satellites = 5; + fix.hdop = 12; + return fix; + }, + gps_start : -1, + start_gps: function() { + Bangle.setGPSPower(1, "libgps"); + this.gps_start = getTime(); + }, + stop_gps: function() { + Bangle.setGPSPower(0, "libgps"); + }, +}; + +/* ui library 0.1.2 */ +let ui = { + display: 0, + numScreens: 2, + drawMsg: function(msg) { + g.reset().setFont("Vector", 35) + .setColor(1,1,1) + .fillRect(0, this.wi, 176, 176) + .setColor(0,0,0) + .drawString(msg, 5, 30) + .flip(); + }, + drawBusy: function() { + this.drawMsg("\n.oO busy"); + }, + nextScreen: function() { + print("nextS"); + this.display = this.display + 1; + if (this.display == this.numScreens) + this.display = 0; + this.drawBusy(); + }, + prevScreen: function() { + print("prevS"); + this.display = this.display - 1; + if (this.display < 0) + this.display = this.numScreens - 1; + this.drawBusy(); + }, + onSwipe: function(dir) { + this.nextScreen(); + }, + h: 176, + w: 176, + wi: 32, + last_b: 0, + touchHandler: function(d) { + let x = Math.floor(d.x); + let y = Math.floor(d.y); + + if (d.b != 1 || this.last_b != 0) { + this.last_b = d.b; + return; + } + + print("touch", x, y, this.h, this.w); + + /* + if ((xthis.h/2) && (ythis.w/2)) { + print("prev"); + this.prevScreen(); + } + if ((x>this.h/2) && (y>this.w/2)) { + print("next"); + this.nextScreen(); + } + }, + init: function() { + this.drawBusy(); + } +}; + +/* egt 0.0.1 */ +let egt = { + init: function() { + }, + parse: function(l) { + let r = {}; + let s = l.split(' '); + + if (s === undefined) + return r; + + if (s[1] === undefined) + return r; + + if (s[1].split('=')[1] === undefined) { + r.lat = 1 * s[0]; + r.lon = 1 * s[1]; + } + + for (let fi of s) { + let f = fi.split('='); + if (f[0] == "utime") { + r.utime = 1 * f[1]; + } + } + + return r; + }, +}; + +function toCartesian(v) { + const R = 6371; // Poloměr Země v km + const latRad = v.lat * Math.PI / 180; + const lonRad = v.lon * Math.PI / 180; + + const x = R * lonRad * Math.cos(latRad); + const y = R * latRad; + + return { x, y }; +} + +function distSegment(x1, x2, xP) { + // Převod zeměpisných souřadnic na kartézské souřadnice + const p1 = toCartesian(x1); + const p2 = toCartesian(x2); + const p = toCartesian(xP); + + // Vektor p1p2 + const dx = p2.x - p1.x; + const dy = p2.y - p1.y; + + // Projekce bodu p na přímku definovanou body p1 a p2 + const dot = ((p.x - p1.x) * dx + (p.y - p1.y) * dy) / (dx * dx + dy * dy); + + // Určení bodu na přímce, kde leží projekce + let closestX, closestY; + if (dot < 0) { + closestX = p1.x; + closestY = p1.y; + } else if (dot > 1) { + closestX = p2.x; + closestY = p2.y; + } else { + closestX = p1.x + dot * dx; + closestY = p1.y + dot * dy; + } + + // Vzdálenost mezi bodem p a nejbližším bodem na úsečce + const distance = Math.sqrt((p.x - closestX) * (p.x - closestX) + (p.y - closestY) * (p.y - closestY)); + + return distance * 1000; +} + +function angleDifference(angle1, angle2) { + // Compute the difference + let difference = angle2 - angle1; + + // Normalize the difference to be within the range -180° to 180° + while (difference > 180) difference -= 360; + while (difference < -180) difference += 360; + + return difference; +} + +function drawThickLine(x1, y1, x2, y2, thickness) { + // Calculate the perpendicular offset for the line thickness + const dx = x2 - x1; + const dy = y2 - y1; + const length = Math.sqrt(dx * dx + dy * dy); + const offsetX = (dy / length) * (thickness / 2); + const offsetY = (dx / length) * (thickness / 2); + + // Draw multiple lines to simulate thickness + for (let i = -thickness / 2; i <= thickness / 2; i++) { + g.drawLine( + x1 + offsetX * i, + y1 - offsetY * i, + x2 + offsetX * i, + y2 - offsetY * i + ); + } +} + +function toxy(pp, p) { + let r = {}; + let d = fmt.distance(pp, p); + let h = fmt.radians(fmt.bearing(pp, p) - pp.course); + let x = pp.x, y = pp.y; + x += d * pp.ppm * Math.sin(h); + y -= d * pp.ppm * Math.cos(h); + r.x = x; + r.y = y; + return r; +} + +function paint(pp, p1, p2, thick) { + let d1 = toxy(pp, p1); + let d2 = toxy(pp, p2); + drawThickLine(d1.x, d1.y, d2.x, d2.y, thick); +} + +var destination = {}, num = 0, dist = 0; + +function read(pp, n) { + g.reset().clear(); + let f = require("Storage").open(n+".st", "r"); + let l = f.readLine(); + let prev = 0; + while (l!==undefined) { + num++; + l = ""+l; + //print(l); + let p = egt.parse(l); + if (p.lat) { + if (prev) { + dist += fmt.distance(prev, p); + //paint(pp, prev, p); + } + prev = p; + } + l = f.readLine(); + if (!(num % 100)) { + ui.drawMsg(num + "\n" + fmt.fmtDist(dist / 1000)); + print(num, "points"); + } + } + ui.drawMsg(num + "\n" + fmt.fmtDist(dist / 1000)); + destination = prev; +} + +function time_read(n) { + print("Converting..."); + to_storage(n); + print("Running..."); + let v1 = getTime(); + let pp = {}; + pp.lat = 50; + pp.lon = 14.75; + pp.ppm = 0.08; /* Pixels per meter */ + pp.course = 270; + read(pp, n); + let v2 = getTime(); + print("Read took", (v2-v1), "seconds"); + step_init(); + print(num, "points", dist, "distance"); + setTimeout(step, 1000); +} + +var track_name = "", inf, point_num, track = [], track_points = 30, north = {}; + +function step_init() { + inf = require("Storage").open(track_name + ".st", "r"); + north = {}; + north.lat = 89.9; + north.lon = 0; + point_num = 0; + track = []; +} + +function load_next() { + while (track.length < track_points) { + let l = inf.readLine(); + if (l === undefined) { + print("End of track"); + ui.drawMsg("End of track"); + break; + } + let p = egt.parse(l); + if (!p.lat) { + print("No latitude?"); + continue; + } + p.point_num = point_num++; + p.passed = 0; + print("Loading ", p.point_num); + track.push(p); + } +} + +function paint_all(pp) { + let prev = 0; + let mDist = 99999999999, m = 0; + const fast = 0; + + g.setColor(1, 0, 0); + for (let i = 1; i < track.length; i++) { + let p = track[i]; + prev = track[i-1]; + if (0 && fmt.distance(p, pp) < 100) + p.passed = 1; + if (!fast) { + let d = distSegment(prev, p, pp); + if (d < mDist) { + mDist = d; + m = i; + } else { + g.setColor(0, 0, 0); + } + } + paint(pp, prev, p, 3); + } + if (fast) + return { quiet: 0, offtrack : 0 }; + print("Best segment was", m, "dist", mDist); + let ahead = 0, a = fmt.bearing(track[m-1], track[m]), quiet = -1; + for (let i = m+1; i < track.length; i++) { + let a2 = fmt.bearing(track[i-1], track[i]); + let ad = angleDifference(a, a2); + if (Math.abs(ad) > 20) { + if (quiet == -1) + quiet = ahead + fmt.distance(pp, track[i-1]); + print("...straight", ahead); + a = a2; + } + ahead += fmt.distance(track[i-1], track[i]); + } + print("...see", ahead); + return { quiet: quiet, offtrack: mDist }; +} + +function step_to(pp, pass_all) { + pp.x = ui.w/2; + pp.y = ui.h*0.66; + + g.setColor(0.5, 0.5, 1); + let sc = 2.5; + g.fillPoly([ pp.x, pp.y, pp.x - 5*sc, pp.y + 12*sc, pp.x + 5*sc, pp.y + 12*sc ]); + + if (0) { + g.setColor(0.5, 0.5, 1); + paint(pp, pp, destination, 1); + + g.setColor(1, 0.5, 0.5); + paint(pp, pp, north, 1); + } + + let quiet = paint_all(pp); + + if ((pass_all || track[0].passed) && distSegment(track[0], track[1], pp) > 150) { + print("Dropping ", track[0].point_num); + track.shift(); + } + return quiet; +} + +var demo_mode = 0; + +function step() { + const fast = 0; + let v1 = getTime(); + g.reset().clear(); + + let fix = gps.getGPSFix(); + + load_next(); + + let pp = fix; + pp.ppm = 0.08 * 3; /* Pixels per meter */ + + if (!fix.fix) { + let i = 2; + pp.lat = track[i].lat; + pp.lon = track[i].lon; + pp.course = fmt.bearing(track[i], track[i+1]); + } + + let quiet = step_to(pp, 1); + + if (!fast) { + g.setFont("Vector", 31); + g.setFontAlign(-1, -1); + let msg = "\noff " + fmt.fmtDist(quiet.offtrack/1000); + g.drawString(fmt.fmtFix(fix, getTime()-gps.gps_start) + msg, 3, 3); + } + if (!fast) { + g.setFont("Vector", 23); + g.setColor(0, 0, 0); + g.setFontAlign(-1, 1); + g.drawString(fmt.fmtNow(), 3, ui.h); + g.setFontAlign(1, 1); + g.drawString(fmt.fmtDist(quiet.quiet/1000), ui.w-3, ui.h); + } + + if (quiet < 200) + Bangle.setLCDPower(1); + + if (demo_mode) + track.shift(); + let v2 = getTime(); + print("Step took", (v2-v1), "seconds"); + setTimeout(step, 100); +} + +function recover() { + ui.drawMsg("Recover..."); + step_init(); + let fix = gps.getGPSFix(); + let pp = fix; + pp.ppm = 0.08 * 3; /* Pixels per meter */ + if (!fix.fix) { + print("Can't recover with no fix\n"); + fix.lat = 50.0122; + fix.lon = 14.7780; + } + load_next(); + load_next(); + while(1) { + let d = distSegment(track[0], track[1], pp); + print("Recover, d", d); + if (d < 400) + break; + track.shift(); + if (0) + step_to(pp, 1); + load_next(); + ui.drawMsg("Recover\n" + fmt.fmtDist(d / 1000)); + } +} + +function to_storage(n) { + let f2 = require("Storage").open(n+".st", "w"); + let pos = 0; + let size = 1024; + while (1) { + let d = require("Storage").read(n, pos, size); + if (!d) + break; + f2.write(d); + pos += size; + print("Copy ", pos); + } +} + +ui.init(); +fmt.init(); +egt.init(); +gps.init(); +gps.start_gps(); + +const st = require('Storage'); + +let l = /^t\..*\.egt$/; +l = st.list(l, {sf:false}); + +print(l); + +function load_track(x) { + print("Loading", x); + Bangle.buzz(50, 1); // Won't happen because load() is quicker + g.reset().clear() + .setFont("Vector", 40) + .drawString("Loading", 0, 30) + .drawString(x, 0, 80); + g.flip(); + track_name = x; + time_read(x); + + Bangle.setUI("clockupdown", btn => { + print("Button", btn); + if (btn == -1) { + recover(); + } + if (btn == 1) { + demo_mode = 1; + } + }); +} + +var menu = { + "< Back" : Bangle.load +}; +if (l.length==0) menu["No tracks"] = ()=>{}; +else for (let id in l) { + let i = id; + menu[l[id]]=()=>{ load_track(l[i]); }; +} + +g.clear(); +E.showMenu(menu); + diff --git a/apps/tvremote/README.md b/apps/tvremote/README.md new file mode 100644 index 000000000..77ee3681f --- /dev/null +++ b/apps/tvremote/README.md @@ -0,0 +1,62 @@ +# TV Remote +A [BangleJS 2](https://shop.espruino.com/banglejs2) app that allows the user to send TV input signals from their watch to their TV. +Currenly there is only support for Panasonic viera TV's however support for other brands may be considered in interest is there. + +# Requirements +1. The [Bangle GadgetBridge App](https://www.espruino.com/Gadgetbridge) with permissions allowed for `http requests`. +2. A domain name and DNS created. +3. A webserver that the DNS points to, that is set up to receive and process the watch http requests. [Here](https://github.com/Guptilious/banglejs-tvremote-webserver) is one I have created that should complete the full set up for users - provided they have their own domain name and DNS created. + +# Set Up +You will need to upload the below JSON file to your BangleJS, which will be used for config settings. At minimum you must provide: +* `webServerDNS` address, which points to your webserver. +* `username` which should mirror what is included in your webservers auth config. If using my webserver it would be `config.json`. +* `password` which should mirror what is included in your webservers auth config. If using my webserver it would be `config.json`. + +`port` and `tvIp` are optional as they can be manually assigned and updated via the tvremote watch app settings. + + ## Tv remote config example + require("Storage").write("tvremote.settings.json", { + "webServerDns": "", + "tvIp": "", + "port": "", + "username": "", + "password": "" + }); + +# Usage +Main Menu +* Select TV type (panasonic is currently the only one supported) +* Settings takes you to the settings menu, that allows you to manually assign ports and IP's. + +Settings Menu +* Device Select sends a http request to the webserver for a scrollable list devices to select. +* Manual IP takes standard number inputs and swipping up will provide a `.` for IP's. + +Power Screen +* Press button - on/off. +* Swipe left - `App Menu`. +* Swipe Right - Main Menu + +App Menu +* Scroll and select to send App menu input. +* Swipe left - Selection menu. +* Swipe right - Power Screen. + +Selection Menu +* ^ - up +* ! - down +* < - left +* `>` - right +* Swipe right - back +* Swipe left - select +* Swipe Down - Number Menu ( used for inputting key passwords). +* Swipe Up - Vol Commands + +Vol Commands +* Swipe Down - Selection Menu +* Swipe right - rewind +* Swipe left - fast forward +* Swipe Up - Play/Pause + +Back Button - Should take you back to the previous menu screen. diff --git a/apps/tvremote/app-icon.js b/apps/tvremote/app-icon.js new file mode 100644 index 000000000..00903b468 --- /dev/null +++ b/apps/tvremote/app-icon.js @@ -0,0 +1 @@ +require("heatshrink").decompress(atob("mEw4kA///7fOlMytX5muVt9T5lK6eMy2l1H4nO+i3d19jq2VxdCltTMf4A/AFsFqAYWqoXXGCxIEJqZIDGiYXCgpkTIYRjUFgIuUJAQuPgMRACEQC+0aw98+3ul3m/l3xQXMjVSkQAH/QUB2c7C48fCxEil4XBnOTC4+iC5Mi0IXKoQXKoJHKuQXKuJ3K4QXK4IXKsQXKsIXK8QXK8IXXjvd7vRC9Z3iU5hHKa5gXKoQXKoJHK0QXK0IXKj4WJksRI5UaqQXI5QXLDAOHvn290u838u+KIoZHIACAX0AH4A/ABo=")) diff --git a/apps/tvremote/app.js b/apps/tvremote/app.js new file mode 100644 index 000000000..55d9fe9ad --- /dev/null +++ b/apps/tvremote/app.js @@ -0,0 +1,655 @@ +require("Font7x11Numeric7Seg").add(Graphics); + +let deviceFileName = "tvdevicelist.json"; +let serverDataFile = "tvremote.settings.json"; +let serverData = require("Storage").readJSON(serverDataFile, true); +let devicefile = require("Storage").readJSON(deviceFileName, true); + +//console.log(require("Storage").list()); +//console.log(devicefile); + + +let serverDns = "webServerDns" in serverData ? serverData.webServerDns : 'undefined'; + +let serverPort = "port" in serverData ? serverData.port : 'undefined'; +let tvIp = "tvIp" in serverData ? serverData.tvIp : 'undefined'; +let username = "username" in serverData ? serverData.username : 'undefined'; +let password = "password" in serverData ? serverData.password : 'undefined'; + +let panaIp = tvIp; +//"tvIp" in serverData ? serverData.tvIp : 'undefined'; +let settingsPort = "port" in serverData ? serverData.port : 'undefined'; + +let counter; +let IPASSIGN; +let samsIp; +let countdownTimer = null; +let currNumber = null; +let selected = "NONE"; +let currentScreen = "power"; +let font = 'Vector'; +let RESULT_HEIGHT = 24; +let RIGHT_MARGIN = 15; +let midpoint = (g.getWidth() / 2); +let IP_AREA = [0, RESULT_HEIGHT, g.getWidth(), g.getHeight()]; // values used for key buttons +let KEY_AREA = [0, 24, g.getWidth(), g.getHeight()]; +let COLORS = { + DEFAULT: ['#FF0000'], + BLACK: ['#000000'], + WHITE: ['#FFFFFF'], + GREY: ['#808080', '#222222'] +}; // background + +let sourceApps = { + "selection": { + '!': { + grid: [0, 1], + globalGrid: [0, 0], + key: 'down' + }, + '^': { + grid: [0, 0], + globalGrid: [0, 0], + key: 'up' + }, + '<': { + grid: [1, 0], + globalGrid: [1, 0], + key: 'left' + }, + '>': { + grid: [1, 1], + globalGrid: [1, 0], + key: 'right' + } + }, + "volume": { + 'Vol Up': { + grid: [0, 0], + globalGrid: [0, 0], + key: 'volume_up' + }, + 'Vol Dwn': { + grid: [0, 1], + globalGrid: [1, 0], + key: 'volume_down' + }, + 'Mute': { + grid: [1, 0], + globalGrid: [2, 0], + key: 'mute' + }, + 'Options': { + grid: [1, 1], + globalGrid: [2, 0], + key: 'option' + } + }, + "numbers": { + '<': { + grid: [0, 3], + globalGrid: [1, 4] + }, + '0': { + grid: [1, 3], + globalGrid: [1, 4] + }, + 'ok': { + grid: [2, 3], + globalGrid: [2, 4] + }, + '1': { + grid: [0, 2], + globalGrid: [0, 3] + }, + '2': { + grid: [1, 2], + globalGrid: [1, 3] + }, + '3': { + grid: [2, 2], + globalGrid: [2, 3] + }, + '4': { + grid: [0, 1], + globalGrid: [0, 2] + }, + '5': { + grid: [1, 1], + globalGrid: [1, 2] + }, + '6': { + grid: [2, 1], + globalGrid: [2, 2] + }, + '7': { + grid: [0, 0], + globalGrid: [0, 1] + }, + '8': { + grid: [1, 0], + globalGrid: [1, 1] + }, + '9': { + grid: [2, 0], + globalGrid: [2, 1] + } + }, + "apps": [{ + "name": "Disney +", + "key": "disney" + }, + { + "name": "Netflix", + "key": "netflix" + }, + { + "name": "Amazon Prime", + "key": "prime" + }, + { + "name": "Youtube", + "key": "youtube" + }, + { + "name": "Home", + "key": "home" + }, + { + "name": "TV", + "key": "tv" + }, + { + "name": "HDMI1", + "key": "hdmi1" + }, + { + "name": "HDMI2", + "key": "hdmi2" + }, + { + "name": "HDMI3", + "key": "hdmi3" + }, + { + "name": "HDMI4", + "key": "hdmi4" + } + ] +}; +let numbersGrid = [3, 4]; +let selectionGrid = [2, 2]; +let volumeGrid = [2, 2]; +let appData = sourceApps.apps; +let volume = sourceApps.volume; +let selection = sourceApps.selection; +let numbers = sourceApps.numbers; + +function assignScreen(screen) { + currentScreen = screen; + console.log(currentScreen); +} + +function sendPost(keyPress) { + serverPort = settingsPort; + tvIp = panaIp; + let credentials = btoa(`${username}:${password}`); + let serverUrl = `https://${serverDns}:${serverPort}`; + + let keyJson = { + "command": keyPress, + "tvip": tvIp, + }; + + Bangle.http( + serverUrl, { + method: 'POST', + headers: { + 'Authorization': `Basic ${credentials}`, + }, + body: JSON.stringify(keyJson) + }) + .then(response => { + console.log("Response received:", response); + }).catch(error => { + console.error("Error sending data:", error); + }); +} + +function receiveDevices() { + let serverPort = settingsPort; + let credentials = btoa(`${username}:${password}`); + let serverUrl = `https://${serverDns}:${serverPort}/ssdp-devices.json`; + return Bangle.http( + serverUrl, { + method: 'GET', + headers: { + 'Authorization': `Basic ${credentials}` + }, + }).then(data => { + require("Storage").write(deviceFileName, data); + devicefile = require("Storage").readJSON(deviceFileName, true); + }); +} + +function prepareScreen(screen, grid, defaultColor, area) { // grid, [3, 4], colour, grid area size + for (let k in screen) { + if (screen.hasOwnProperty(k)) { + screen[k].color = screen[k].color || defaultColor; + let position = []; + let xGrid = (area[2] - area[0]) / grid[0]; + let yGrid = (area[3] - area[1]) / grid[1]; + + //console.log(xGrid + " " + yGrid); + position[0] = area[0] + xGrid * screen[k].grid[0]; + position[1] = area[1] + yGrid * screen[k].grid[1]; + + position[2] = position[0] + xGrid - 1; + position[3] = position[1] + yGrid - 1; + + screen[k].xy = position; + //console.log("prepared " + screen+"\n"); + } + } + Bangle.setUI({ + mode: "custom", + back: function() { + appMenu(); + } + }); +} + +function drawKey(name, k, selected) { // number, number data, NONE + g.setColor(COLORS.DEFAULT[0]); // set color for rectangles + g.setFont('Vector', 20).setFontAlign(0, 0); + g.fillRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]); // create rectangles based on letters xy areas + + g.setColor(COLORS.BLACK[0]).drawRect(k.xy[0], k.xy[1], k.xy[2], k.xy[3]); + + g.setColor(COLORS.WHITE[0]); // color for numbers + g.drawString(name, (k.xy[0] + k.xy[2]) / 2, (k.xy[1] + k.xy[3]) / 2); // center the keys to the rectangle that is drawn +} + +function drawKeys(area, buttons) { + g.setColor(COLORS.DEFAULT[0]); // background colour + g.fillRect(area[0], area[1], area[2], area[3]); // number grid area + for (let k in buttons) { + if (buttons.hasOwnProperty(k)) { + drawKey(k, buttons[k], k == selected); + } + } +} + +function displayOutput(num, screenValue) { // top block + num = num.toString(); + g.setFont('Vector', 18); //'7x11Numeric7Seg' + g.setFontAlign(1, 0); + g.setBgColor(0).clearRect(0, 0, g.getWidth(), RESULT_HEIGHT - 1); + g.setColor(-1); // value + + g.drawString(num, g.getWidth() - RIGHT_MARGIN, RESULT_HEIGHT / 2); +} + +function buttonPress(val, screenValue) { + + if (screenValue === "ip") { + if (val === "<") currNumber = currNumber.slice(0, -1); + else if (val === ".") currNumber = currNumber + "."; + else currNumber = currNumber == null ? val : currNumber + val; // currNumber is null if no value pressed + + let ipcount = (currNumber.match(/\./g) || []).length; + if (ipcount > 3 || currNumber.length > 15) currNumber = currNumber.slice(0, -1); + + displayOutput(currNumber, screenValue); + } + + let checkValue = appData.some(app => app.name === screenValue); // check app data + + if (checkValue) sendPost(val); // app values + + if (screenValue === "numbers") { + if (val === '<') sendPost('back'); + else if (val === 'ok') sendPost('enter'); + else sendPost("num_" + val); + } else if (screenValue === "selection") sendPost(selection[val].key); + else if (screenValue === "volume") sendPost(volume[val].key); +} + +const powerScreen = () => { + currentScreen = "power"; + g.setColor(COLORS.GREY[0]).fillRect(0, 24, g.getWidth(), g.getWidth()); // outer circ + g.setColor(COLORS.WHITE[0]).fillCircle(midpoint, 76 + 24, 50); // inner circ + g.setColor(COLORS.BLACK[0]).setFont('Vector', 25).setFontAlign(0, 0).drawString("On/Off", 88, 76 + 24); // circ text + + Bangle.setUI({ + mode: "custom", + back: function() { + mainMenu(); + } + }); +}; + +const appMenu = () => { + + assignScreen("apps"); + E.showScroller({ + h: 54, + c: appData.length, + draw: (i, r) => { + let sourceOption = appData[i]; + g.setColor(COLORS.DEFAULT[0]).fillRect((r.x), (r.y), (r.x + r.w), (r.y + r.h)); + g.setColor(COLORS.BLACK[0]).drawRect((r.x), (r.y), (r.x + r.w), (r.y + r.h)); + g.setColor(COLORS.WHITE[0]).setFont(font, 20).setFontAlign(-1, 1).drawString(sourceOption.name, 15, r.y + 32); + }, + + select: i => { + let sourceOption = appData[i]; + let appPressed = sourceOption.name; + let appKey = sourceOption.key; + buttonPress(appKey, appPressed); + }, + + back: main => { + E.showScroller(); + powerScreen(); + }, + }); + g.flip(); // force a render before widgets have finished drawing +}; + +function ipScreen() { + //require("widget_utils").hide(); + assignScreen("ip"); + currNumber = ""; + prepareScreen(numbers, numbersGrid, COLORS.DEFAULT, IP_AREA); + drawKeys(IP_AREA, numbers); + displayOutput(0); +} + +let tvSelector = { + "": { + title: "TV Selector", + back: function() { + load(); //E.showMenu(tvSelector); + } + }, + "Panasonic": function() { + assignScreen("power"); + powerScreen(); + }, + "Samsung": function() { + assignScreen("power"); + powerScreen(); + }, + "Settings": function() { + subMenu(); + } +}; + +function mainMenu() { + assignScreen("mainmenu"); + E.showMenu(tvSelector); +} + +function clearCountdown() { + if (countdownTimer) { + clearTimeout(countdownTimer); + countdownTimer = null; + } +} + +function countDown(callback) { + require("widget_utils").show(); + if (counter === 0) { + callback(); // Call the callback function when countdown reaches 0 + return; + } + E.showMessage(`Searching for devices...\n${counter}`, "Device Search"); + counter--; + countdownTimer = setTimeout(() => countDown(callback), 1000); +} + +function clearDisplayOutput() { + g.clear(); + Bangle.loadWidgets(); + Bangle.drawWidgets(); +} + +function subMenu() { + + if (typeof IPASSIGN !== 'undefined' && currNumber !== "") { + if (IPASSIGN === "pana") { + console.log("current numeber = " + currNumber); + panaIp = currNumber; + console.log("pana ip " + panaIp); + console.log("default ip " + serverData.tvIp); + serverData.tvIp = panaIp; + require("Storage").write(serverDataFile, serverData); + console.log("tv ip is now " + serverData.tvIp); + + } else if (IPASSIGN === "sams") { + samsIp = currNumber; + + } else if (IPASSIGN === "port") { + settingsPort = currNumber; + console.log("setting port " + settingsPort); + console.log("server port " + serverData.port); + serverData.port = settingsPort; + require("Storage").write(serverDataFile, serverData); + console.log("port is now " + serverData.port); + } + } + + require("widget_utils").show(); + assignScreen("settingssub"); + clearDisplayOutput(); + + let settingssub = { + "": { + title: "Settings", + back: function() { + E.showMenu(tvSelector); + clearCountdown(); + } + }, + }; + + if (typeof settingsPort !== 'undefined' && settingsPort !== 'undefined') { + let portHeader = `Port: ${settingsPort}`; + settingssub[portHeader] = function() { + IPASSIGN = "port"; + ipScreen(); + }; + } else { + settingssub["Set DNS Port"] = function() { + IPASSIGN = "port"; + ipScreen(); + }; + } + + if (typeof panaIp !== 'undefined' && panaIp !== 'undefined') { + let panaheader = `Pana IP: ${panaIp}`; + settingssub[panaheader] = function() { + IPASSIGN = "pana"; + E.showMenu(deviceSelect); + devicefile = require("Storage").readJSON("tvdevicelist.json", true); + console.log(devicefile); + }; + } else { + settingssub["Set Pana IP"] = function() { + IPASSIGN = "pana"; + ipScreen(); + }; + } + + if (typeof samsIp !== 'undefined' && panaIp !== 'undefined') { + let samsheader = `Sams IP: ${samsIp}`; + settingssub[samsheader] = function() { + IPASSIGN = "sams"; + ipScreen(); + }; + } else { + settingssub["Set Sams IP"] = function() { + IPASSIGN = "sams"; + ipScreen(); + }; + } + + E.showMenu(settingssub); +} + +const deviceMenu = () => { + let parsedResp = JSON.parse(devicefile.resp); + E.showScroller({ + h: 54, + c: parsedResp.length, + draw: (i, r) => { + let sourceOption = parsedResp[i]; + g.setColor(COLORS.DEFAULT[0]).fillRect((r.x), (r.y), (r.x + r.w), (r.y + r.h)); + g.setColor(COLORS.BLACK[0]).drawRect((r.x), (r.y), (r.x + r.w), (r.y + r.h)); + g.setColor(COLORS.WHITE[0]).setFont(font, 15).setFontAlign(-1, 1).drawString(sourceOption.name, 15, r.y + 32); + }, + + select: i => { + let sourceOption = parsedResp[i]; + //let devicePressed = sourceOption.name; + let deviceIp = sourceOption.ip; + + assignScreen("deviceSearch"); + serverData.tvIp = deviceIp.replace('http://', ''); + currNumber = serverData.tvIp; + require("Storage").write(serverDataFile, serverData); + console.log("tv ip is now " + serverData.tvIp); + subMenu(); + }, + + back: main => { + E.showScroller(); + E.showMenu(deviceSelect); + }, + }); + g.flip(); // force a render before widgets have finished drawing +}; + + +let deviceSelect = { + "": { + title: "Device Select", + back: function() { + subMenu(); + } + }, + "Manual IP Assign": function() { + ipScreen(); + }, + "Device Select": function() { + receiveDevices(); + counter = 5; + countDown(deviceMenu); + } +}; + + +function swipeHandler(LR, UD) { + if (LR == -1) { // swipe left + if (currentScreen === "power") { + assignScreen("apps"); + appMenu(); + + } else if (currentScreen === "apps") { + //require("widget_utils").hide(); + assignScreen("selection"); + E.showScroller(); + prepareScreen(selection, selectionGrid, COLORS.DEFAULT, KEY_AREA); + drawKeys(KEY_AREA, selection); + + } else if (currentScreen === "volume") { + sendPost("fast_forward"); + + } else if (currentScreen === "selection") { + sendPost("enter"); + } + } + if (LR == 1) { // swipe right + if (currentScreen === "apps") { + assignScreen("power"); + E.showScroller(); + powerScreen(); + } else if (currentScreen === "volume") { + sendPost("rewind"); + } else if (currentScreen === "selection") { + sendPost("back"); + } + } + if (UD == -1) { // swipe up + if (currentScreen === "selection") { + assignScreen("volume"); + prepareScreen(volume, volumeGrid, COLORS.DEFAULT, KEY_AREA); + drawKeys(KEY_AREA, volume); + } else if (currentScreen === "volume") { + sendPost("enter"); + } else if (currentScreen === "ip") { + buttonPress(".", "ip"); + } else if (currentScreen == "numbers") { + assignScreen("selection"); + prepareScreen(selection, selectionGrid, COLORS.DEFAULT, KEY_AREA); + drawKeys(KEY_AREA, selection); + } + } + if (UD == 1) { // swipe down + if (currentScreen === "volume") { + assignScreen("selection"); + prepareScreen(selection, selectionGrid, COLORS.DEFAULT, KEY_AREA); + drawKeys(KEY_AREA, selection); + } else if (currentScreen === "selection") { + assignScreen("numbers"); + prepareScreen(numbers, numbersGrid, COLORS.DEFAULT, KEY_AREA); + drawKeys(KEY_AREA, numbers); + } + } +} +Bangle.on('swipe', swipeHandler); + + +function touchHandler(button, e) { + const screenActions = { + ip: () => checkButtons(numbers), + volume: () => checkButtons(volume), + numbers: () => checkButtons(numbers), + selection: () => checkButtons(selection), + power: () => { + if (Math.pow(e.x - 88, 2) + Math.pow(e.y - 88, 2) < 2500) { + sendPost("power"); + } + } + }; + + function checkButtons(buttonMap) { + for (let key in buttonMap) { + if (typeof buttonMap[key] === "undefined") continue; + let r = buttonMap[key].xy; + + if (e.x >= r[0] && e.y >= r[1] && e.x < r[2] && e.y < r[3]) { + if (currentScreen === "ip" && key === "ok") { + subMenu(); + } else { + buttonPress("" + key, currentScreen); + } + } + } + } + + if (currentScreen in screenActions) screenActions[currentScreen](); +} +Bangle.on('touch', touchHandler); + +Bangle.loadWidgets(); +Bangle.drawWidgets(); + +if (serverData === undefined) { + E.showAlert(`No settings.\nSee READ.me`, "Config Error").then(function() { + mainMenu(); + }); +} else { + mainMenu(); +} diff --git a/apps/tvremote/app.png b/apps/tvremote/app.png new file mode 100644 index 000000000..2d661af88 Binary files /dev/null and b/apps/tvremote/app.png differ diff --git a/apps/tvremote/metadata.json b/apps/tvremote/metadata.json new file mode 100644 index 000000000..a624b625f --- /dev/null +++ b/apps/tvremote/metadata.json @@ -0,0 +1,15 @@ +{ "id": "tvremote", + "name": "TV Remote", + "shortName":"TV Remote", + "icon": "app.png", + "version":"0.01", + "description": "remote for controlling your tv, using a webserver and the bangle Gadget Bridge (https://www.espruino.com/Gadgetbridge).", + "icon": "app.png", + "tags": "remote", + "supports": ["BANGLEJS2"], + "readme": "README.md", + "storage": [ + {"name":"tvremote.app.js","url":"app.js"}, + {"name":"tvremote.img","url":"app-icon.js","evaluate":true} + ] +} diff --git a/backup.js b/backup.js index 09f65af99..cc4c327bc 100644 --- a/backup.js +++ b/backup.js @@ -53,7 +53,12 @@ function bangleDownload() { }).then(content => { Progress.hide({ sticky: true }); showToast('Backup complete!', 'success'); - Espruino.Core.Utils.fileSaveDialog(content, "Banglejs backup.zip"); + if (typeof Android !== "undefined" && typeof Android.saveFile === 'function') { + // Recent Gadgetbridge version that provides the saveFile interface + Android.saveFile("Banglejs backup.zip", "application/zip", btoa(content)); + } else { + Espruino.Core.Utils.fileSaveDialog(content, "Banglejs backup.zip"); + } }).catch(err => { Progress.hide({ sticky: true }); showToast('Backup failed, ' + err, 'error'); diff --git a/css/main.css b/css/main.css index 810b9a032..ce27a0eb7 100644 --- a/css/main.css +++ b/css/main.css @@ -81,7 +81,10 @@ a.btn.btn-link.dropdown-toggle { min-height: 8em; } -.tile-content { position: relative; } +.tile-content { + position: relative; + overflow-wrap: anywhere; /* stop long text like links pushing the width out too far*/ +} .link-github { position:absolute; top: 36px; @@ -137,4 +140,4 @@ Not sure how to get 'normal' wrap behaviour (eg fill up until max-width, then wr /*.tooltip:hover::after { white-space: normal; min-width: 160px; -}*/ \ No newline at end of file +}*/ diff --git a/modules/more_pickers.js b/modules/more_pickers.js index 596b36fdf..865ff689c 100644 --- a/modules/more_pickers.js +++ b/modules/more_pickers.js @@ -10,27 +10,37 @@ exports.doublePicker = function (options) { var v_1 = options.value_1; var v_2 = options.value_2; - function draw() { + function draw1() { + var txt_1 = options.format_1 ? options.format_1(v_1) : v_1; + g.setColor(g.theme.bg2) .fillRect(14, 60, 81, 166) - .fillRect(95, 60, 162, 166); - - g.setColor(g.theme.fg2) + .setColor(g.theme.fg2) .fillPoly([47.5, 68, 62.5, 83, 32.5, 83]) .fillPoly([47.5, 158, 62.5, 143, 32.5, 143]) - .fillPoly([128.5, 68, 143.5, 83, 113.5, 83]) - .fillPoly([128.5, 158, 143.5, 143, 113.5, 143]); - - var txt_1 = options.format_1 ? options.format_1(v_1) : v_1; + .setFontAlign(0, 0) + .setFontVector(Math.min(30, (R.w - 110) * 100 / g.setFontVector(100).stringWidth(txt_1))) + .drawString(txt_1, 47.5, 113); + } + function draw2() { var txt_2 = options.format_2 ? options.format_2(v_2) : v_2; - g.setFontAlign(0, 0) - .setFontVector(Math.min(30, (R.w - 110) * 100 / g.setFontVector(100).stringWidth(txt_1))) - .drawString(txt_1, 47.5, 113) + g.setColor(g.theme.bg2) + .fillRect(95, 60, 162, 166) + .setColor(g.theme.fg2) + .fillPoly([128.5, 68, 143.5, 83, 113.5, 83]) + .fillPoly([128.5, 158, 143.5, 143, 113.5, 143]) + .setFontAlign(0, 0) .setFontVector(Math.min(30, (R.w - 110) * 100 / g.setFontVector(100).stringWidth(txt_2))) - .drawString(txt_2, 128.5, 113) - .setFontVector(30) - .drawString(options.separator ?? "", 88, 110); + .drawString(txt_2, 128.5, 113); + } + function drawSeparator(){ + g.setFontVector(30).drawString(options.separator ?? "", 88, 110); + } + function drawAll() { + draw1(); + draw2(); + drawSeparator() } function cb(dir, x_part) { if (dir) { @@ -38,12 +48,14 @@ exports.doublePicker = function (options) { v_1 -= (dir || 1) * (options.step_1 || 1); if (options.min_1 !== undefined && v_1 < options.min_1) v_1 = options.wrap_1 ? options.max_1 : options.min_1; if (options.max_1 !== undefined && v_1 > options.max_1) v_1 = options.wrap_1 ? options.min_1 : options.max_1; + draw1(); } else { v_2 -= (dir || 1) * (options.step_2 || 1); if (options.min_2 !== undefined && v_2 < options.min_2) v_2 = options.wrap_2 ? options.max_2 : options.min_2; if (options.max_2 !== undefined && v_2 > options.max_2) v_2 = options.wrap_2 ? options.min_2 : options.max_2; + draw2(); } - draw(); + drawSeparator(); } else { // actually selected options.value_1 = v_1; options.value_2 = v_2; @@ -52,14 +64,14 @@ exports.doublePicker = function (options) { } } - draw(); + drawAll(); var dy = 0; Bangle.setUI({ mode: "custom", back: options.back, remove: options.remove, - redraw: draw, + redraw: drawAll, drag: e => { dy += e.dy; // after a certain amount of dragging up/down fire cb if (!e.b) dy = 0; @@ -101,51 +113,72 @@ exports.triplePicker = function (options) { var v_2 = options.value_2; var v_3 = options.value_3; - function draw() { + function draw1() { + var txt_1 = options.format_1 ? options.format_1(v_1) : v_1; + g.setColor(g.theme.bg2) .fillRect(8, 60, 56, 166) - .fillRect(64, 60, 112, 166) - .fillRect(120, 60, 168, 166); - - g.setColor(g.theme.fg2) + .setColor(g.theme.fg2) .fillPoly([32, 68, 47, 83, 17, 83]) .fillPoly([32, 158, 47, 143, 17, 143]) + .setFontAlign(0, 0) + .setFontVector(Math.min(30, (R.w - 130) * 100 / g.setFontVector(100).stringWidth(txt_1))) + .drawString(txt_1, 32, 113); + } + function draw2() { + var txt_2 = options.format_2 ? options.format_2(v_2) : v_2; + + g.setColor(g.theme.bg2) + .fillRect(64, 60, 112, 166) + .setColor(g.theme.fg2) .fillPoly([88, 68, 103, 83, 73, 83]) .fillPoly([88, 158, 103, 143, 73, 143]) - .fillPoly([144, 68, 159, 83, 129, 83]) - .fillPoly([144, 158, 159, 143, 129, 143]); - - var txt_1 = options.format_1 ? options.format_1(v_1) : v_1; - var txt_2 = options.format_2 ? options.format_2(v_2) : v_2; - var txt_3 = options.format_3 ? options.format_3(v_3) : v_3; - - g.setFontAlign(0, 0) - .setFontVector(Math.min(30, (R.w - 130) * 100 / g.setFontVector(100).stringWidth(txt_1))) - .drawString(txt_1, 32, 113) + .setFontAlign(0, 0) .setFontVector(Math.min(30, (R.w - 130) * 100 / g.setFontVector(100).stringWidth(txt_2))) - .drawString(txt_2, 88, 113) + .drawString(txt_2, 88, 113); + } + function draw3() { + var txt_3 = options.format_3 ? options.format_3(v_3) : v_3; + + g.setColor(g.theme.bg2) + .fillRect(120, 60, 168, 166) + .setColor(g.theme.fg2) + .fillPoly([144, 68, 159, 83, 129, 83]) + .fillPoly([144, 158, 159, 143, 129, 143]) + .setFontAlign(0, 0) .setFontVector(Math.min(30, (R.w - 130) * 100 / g.setFontVector(100).stringWidth(txt_3))) - .drawString(txt_3, 144, 113) - .setFontVector(30) + .drawString(txt_3, 144, 113); + } + function drawSeparators(){ + g.setFontVector(30) .drawString(options.separator_1 ?? "", 60, 113) .drawString(options.separator_2 ?? "", 116, 113); } + function drawAll() { + draw1(); + draw2(); + draw3(); + drawSeparators(); + } function cb(dir, x_part) { if (dir) { if (x_part == -1) { v_1 -= (dir || 1) * (options.step_1 || 1); if (options.min_1 !== undefined && v_1 < options.min_1) v_1 = options.wrap_1 ? options.max_1 : options.min_1; if (options.max_1 !== undefined && v_1 > options.max_1) v_1 = options.wrap_1 ? options.min_1 : options.max_1; + draw1(); } else if (x_part == 0) { v_2 -= (dir || 1) * (options.step_2 || 1); if (options.min_2 !== undefined && v_2 < options.min_2) v_2 = options.wrap_2 ? options.max_2 : options.min_3; if (options.max_2 !== undefined && v_2 > options.max_2) v_2 = options.wrap_2 ? options.min_2 : options.max_3; + draw2(); } else { v_3 -= (dir || 1) * (options.step_3 || 1); if (options.min_3 !== undefined && v_3 < options.min_3) v_3 = options.wrap_3 ? options.max_3 : options.min_3; if (options.max_3 !== undefined && v_3 > options.max_3) v_3 = options.wrap_3 ? options.min_3 : options.max_3; + draw3(); } - draw(); + drawSeparators(); } else { // actually selected options.value_1 = v_1; options.value_2 = v_2; @@ -155,14 +188,14 @@ exports.triplePicker = function (options) { } } - draw(); + drawAll(); var dy = 0; Bangle.setUI({ mode: "custom", back: options.back, remove: options.remove, - redraw: draw, + redraw: drawAll, drag: e => { dy += e.dy; // after a certain amount of dragging up/down fire cb if (!e.b) dy = 0; diff --git a/webtools b/webtools index 71f271a1c..c59402259 160000 --- a/webtools +++ b/webtools @@ -1 +1 @@ -Subproject commit 71f271a1c7be37efe4e472b7482b08ded1d0ab6f +Subproject commit c59402259c779b578e68995ea0237b813fab09c0