Merge branch 'master' into env

master
Gordon Williams 2023-08-07 08:48:40 +01:00 committed by GitHub
commit bdcfb7e616
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
170 changed files with 4798 additions and 2343 deletions

View File

@ -98,7 +98,7 @@ This is the best way to test...
**Note:** It's a great idea to get a local copy of the repository on your PC, **Note:** It's a great idea to get a local copy of the repository on your PC,
then run `bin/sanitycheck.js` - it'll run through a bunch of common issues then run `bin/sanitycheck.js` - it'll run through a bunch of common issues
that there might be. To get the project running locally, you have to initialize and update the git submodules first: `git submodule --init && git submodule update`. that there might be. To get the project running locally, you have to initialize and update the git submodules first: `git submodule update --init`.
Be aware of the delay between commits and updates on github.io - it can take a few minutes (and a 'hard refresh' of your browser) for changes to take effect. Be aware of the delay between commits and updates on github.io - it can take a few minutes (and a 'hard refresh' of your browser) for changes to take effect.

View File

@ -135,15 +135,17 @@
<h3>Utilities</h3> <h3>Utilities</h3>
<p> <p>
<button class="btn tooltip" id="settime" data-tooltip="Set the Bangle's time to your Browser's time">Set Bangle.js Time</button> <button class="btn tooltip" id="settime" data-tooltip="Set the Bangle's time to your Browser's time">Set Bangle.js Time</button>
<button class="btn tooltip" id="screenshot" data-tooltip="Create screenshot">Screenshot</button>
<button class="btn tooltip" id="downloadallapps" data-tooltip="Download all Bangle.js files to a ZIP file">Backup</button>
<button class="btn tooltip" id="uploadallapps" data-tooltip="Restore Bangle.js from a ZIP file">Restore</button>
</p><p>
<button class="btn tooltip" id="removeall" data-tooltip="Delete everything, leave it blank">Remove all Apps</button> <button class="btn tooltip" id="removeall" data-tooltip="Delete everything, leave it blank">Remove all Apps</button>
<button class="btn tooltip" id="reinstallall" data-tooltip="Re-install every app, leave all data">Reinstall apps</button> <button class="btn tooltip" id="reinstallall" data-tooltip="Re-install every app, leave all data">Reinstall apps</button>
<button class="btn tooltip" id="installdefault" data-tooltip="Delete everything, install default apps">Install default apps</button> <button class="btn tooltip" id="installdefault" data-tooltip="Delete everything, install default apps">Install default apps</button>
<button class="btn tooltip" id="installfavourite" data-tooltip="Delete everything, install your favourites">Install favourite apps</button> <button class="btn tooltip" id="installfavourite" data-tooltip="Delete everything, install your favourites">Install favourite apps</button>
<button class="btn tooltip" id="defaultbanglesettings" data-tooltip="Reset your Bangle's settings to the defaults">Reset Settings</button>
</p><p> </p><p>
<button class="btn tooltip" id="newGithubIssue" data-tooltip="Create a new issue on GitHub">New issue on GitHub</button> <button class="btn tooltip" id="newGithubIssue" data-tooltip="Create a new issue on GitHub">New issue on GitHub</button>
<button class="btn tooltip" id="downloadallapps" data-tooltip="Download all Bangle.js files to a ZIP file">Backup</button>
<button class="btn tooltip" id="uploadallapps" data-tooltip="Restore Bangle.js from a ZIP file">Restore</button>
<button class="btn tooltip" id="defaultbanglesettings" data-tooltip="Reset your Bangle's settings to the defaults">Reset Settings</button>
<button class="btn tooltip" id="webideremote" data-tooltip="Enable the Web IDE remote server">Web IDE Remote</button> <button class="btn tooltip" id="webideremote" data-tooltip="Enable the Web IDE remote server">Web IDE Remote</button>
</p> </p>
<h3>Settings</h3> <h3>Settings</h3>
@ -172,6 +174,10 @@
<input type="checkbox" id="settings-minify"> <input type="checkbox" id="settings-minify">
<i class="form-icon"></i> Minify apps before upload (⚠DANGER⚠: Not recommended. Uploads smaller, faster apps but this <b>will</b> break many apps) <i class="form-icon"></i> Minify apps before upload (⚠DANGER⚠: Not recommended. Uploads smaller, faster apps but this <b>will</b> break many apps)
</label> </label>
<label class="form-switch">
<input type="checkbox" id="settings-alwaysAllowUpdate">
<i class="form-icon"></i> Always allow to reinstall apps in place regardless of the version
</label>
<button class="btn" id="defaultsettings">Reset to default App Loader settings</button> <button class="btn" id="defaultsettings">Reset to default App Loader settings</button>
</details> </details>
</div> </div>

View File

@ -44,3 +44,4 @@
0.39: Dated event repeat option 0.39: Dated event repeat option
0.40: Use substring of message when it's longer than fits the designated menu entry. 0.40: Use substring of message when it's longer than fits the designated menu entry.
0.41: Fix a menu bug affecting alarms with empty messages. 0.41: Fix a menu bug affecting alarms with empty messages.
0.42: Fix date not getting saved in event edit menu when tapping Confirm

View File

@ -190,7 +190,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
}, },
/*LANG*/"Cancel": () => showMainMenu(), /*LANG*/"Cancel": () => showMainMenu(),
/*LANG*/"Confirm": () => { /*LANG*/"Confirm": () => {
prepareAlarmForSave(alarm, alarmIndex, time); prepareAlarmForSave(alarm, alarmIndex, time, date);
saveAndReload(); saveAndReload();
showMainMenu(); showMainMenu();
} }

View File

@ -2,7 +2,7 @@
"id": "alarm", "id": "alarm",
"name": "Alarms & Timers", "name": "Alarms & Timers",
"shortName": "Alarms", "shortName": "Alarms",
"version": "0.41", "version": "0.42",
"description": "Set alarms and timers on your Bangle", "description": "Set alarms and timers on your Bangle",
"icon": "app.png", "icon": "app.png",
"tags": "tool,alarm", "tags": "tool,alarm",

View File

@ -29,3 +29,4 @@
0.28: Navigation messages no longer launch the Maps view unless they're new 0.28: Navigation messages no longer launch the Maps view unless they're new
0.29: Support for http request xpath return format 0.29: Support for http request xpath return format
0.30: Send firmware and hardware versions on connection 0.30: Send firmware and hardware versions on connection
Allow alarm enable/disable

View File

@ -86,7 +86,7 @@
var a = require("sched").newDefaultAlarm(); var a = require("sched").newDefaultAlarm();
a.id = "gb"+j; a.id = "gb"+j;
a.appid = "gbalarms"; a.appid = "gbalarms";
a.on = true; a.on = event.d[j].on !== undefined ? event.d[j].on : true;
a.t = event.d[j].h * 3600000 + event.d[j].m * 60000; a.t = event.d[j].h * 3600000 + event.d[j].m * 60000;
a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format a.dow = ((dow&63)<<1) | (dow>>6); // Gadgetbridge sends DOW in a different format
a.last = last; a.last = last;

View File

@ -2,3 +2,4 @@
0.02: Store last GPS lock, can be used instead of waiting for new GPS on start 0.02: Store last GPS lock, can be used instead of waiting for new GPS on start
0.03: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps 0.03: Use 'modules/suncalc.js' to avoid it being copied 8 times for different apps
0.04: Compatibility with Bangle.js 2, get location from My Location 0.04: Compatibility with Bangle.js 2, get location from My Location
0.05: Enable widgets

View File

@ -11,7 +11,6 @@
const SunCalc = require("suncalc"); // from modules folder const SunCalc = require("suncalc"); // from modules folder
const storage = require("Storage"); const storage = require("Storage");
const BANGLEJS2 = process.env.HWVERSION == 2; // check for bangle 2
function drawMoon(phase, x, y) { function drawMoon(phase, x, y) {
const moonImgFiles = [ const moonImgFiles = [
@ -110,7 +109,7 @@ function drawPoints() {
} }
function drawData(title, obj, startX, startY) { function drawData(title, obj, startX, startY) {
g.clear(); g.clearRect(Bangle.appRect);
drawTitle(title); drawTitle(title);
let xPos, yPos; let xPos, yPos;
@ -154,9 +153,7 @@ function drawMoonPositionPage(gps, title) {
drawPoints(); drawPoints();
drawPoint(azimuthDegrees, 8, moonColor); drawPoint(azimuthDegrees, 8, moonColor);
let m = setWatch(() => { Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)});
let m = moonIndexPageMenu(gps);
}, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"});
} }
function drawMoonIlluminationPage(gps, title) { function drawMoonIlluminationPage(gps, title) {
@ -174,9 +171,7 @@ function drawMoonIlluminationPage(gps, title) {
drawData(title, pageData, null, 35); drawData(title, pageData, null, 35);
drawMoon(phaseIdx, g.getWidth() / 2, g.getHeight() / 2); drawMoon(phaseIdx, g.getWidth() / 2, g.getHeight() / 2);
let m = setWatch(() => { Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)});
let m = moonIndexPageMenu(gps);
}, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"});
} }
@ -202,9 +197,7 @@ function drawMoonTimesPage(gps, title) {
const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI); const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI);
drawPoint(setAzimuthDegrees, 8, moonColor); drawPoint(setAzimuthDegrees, 8, moonColor);
let m = setWatch(() => { Bangle.setUI({mode: "custom", back: () => moonIndexPageMenu(gps)});
let m = moonIndexPageMenu(gps);
}, BANGLEJS2 ? BTN : BTN3, {repease: false, edge: "falling"});
} }
function drawSunShowPage(gps, key, date) { function drawSunShowPage(gps, key, date) {
@ -233,9 +226,7 @@ function drawSunShowPage(gps, key, date) {
// Draw the suns position // Draw the suns position
drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 0}); drawPoint(azimuthDegrees, 8, {r: 1, g: 1, b: 0});
m = setWatch(() => { Bangle.setUI({mode: "custom", back: () => sunIndexPageMenu(gps)});
m = sunIndexPageMenu(gps);
}, BANGLEJS2 ? BTN : BTN3, {repeat: false, edge: "falling"});
return null; return null;
} }
@ -314,7 +305,9 @@ function getCenterStringX(str) {
function init() { function init() {
let location = require("Storage").readJSON("mylocation.json",1)||{"lat":51.5072,"lon":0.1276,"location":"London"}; let location = require("Storage").readJSON("mylocation.json",1)||{"lat":51.5072,"lon":0.1276,"location":"London"};
Bangle.loadWidgets();
indexPageMenu(location); indexPageMenu(location);
Bangle.drawWidgets();
} }
let m; let m;

View File

@ -1,7 +1,7 @@
{ {
"id": "astrocalc", "id": "astrocalc",
"name": "Astrocalc", "name": "Astrocalc",
"version": "0.04", "version": "0.05",
"description": "Calculates interesting information on the sun like sunset and sunrise and moon cycles for the current day based on your location from MyLocation app", "description": "Calculates interesting information on the sun like sunset and sunrise and moon cycles for the current day based on your location from MyLocation app",
"icon": "astrocalc.png", "icon": "astrocalc.png",
"tags": "app,sun,moon,cycles,tool", "tags": "app,sun,moon,cycles,tool",

View File

@ -1 +1,2 @@
0.01: New App! 0.01: New App!
0.02: Handle missing settings (e.g. first-install)

View File

@ -1,5 +1,5 @@
(() => { (() => {
const chargingRotation = 0 | require('Storage').readJSON("chargerot.settings.json").rotate; const chargingRotation = 0 | (require('Storage').readJSON("chargerot.settings.json",1)||{}).rotate;
const defaultRotation = 0 | require('Storage').readJSON("setting.json").rotate; const defaultRotation = 0 | require('Storage').readJSON("setting.json").rotate;
if (Bangle.isCharging()) g.setRotation(chargingRotation&3,chargingRotation>>2).clear(); if (Bangle.isCharging()) g.setRotation(chargingRotation&3,chargingRotation>>2).clear();
Bangle.on('charging', (charging) => { Bangle.on('charging', (charging) => {

View File

@ -1,7 +1,7 @@
{ {
"id": "chargerot", "id": "chargerot",
"name": "Charge LCD rotation", "name": "Charge LCD rotation",
"version": "0.01", "version": "0.02",
"description": "When charging, this app can rotate your screen and revert it when unplugged. Made for all sort of cradles.", "description": "When charging, this app can rotate your screen and revert it when unplugged. Made for all sort of cradles.",
"icon": "icon.png", "icon": "icon.png",
"tags": "battery", "tags": "battery",

View File

@ -4,4 +4,5 @@
0.04: On 2v18+ firmware, we can now stop swipe events from being handled by other apps 0.04: On 2v18+ firmware, we can now stop swipe events from being handled by other apps
eg. when a clockinfo is selected, swipes won't affect swipe-down widgets eg. when a clockinfo is selected, swipes won't affect swipe-down widgets
0.05: Reported image for battery is now transparent (2v18+) 0.05: Reported image for battery is now transparent (2v18+)
0.06: When >1 clockinfo, swiping one back tries to ensure they don't display the same thing 0.06: When >1 clockinfo, swiping one back tries to ensure they don't display the same thing
0.07: Developer tweak: clkinfo load errors are emitted

View File

@ -141,7 +141,7 @@ exports.load = function() {
if(b) b.items = b.items.concat(a.items); if(b) b.items = b.items.concat(a.items);
else menu = menu.concat(a); else menu = menu.concat(a);
} catch(e){ } catch(e){
console.log("Could not load clock info "+E.toJS(fn)); console.log("Could not load clock info "+E.toJS(fn)+": "+e);
} }
}); });

View File

@ -1,7 +1,7 @@
{ "id": "clock_info", { "id": "clock_info",
"name": "Clock Info Module", "name": "Clock Info Module",
"shortName": "Clock Info", "shortName": "Clock Info",
"version":"0.06", "version":"0.07",
"description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)", "description": "A library used by clocks to provide extra information on the clock face (Altitude, BPM, etc)",
"icon": "app.png", "icon": "app.png",
"type": "module", "type": "module",

View File

@ -5,3 +5,5 @@
0.05: Now scrolls text when string gets longer than screen width. 0.05: Now scrolls text when string gets longer than screen width.
0.06: The code is now more reliable and the input snappier. Widgets will be drawn if present. 0.06: The code is now more reliable and the input snappier. Widgets will be drawn if present.
0.07: Settings for display colors 0.07: Settings for display colors
0.08: Catch and discard swipe events on fw2v19 and up (as well as some cutting
edge 2v18 ones), allowing compatability with the Back Swipe app.

View File

@ -103,6 +103,133 @@ exports.input = function(options) {
initDraw(); initDraw();
//setTimeout(initDraw, 0); // So Bangle.appRect reads the correct environment. It would draw off to the side sometimes otherwise. //setTimeout(initDraw, 0); // So Bangle.appRect reads the correct environment. It would draw off to the side sometimes otherwise.
let dragHandlerDB = function(event) {
"ram";
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
// Choose character by draging along red rectangle at bottom of screen
if (event.y >= ( (R.y+R.h) - 12 )) {
// Translate x-position to character
if (event.x < ABCPADDING) { abcHL = 0; }
else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
// Datastream for development purposes
//print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
// Unmark previous character and mark the current one...
// Handling switching between letters and numbers/punctuation
if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
if (abcHL != abcHLPrev) {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
}
// Print string at top of screen
if (event.b == 0) {
text = text + ABC.charAt(abcHL);
updateTopString();
// Autoswitching letter case
if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
}
// Update previous character to current one
abcHLPrev = abcHL;
typePrev = 'abc';
}
// 12345678901234567890
// Choose number or puctuation by draging on green rectangle
else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) {
// Translate x-position to character
if (event.x < NUMPADDING) { numHL = 0; }
else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
else { numHL = Math.floor((event.x-NUMPADDING)/6); }
// Datastream for development purposes
//print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
// Unmark previous character and mark the current one...
// Handling switching between letters and numbers/punctuation
if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
if (numHL != numHLPrev) {
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
}
// Print string at top of screen
if (event.b == 0) {
g.setColor(HLCOLOR);
// Backspace if releasing before list of numbers/punctuation
if (event.x < NUMPADDING) {
// show delete sign
showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
delSpaceLast = 1;
text = text.slice(0, -1);
updateTopString();
//print(text);
}
// Append space if releasing after list of numbers/punctuation
else if (event.x > (R.x+R.w)-NUMPADDING) {
//show space sign
showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
delSpaceLast = 1;
text = text + ' ';
updateTopString();
//print(text);
}
// Append selected number/punctuation
else {
text = text + NUMHIDDEN.charAt(numHL);
updateTopString();
// Autoswitching letter case
if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
}
}
// Update previous character to current one
numHLPrev = numHL;
typePrev = 'num';
}
// Make a space or backspace by swiping right or left on screen above green rectangle
else if (event.y > 20+4) {
if (event.b == 0) {
g.setColor(HLCOLOR);
if (event.x < (R.x+R.w)/2) {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
// show delete sign
showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
delSpaceLast = 1;
// Backspace and draw string upper right corner
text = text.slice(0, -1);
updateTopString();
if (text.length==0) changeCase(abcHL);
//print(text, 'undid');
}
else {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
//show space sign
showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
delSpaceLast = 1;
// Append space and draw string upper right corner
text = text + NUMHIDDEN.charAt(0);
updateTopString();
//print(text, 'made space');
}
}
}
};
let catchSwipe = ()=>{
E.stopEventPropagation&&E.stopEventPropagation();
};
function changeCase(abcHL) { function changeCase(abcHL) {
if (settings.uppercase) return; if (settings.uppercase) return;
g.setColor(BGCOLOR); g.setColor(BGCOLOR);
@ -119,131 +246,12 @@ exports.input = function(options) {
mode: 'custom', mode: 'custom',
back: ()=>{ back: ()=>{
Bangle.setUI(); Bangle.setUI();
Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up).
g.clearRect(Bangle.appRect); g.clearRect(Bangle.appRect);
resolve(text); resolve(text);
}, },
drag: function(event) { drag: dragHandlerDB,
"ram";
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
// Choose character by draging along red rectangle at bottom of screen
if (event.y >= ( (R.y+R.h) - 12 )) {
// Translate x-position to character
if (event.x < ABCPADDING) { abcHL = 0; }
else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
// Datastream for development purposes
//print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
// Unmark previous character and mark the current one...
// Handling switching between letters and numbers/punctuation
if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
if (abcHL != abcHLPrev) {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
}
// Print string at top of screen
if (event.b == 0) {
text = text + ABC.charAt(abcHL);
updateTopString();
// Autoswitching letter case
if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
}
// Update previous character to current one
abcHLPrev = abcHL;
typePrev = 'abc';
}
// 12345678901234567890
// Choose number or puctuation by draging on green rectangle
else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) {
// Translate x-position to character
if (event.x < NUMPADDING) { numHL = 0; }
else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
else { numHL = Math.floor((event.x-NUMPADDING)/6); }
// Datastream for development purposes
//print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
// Unmark previous character and mark the current one...
// Handling switching between letters and numbers/punctuation
if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
if (numHL != numHLPrev) {
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
}
// Print string at top of screen
if (event.b == 0) {
g.setColor(HLCOLOR);
// Backspace if releasing before list of numbers/punctuation
if (event.x < NUMPADDING) {
// show delete sign
showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
delSpaceLast = 1;
text = text.slice(0, -1);
updateTopString();
//print(text);
}
// Append space if releasing after list of numbers/punctuation
else if (event.x > (R.x+R.w)-NUMPADDING) {
//show space sign
showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
delSpaceLast = 1;
text = text + ' ';
updateTopString();
//print(text);
}
// Append selected number/punctuation
else {
text = text + NUMHIDDEN.charAt(numHL);
updateTopString();
// Autoswitching letter case
if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
}
}
// Update previous character to current one
numHLPrev = numHL;
typePrev = 'num';
}
// Make a space or backspace by swiping right or left on screen above green rectangle
else if (event.y > 20+4) {
if (event.b == 0) {
g.setColor(HLCOLOR);
if (event.x < (R.x+R.w)/2) {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
// show delete sign
showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
delSpaceLast = 1;
// Backspace and draw string upper right corner
text = text.slice(0, -1);
updateTopString();
if (text.length==0) changeCase(abcHL);
//print(text, 'undid');
}
else {
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
//show space sign
showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
delSpaceLast = 1;
// Append space and draw string upper right corner
text = text + NUMHIDDEN.charAt(0);
updateTopString();
//print(text, 'made space');
}
}
}
}
}); });
Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
}); });
}; };

View File

@ -1,6 +1,6 @@
{ "id": "dragboard", { "id": "dragboard",
"name": "Dragboard", "name": "Dragboard",
"version":"0.07", "version":"0.08",
"description": "A library for text input via swiping keyboard", "description": "A library for text input via swiping keyboard",
"icon": "app.png", "icon": "app.png",
"type":"textinput", "type":"textinput",

View File

@ -1 +1,3 @@
0.01: New App based on dragboard, but with a U shaped drag area 0.01: New App based on dragboard, but with a U shaped drag area
0.02: Catch and discard swipe events on fw2v19 and up (as well as some cutting
edge 2v18 ones), allowing compatability with the Back Swipe app.

View File

@ -104,45 +104,53 @@ exports.input = function(options) {
} }
} }
let dragHandlerUB = function(event) {
"ram";
// drag on middle bottom rectangle
if (event.x > MIDPADDING - 2 && event.x < (R.x2-MIDPADDING + 2) && event.y >= ( (R.y2) - 12 )) {
moveCharPos(MIDDLE, event.b == 0, (event.x-middleStart)/(middleWidth/MIDDLE.length));
}
// drag on left or right rectangle
else if (event.y > R.y && (event.x < MIDPADDING-2 || event.x > (R.x2-MIDPADDING + 2))) {
moveCharPos(event.x<MIDPADDING-2 ? LEFT : RIGHT, event.b == 0, (event.y-topStart)/((R.y2 - topStart)/vLength));
}
// drag on top rectangle for number or punctuation
else if ((event.y < ( (R.y2) - 12 )) && (event.y > ( (R.y2) - 52 ))) {
moveCharPos(NUM, event.b == 0, (event.x-NUMPADDING)/6);
}
// Make a space or backspace by tapping right or left on screen above green rectangle
else if (event.y > R.y && event.b == 0) {
if (event.x < (R.x2)/2) {
showChars('<-');
text = text.slice(0, -1);
} else {
//show space sign
showChars('->');
text += ' ';
}
prevChar = null;
updateTopString();
}
};
let catchSwipe = ()=>{
E.stopEventPropagation&&E.stopEventPropagation();
};
return new Promise((resolve,reject) => { return new Promise((resolve,reject) => {
// Interpret touch input // Interpret touch input
Bangle.setUI({ Bangle.setUI({
mode: 'custom', mode: 'custom',
back: ()=>{ back: ()=>{
Bangle.setUI(); Bangle.setUI();
Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up).
g.clearRect(Bangle.appRect); g.clearRect(Bangle.appRect);
resolve(text); resolve(text);
}, },
drag: function(event) { drag: dragHandlerDB
"ram";
// drag on middle bottom rectangle
if (event.x > MIDPADDING - 2 && event.x < (R.x2-MIDPADDING + 2) && event.y >= ( (R.y2) - 12 )) {
moveCharPos(MIDDLE, event.b == 0, (event.x-middleStart)/(middleWidth/MIDDLE.length));
}
// drag on left or right rectangle
else if (event.y > R.y && (event.x < MIDPADDING-2 || event.x > (R.x2-MIDPADDING + 2))) {
moveCharPos(event.x<MIDPADDING-2 ? LEFT : RIGHT, event.b == 0, (event.y-topStart)/((R.y2 - topStart)/vLength));
}
// drag on top rectangle for number or punctuation
else if ((event.y < ( (R.y2) - 12 )) && (event.y > ( (R.y2) - 52 ))) {
moveCharPos(NUM, event.b == 0, (event.x-NUMPADDING)/6);
}
// Make a space or backspace by tapping right or left on screen above green rectangle
else if (event.y > R.y && event.b == 0) {
if (event.x < (R.x2)/2) {
showChars('<-');
text = text.slice(0, -1);
} else {
//show space sign
showChars('->');
text += ' ';
}
prevChar = null;
updateTopString();
}
}
}); });
Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
R = Bangle.appRect; R = Bangle.appRect;
MIDPADDING = R.x + 35; MIDPADDING = R.x + 35;

View File

@ -1,6 +1,6 @@
{ "id": "draguboard", { "id": "draguboard",
"name": "DragUboard", "name": "DragUboard",
"version":"0.01", "version":"0.02",
"description": "A library for text input via swiping U-shaped keyboard.", "description": "A library for text input via swiping U-shaped keyboard.",
"icon": "app.png", "icon": "app.png",
"type":"textinput", "type":"textinput",

View File

@ -28,4 +28,4 @@ immediately follows the correct theme.
0.22: Bangle 2: Change to not automatically marking the first app on a page 0.22: Bangle 2: Change to not automatically marking the first app on a page
when moving pages. Add caching for faster startups. when moving pages. Add caching for faster startups.
0.23: Bangle 1: Fix issue with missing icons, added touch screen interactions 0.23: Bangle 1: Fix issue with missing icons, added touch screen interactions
0.24: Add buzz-on-interaction setting

View File

@ -9,7 +9,8 @@
showLaunchers: true, showLaunchers: true,
direct: false, direct: false,
swipeExit: false, swipeExit: false,
timeOut: "Off" timeOut: "Off",
interactionBuzz: false,
}, require('Storage').readJSON("dtlaunch.json", true) || {}); }, require('Storage').readJSON("dtlaunch.json", true) || {});
let s = require("Storage"); let s = require("Storage");
@ -89,6 +90,13 @@
g.flip(); g.flip();
}; };
let buzzShort = function() {
if (settings.interactionBuzz) Bangle.buzz(20);
};
let buzzLong = function() {
if (settings.interactionBuzz) Bangle.buzz(100);
};
Bangle.drawWidgets(); // To immediately update widget field to follow current theme - remove leftovers if previous app set custom theme. Bangle.drawWidgets(); // To immediately update widget field to follow current theme - remove leftovers if previous app set custom theme.
Bangle.loadWidgets(); Bangle.loadWidgets();
drawPage(0); drawPage(0);
@ -100,9 +108,11 @@
if(settings.swipeExit && dirLeftRight==1) Bangle.showClock(); if(settings.swipeExit && dirLeftRight==1) Bangle.showClock();
if (dirUpDown==-1||dirLeftRight==-1){ if (dirUpDown==-1||dirLeftRight==-1){
++page; if (page>maxPage) page=0; ++page; if (page>maxPage) page=0;
buzzShort();
drawPage(page); drawPage(page);
} else if (dirUpDown==1||(dirLeftRight==1 && !settings.swipeExit)){ } else if (dirUpDown==1||(dirLeftRight==1 && !settings.swipeExit)){
--page; if (page<0) page=maxPage; --page; if (page<0) page=maxPage;
buzzShort();
drawPage(page); drawPage(page);
} }
}; };
@ -123,8 +133,10 @@
drawIcon(page,i,true && !settings.direct); drawIcon(page,i,true && !settings.direct);
if (selected>=0 || settings.direct) { if (selected>=0 || settings.direct) {
if (selected!=i && !settings.direct){ if (selected!=i && !settings.direct){
buzzShort();
drawIcon(page,selected,false); drawIcon(page,selected,false);
} else { } else {
buzzLong();
load(apps[page*4+i].src); load(apps[page*4+i].src);
} }
} }
@ -134,6 +146,7 @@
} }
} }
if ((i==4 || (page*4+i)>Napps) && selected>=0) { if ((i==4 || (page*4+i)>Napps) && selected>=0) {
buzzShort();
drawIcon(page,selected,false); drawIcon(page,selected,false);
selected=-1; selected=-1;
} }

View File

@ -1,7 +1,7 @@
{ {
"id": "dtlaunch", "id": "dtlaunch",
"name": "Desktop Launcher", "name": "Desktop Launcher",
"version": "0.23", "version": "0.24",
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.", "description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}], "screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
"icon": "icon.png", "icon": "icon.png",

View File

@ -6,7 +6,8 @@
showLaunchers: true, showLaunchers: true,
direct: false, direct: false,
swipeExit: false, swipeExit: false,
timeOut: "Off" timeOut: "Off",
interactionBuzz: false,
}, require('Storage').readJSON(FILE, true) || {}); }, require('Storage').readJSON(FILE, true) || {});
function writeSettings() { function writeSettings() {
@ -55,6 +56,13 @@
settings.timeOut = timeOutChoices[v]; settings.timeOut = timeOutChoices[v];
writeSettings(); writeSettings();
} }
} },
/*LANG*/'Interaction buzz': {
value: settings.interactionBuzz,
onchange: v => {
settings.interactionBuzz = v;
writeSettings();
}
},
}); });
}); });

View File

@ -2,3 +2,5 @@
0.02: Allow redirection of loads to the launcher 0.02: Allow redirection of loads to the launcher
0.03: Allow hiding the fastloading info screen 0.03: Allow hiding the fastloading info screen
0.04: (WIP) Allow use of app history when going back (`load()` or `Bangle.load()` calls without specified app). 0.04: (WIP) Allow use of app history when going back (`load()` or `Bangle.load()` calls without specified app).
0.05: Check for changes in setting.js and force real reload if needed
0.06: Fix caching whether an app is fastloadable

View File

@ -12,6 +12,7 @@ This allows fast loading of all apps with two conditions:
* If Quick Launch is installed it can be excluded from app history * If Quick Launch is installed it can be excluded from app history
* Allows to redirect all loads usually loading the clock to the launcher instead * Allows to redirect all loads usually loading the clock to the launcher instead
* The "Fastloading..." screen can be switched off * The "Fastloading..." screen can be switched off
* Enable checking `setting.json` and force a complete load on changes
## App history ## App history

View File

@ -19,15 +19,24 @@ let loadingScreen = function(){
let cache = s.readJSON("fastload.cache") || {}; let cache = s.readJSON("fastload.cache") || {};
let checkApp = function(n){ const SYS_SETTINGS="setting.json";
let appFastloadPossible = function(n){
if(SETTINGS.detectSettingsChange && (!cache[SYS_SETTINGS] || s.hash(SYS_SETTINGS) != cache[SYS_SETTINGS])){
cache[SYS_SETTINGS] = s.hash(SYS_SETTINGS);
s.writeJSON("fastload.cache", cache);
return false;
}
// no widgets, no problem // no widgets, no problem
if (!global.WIDGETS) return true; if (!global.WIDGETS) return true;
let app = s.read(n); let hash = s.hash(n);
if (cache[n] && E.CRC32(app) == cache[n].crc) if (cache[n] && hash == cache[n].hash)
return cache[n].fast; return cache[n].fast;
let app = s.read(n);
cache[n] = {}; cache[n] = {};
cache[n].fast = app.includes("Bangle.loadWidgets"); cache[n].fast = app.includes("Bangle.loadWidgets");
cache[n].crc = E.CRC32(app); cache[n].hash = hash;
s.writeJSON("fastload.cache", cache); s.writeJSON("fastload.cache", cache);
return cache[n].fast; return cache[n].fast;
}; };
@ -39,7 +48,7 @@ let slowload = function(n){
}; };
let fastload = function(n){ let fastload = function(n){
if (!n || checkApp(n)){ if (!n || appFastloadPossible(n)){
// Bangle.load can call load, to prevent recursion this must be the system load // Bangle.load can call load, to prevent recursion this must be the system load
global.load = slowload; global.load = slowload;
Bangle.load(n); Bangle.load(n);

View File

@ -1,7 +1,7 @@
{ "id": "fastload", { "id": "fastload",
"name": "Fastload Utils", "name": "Fastload Utils",
"shortName" : "Fastload Utils", "shortName" : "Fastload Utils",
"version": "0.04", "version": "0.06",
"icon": "icon.png", "icon": "icon.png",
"description": "Enable experimental fastloading for more apps", "description": "Enable experimental fastloading for more apps",
"type":"bootloader", "type":"bootloader",

View File

@ -59,6 +59,13 @@
} }
}; };
mainmenu['Detect settings changes'] = {
value: !!settings.detectSettingsChange,
onchange: v => {
writeSettings("detectSettingsChange",v);
}
};
return mainmenu; return mainmenu;
} }

View File

@ -1,4 +1,5 @@
1.0: Local cards data 1.00: Local cards data
1.1: Download cards data from Trello public board 1.10: Download cards data from Trello public board
1.2: Configuration instructions added and card layout optimized 1.20: Configuration instructions added and card layout optimized
1.3: Font size can be changed in Settings 1.30: Font size can be changed in Settings
1.31: Fix for fast-loading support

View File

@ -10,7 +10,7 @@ Configuration:
4. Add ".json" to the end of the Trello board URL and refresh page 4. Add ".json" to the end of the Trello board URL and refresh page
5. Find your list ID 5. Find your list ID
6. Save list ID to the "flashcards.settings.json" file on your watch, e.g.: 6. Save list ID to the "flashcards.settings.json" file on your watch, e.g.:
{"listId":"65942f7b27z68000996ddc00","fontSize":1,"cardWidth":9,"swipeGesture":0} {"listId":"65942f7b27z68000996ddc00","fontSize":1,"cardWidth":9,"swipeGesture":1}
7. Connect phone with Gadgetbridge to the watch 7. Connect phone with Gadgetbridge to the watch
8. Enable "Allow Internet Access" in Gadgetbridge 8. Enable "Allow Internet Access" in Gadgetbridge
9. On the watch go to Settings -> Apps -> Flash Cards -> Get from Trello 9. On the watch go to Settings -> Apps -> Flash Cards -> Get from Trello

View File

@ -2,186 +2,185 @@
* Copyright 2023 Crisp Advice * Copyright 2023 Crisp Advice
* We believe in Finnish * We believe in Finnish
*/ */
{
// Modules
let Layout = require("Layout");
let locale = require("locale");
let storage = require("Storage");
// Modules // Global variables
var Layout = require("Layout"); const SWAP_SIDE_BUZZ_MILLISECONDS = 50;
var locale = require("locale"); const CARD_DATA_FILE = "flashcards.data.json";
var storage = require("Storage"); const CARD_SETTINGS_FILE = "flashcards.settings.json";
const CARD_EMPTY = "no cards found";
let cards = [];
let cardIndex = 0;
let backSide = false;
let drawTimeout;
let fontSizes = ["15%","20%","25%"];
let lastDragX = 0;
let lastDragY = 0;
// Global variables let settings = Object.assign({
let SWAP_SIDE_BUZZ_MILLISECONDS = 50; listId: "",
let CARD_DATA_FILE = "flashcards.data.json"; fontSize: 1,
let CARD_SETTINGS_FILE = "flashcards.settings.json"; cardWidth: 9,
let CARD_EMPTY = "no cards found"; swipeGesture: 1
let cards = []; }, storage.readJSON(CARD_SETTINGS_FILE, true) || {});
let cardIndex = 0;
let backSide = false;
let drawTimeout;
let fontSizes = ["15%","20%","25%"];
let lastDragX = 0;
let lastDragY = 0;
let settings = Object.assign({ // Cards data
listId: "", let wordWrap = function (textStr, maxLength) {
fontSize: 1, if (maxLength == undefined) {
cardWidth: 9, maxLength = settings.cardWidth;
swipeGesture: 0 }
}, storage.readJSON(CARD_SETTINGS_FILE, true) || {}); let res = '';
let str = textStr.trim();
// Cards data while (str.length > maxLength) {
function wordWrap(textStr, maxLength) { let found = false;
if (maxLength == undefined) { // Inserts new line at first whitespace of the line
maxLength = settings.cardWidth; for (i = maxLength - 1; i > 0; i--) {
} if (str.charAt(i)==' ') {
let res = ''; res = res + [str.slice(0, i), "\n"].join('');
let str = textStr.trim(); str = str.slice(i + 1);
while (str.length > maxLength) { found = true;
let found = false; break;
// Inserts new line at first whitespace of the line }
for (i = maxLength - 1; i > 0; i--) { }
if (str.charAt(i)==' ') { // Inserts new line at MAX_LENGTH position, the word is too long to wrap
res = res + [str.slice(0, i), "\n"].join(''); if (!found) {
str = str.slice(i + 1); res += [str.slice(0, maxLength), "\n"].join('');
found = true; str = str.slice(maxLength);
break;
} }
} }
// Inserts new line at MAX_LENGTH position, the word is too long to wrap return res + str;
if (!found) {
res += [str.slice(0, maxLength), "\n"].join('');
str = str.slice(maxLength);
}
} }
return res + str;
}
function loadLocalCards() { let loadLocalCards = function() {
var cardsJSON = ""; var cardsJSON = "";
if (storage.read(CARD_DATA_FILE)) if (storage.read(CARD_DATA_FILE))
{
cardsJSON = storage.readJSON(CARD_DATA_FILE, 1) || {};
}
refreshCards(cardsJSON,false);
}
let refreshCards = function(cardsJSON,showMsg)
{ {
cardsJSON = storage.readJSON(CARD_DATA_FILE, 1) || {}; cardIndex = 0;
} backSide = false;
refreshCards(cardsJSON,false); cards = [];
}
function refreshCards(cardsJSON,showMsg) if (cardsJSON && cardsJSON.length) {
{ cardsJSON.forEach(card => {
cardIndex = 0; cards.push([ wordWrap(card.name), wordWrap(card.desc) ]);
backSide = false; });
cards = []; }
if (cardsJSON && cardsJSON.length) { if (!cards.length) {
cardsJSON.forEach(card => { cards.push([ wordWrap(CARD_EMPTY), wordWrap(CARD_EMPTY) ]);
cards.push([ wordWrap(card.name), wordWrap(card.desc) ]); drawMessage("e: cards not found");
}); } else if (showMsg) {
} drawMessage("i: cards refreshed");
if (!cards.length) {
cards.push([ wordWrap(CARD_EMPTY), wordWrap(CARD_EMPTY) ]);
drawMessage("e: cards not found");
} else if (showMsg) {
drawMessage("i: cards refreshed");
}
}
// Drawing a card
let queueDraw = function() {
let timeout = 60000;
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, timeout - (Date.now() % timeout));
};
let cardLayout = new Layout( {
type:"v", c: [
{type:"txt", font:"6x8:3", label:"", id:"widgets", fillx:1 },
{type:"txt", font:fontSizes[settings.fontSize], label:"ABCDEFGHIJ KLMNOPQRST UVWXYZÅÖÄ", filly:1, fillx:1, id:"card" },
{type:"txt", font:"6x8:2", label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg }
]
}, {lazy:true});
function drawCard() {
cardLayout.card.label = cards[cardIndex][backSide ? 1 : 0];
cardLayout.clock.label = locale.time(new Date(),1);
cardLayout.render();
}
function drawMessage(msg) {
cardLayout.card.label = wordWrap(msg);
cardLayout.render();
console.log(msg);
}
function draw() {
drawCard();
Bangle.drawWidgets();
queueDraw();
}
function swipeCard(forward)
{
if(forward) {
cardIndex = (cardIndex + 1) % cards.length;
}
else if(--cardIndex < 0) {
cardIndex = cards.length - 1;
}
drawCard();
}
// Handle a touch: swap card side
function handleTouch(zone, event) {
backSide = !backSide;
drawCard();
Bangle.buzz(SWAP_SIDE_BUZZ_MILLISECONDS);
}
// Handle a stroke event: cycle cards
function handleStroke(event) {
let first_x = event.xy[0];
let last_x = event.xy[event.xy.length - 2];
swipeCard((last_x - first_x) > 0);
}
// Handle a drag event: cycle cards
function handleDrag(event) {
let isFingerReleased = (event.b === 0);
if(isFingerReleased) {
let isHorizontalDrag = (Math.abs(lastDragX) >= Math.abs(lastDragY)) &&
(lastDragX !== 0);
if(isHorizontalDrag) {
swipeCard(lastDragX > 0);
} }
} }
else {
lastDragX = event.dx; // Drawing a card
lastDragY = event.dy; let queueDraw = function() {
let timeout = 60000;
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, timeout - (Date.now() % timeout));
};
let cardLayout = new Layout( {
type:"v", c: [
{type:"txt", font:"6x8:3", label:"", id:"widgets", fillx:1 },
{type:"txt", font:fontSizes[settings.fontSize], label:"ABCDEFGHIJ KLMNOPQRST UVWXYZÅÖÄ", filly:1, fillx:1, id:"card" },
{type:"txt", font:"6x8:2", label:"00:00", id:"clock", fillx:1, bgCol:g.theme.fg, col:g.theme.bg }
]
}, {lazy:true});
let drawCard = function() {
cardLayout.card.label = cards[cardIndex][backSide ? 1 : 0];
cardLayout.clock.label = locale.time(new Date(),1);
cardLayout.render();
} }
let drawMessage = function(msg) {
cardLayout.card.label = wordWrap(msg);
cardLayout.render();
console.log(msg);
}
let draw = function() {
drawCard();
Bangle.drawWidgets();
queueDraw();
}
let swipeCard = function(forward)
{
if(forward) {
cardIndex = (cardIndex + 1) % cards.length;
}
else if(--cardIndex < 0) {
cardIndex = cards.length - 1;
}
drawCard();
}
// Handle a touch: swap card side
let handleTouch = function(zone, event) {
backSide = !backSide;
drawCard();
Bangle.buzz(SWAP_SIDE_BUZZ_MILLISECONDS);
}
// Handle a stroke event: cycle cards
let handleStroke = function(event) {
let first_x = event.xy[0];
let last_x = event.xy[event.xy.length - 2];
swipeCard((last_x - first_x) > 0);
}
// Handle a drag event: cycle cards
let handleDrag = function(event) {
let isFingerReleased = (event.b === 0);
if(isFingerReleased) {
let isHorizontalDrag = (Math.abs(lastDragX) >= Math.abs(lastDragY)) &&
(lastDragX !== 0);
if(isHorizontalDrag) {
swipeCard(lastDragX > 0);
}
}
else {
lastDragX = event.dx;
lastDragY = event.dy;
}
}
// Ensure pressing the button goes to the launcher (by making this seem like a clock?)
Bangle.setUI({mode:"clock", remove:function() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
Bangle.removeListener("touch", handleTouch);
if (settings.swipeGesture) { Bangle.removeListener("drag", handleDrag);} else { Bangle.removeListener("stroke", handleStroke); }
}});
// initialize
cardLayout.update();
Bangle.loadWidgets();
loadLocalCards();
Bangle.on("touch", handleTouch);
if (settings.swipeGesture) { Bangle.on("drag", handleDrag); } else { Bangle.on("stroke", handleStroke); }
// On start: display the first card
g.clear();
draw();
} }
// Ensure pressing the button goes to the launcher (by making this seem like a clock?)
Bangle.setUI({mode:"clock"/*, remove:function() {
// Code to enable fast load. NOTE: this doesn't work on this app because all
// functions and vars are declared global: https://www.espruino.com/Bangle.js+Fast+Load
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
Bangle.removeListener("touch", handleTouch);
if (settings.swipeGesture) { Bangle.removeListener("drag", handleDrag);} else { Bangle.removeListener("stroke", handleStroke); }
}*/});
// initialize
cardLayout.update();
Bangle.loadWidgets();
loadLocalCards();
Bangle.on("touch", handleTouch);
if (settings.swipeGesture) { Bangle.on("drag", handleDrag); } else { Bangle.on("stroke", handleStroke); }
// On start: display the first card
g.clear();
draw();

View File

@ -1 +1 @@
{"listId":"","fontSize":1,"cardWidth":9,"swipeGesture":0} {"listId":"","fontSize":1,"cardWidth":9,"swipeGesture":1}

View File

@ -2,7 +2,7 @@
"id": "flashcards", "id": "flashcards",
"name": "Flash Cards", "name": "Flash Cards",
"shortName": "Flash Cards", "shortName": "Flash Cards",
"version": "1.3", "version": "1.31",
"description": "Flash cards based on public Trello board", "description": "Flash cards based on public Trello board",
"readme":"README.md", "readme":"README.md",
"screenshots" : [ { "url":"screenshot.png" }], "screenshots" : [ { "url":"screenshot.png" }],

View File

@ -10,7 +10,7 @@
listId: "", listId: "",
fontSize: 1, fontSize: 1,
cardWidth: 9, cardWidth: 9,
swipeGesture: 0 swipeGesture: 1
}, storage.readJSON(settingsFile, true) || {}); }, storage.readJSON(settingsFile, true) || {});
function writeSettings() { function writeSettings() {
@ -24,7 +24,7 @@
"< Back" : () => back(), "< Back" : () => back(),
/*LANG*/"Get from Trello": () => { /*LANG*/"Get from Trello": () => {
if (!storage.read(settingsFile)) { writeSettings();} if (!storage.read(settingsFile)) { writeSettings();}
E.showPrompt("Download cards?").then((v) => { E.showPrompt(/*LANG*/"Download cards?").then((v) => {
let delay = 500; let delay = 500;
if (v) { if (v) {
if (Bangle.http) if (Bangle.http)

View File

@ -87,3 +87,14 @@
* Reduce framerate if locked * Reduce framerate if locked
* Stroke to move around in the map * Stroke to move around in the map
* Fix for missing paths in display * Fix for missing paths in display
0.20:
* Large display for instant speed
* Bugfix for negative coordinates
* Disable menu while the map is not loaded
* Turn screen off while idling to save battery (with setting)
* New setting : disable buzz on turns
* New setting : turn bluetooth off to save battery
* New setting : power screen off between points to save battery
* Color change for lost direction (now purple)
* Adaptive screen powersaving

View File

@ -18,8 +18,8 @@ It provides the following features :
- display the path with current position from gps - display the path with current position from gps
- display a local map around you, downloaded from openstreetmap - display a local map around you, downloaded from openstreetmap
- detects and buzzes if you leave the path - detects and buzzes if you leave the path
- buzzes before sharp turns - (optional) buzzes before sharp turns
- buzzes before waypoints - (optional) buzzes before waypoints
(for example when you need to turn in https://mapstogpx.com/) (for example when you need to turn in https://mapstogpx.com/)
- display instant / average speed - display instant / average speed
- display distance to next point - display distance to next point
@ -51,8 +51,8 @@ Your path will be displayed in svg.
### Starting Gipy ### Starting Gipy
At start you will have a menu for selecting your trace (if more than one). At start you will have a menu for selecting your trace (if more than one).
Choose the one you want and you will reach the splash screen where you'll wait for the gps signal. Choose the one you want and you will reach the splash screen where you'll wait for the map.
Once you have a signal you will reach the main screen: Once the map is loaded you will reach the main screen:
![Screenshot](legend.png) ![Screenshot](legend.png)
@ -83,7 +83,7 @@ On your screen you can see:
### Lost ### Lost
If you stray away from path we will rescale the display to continue displaying nearby segments and If you stray away from path we will rescale the display to continue displaying nearby segments and
display the direction to follow as a black segment. display the direction to follow as a purple segment.
Note that while lost, the app will slow down a lot since it will start scanning all possible points to figure out where you Note that while lost, the app will slow down a lot since it will start scanning all possible points to figure out where you
are. On path it just needed to scan a few points ahead and behind. are. On path it just needed to scan a few points ahead and behind.
@ -93,13 +93,18 @@ The distance to next point displayed corresponds to the length of the black segm
### Menu ### Menu
If you click the button you'll reach a menu where you can currently zoom out to see more of the map If you click the button you'll reach a menu where you can currently zoom out to see more of the map
(with a slower refresh rate) and reverse the path direction. (with a slower refresh rate), reverse the path direction and disable power saving (keeping backlight on).
### Settings ### Settings
Few settings for now (feel free to suggest me more) : Few settings for now (feel free to suggest me more) :
- lost distance : at which distance from path are you considered to be lost ? - lost distance : at which distance from path are you considered to be lost ?
- buzz on turns : should the watch buzz when reaching a waypoint ?
- disable bluetooth : turn bluetooth off completely to try to save some power.
- brightness : how bright should screen be ? (by default 0.5, again saving power)
- power lcd off (disabled by default): turn lcd off when inactive to save power. the watch will wake up when reaching points,
when you touch the screen and when speed is below 13km/h.
### Caveats ### Caveats
@ -107,6 +112,7 @@ It is good to use but you should know :
- the gps might take a long time to start initially (see the assisted gps update app). - the gps might take a long time to start initially (see the assisted gps update app).
- gps signal is noisy : there is therefore a small delay for instant speed. sometimes you may jump somewhere else. - gps signal is noisy : there is therefore a small delay for instant speed. sometimes you may jump somewhere else.
- if you adventure in gorges the gps signal will become garbage.
- your gpx trace has been decimated and approximated : the **REAL PATH** might be **A FEW METERS AWAY** - your gpx trace has been decimated and approximated : the **REAL PATH** might be **A FEW METERS AWAY**
- sometimes the watch will tell you that you are lost but you are in fact on the path. It usually figures again - sometimes the watch will tell you that you are lost but you are in fact on the path. It usually figures again
the real gps position after a few minutes. It usually happens when the signal is acquired very fast. the real gps position after a few minutes. It usually happens when the signal is acquired very fast.
@ -119,4 +125,8 @@ I had to go back uphill by quite a distance.
Feel free to give me feedback : is it useful for you ? what other features would you like ? Feel free to give me feedback : is it useful for you ? what other features would you like ?
If you want to raise issues the main repository is [https://github.com/wagnerf42/BangleApps](here) and
the rust code doing the actual map computations is located [https://github.com/wagnerf42/gps](here).
You can try the cutting edge version at [https://wagnerf42.github.io/BangleApps/](https://wagnerf42.github.io/BangleApps/)
frederic.wagner@imag.fr frederic.wagner@imag.fr

View File

@ -1,19 +1,51 @@
*** thoughts on lcd power ***
so, i tried experimenting with turning the lcd off in order to save power.
the good news: this saves a lot. i did a 3h ride which usually depletes the battery and I still had
around two more hours to go.
now the bad news:
- i had to de-activate the twist detection : you cannot raise your watch to the eyes to turn it on.
that's because with twist detection on all road bumps turn the watch on constantly.
- i tried manual detection like :
Bangle.on('accel', function(xyz) {
if (xyz.diff > 0.4 && xyz.mag > 1 && xyz.z < -1.4) {
Bangle.setLCDPower(true);
Bangle.setLocked(false);
}
});
this works nicely when you sit on a chair with a simulated gps signal but does not work so nicely when on the bike.
sometimes it is ok, sometimes you can try 10 times with no success.
- instead i use screen touch to turn it on. that's a bother since you need two hands but well it could be worth it.
the problem is in the delay: between 1 and 5 seconds before the screen comes back on.
my conclusion is that:
* we should not turn screen off unless we agree to have an unresponsive ui
* we should maybe autowake near segments ends and when lost
* we should play with backlight instead
**************************
+ when you walk the direction still has a tendency to shift
+ put back foot only ways
+ try fiddling with jit
+ put back street names
+ put back shortest paths but with points cache this time and jit
+ how to display paths from shortest path ?
misc:
+ use Bangle.project(latlong) + use Bangle.project(latlong)
* additional features
- config screen
- are we on foot (and should use compass)
- we need to buzz 200m before sharp turns (or even better, 30seconds)
(and look at more than next point)
- display distance to next water/toilet ?
- display scale (100m)
- compress path ?
* misc
- code is becoming messy

File diff suppressed because one or more lines are too long

View File

@ -2,7 +2,7 @@
"id": "gipy", "id": "gipy",
"name": "Gipy", "name": "Gipy",
"shortName": "Gipy", "shortName": "Gipy",
"version": "0.19", "version": "0.20",
"description": "Follow gpx files using the gps. Don't get lost in your bike trips and hikes.", "description": "Follow gpx files using the gps. Don't get lost in your bike trips and hikes.",
"allow_emulator":false, "allow_emulator":false,
"icon": "gipy.png", "icon": "gipy.png",

View File

@ -67,11 +67,11 @@ export interface InitOutput {
readonly __wbindgen_malloc: (a: number) => number; readonly __wbindgen_malloc: (a: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number) => number; readonly __wbindgen_realloc: (a: number, b: number, c: number) => number;
readonly __wbindgen_export_2: WebAssembly.Table; readonly __wbindgen_export_2: WebAssembly.Table;
readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a: (a: number, b: number, c: number) => void; readonly _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1: (a: number, b: number, c: number) => void;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number; readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_free: (a: number, b: number) => void; readonly __wbindgen_free: (a: number, b: number) => void;
readonly __wbindgen_exn_store: (a: number) => void; readonly __wbindgen_exn_store: (a: number) => void;
readonly wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b: (a: number, b: number, c: number, d: number) => void; readonly wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137: (a: number, b: number, c: number, d: number) => void;
} }
export type SyncInitInput = BufferSource | WebAssembly.Module; export type SyncInitInput = BufferSource | WebAssembly.Module;

View File

@ -205,7 +205,7 @@ function makeMutClosure(arg0, arg1, dtor, f) {
return real; return real;
} }
function __wbg_adapter_24(arg0, arg1, arg2) { function __wbg_adapter_24(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a(arg0, arg1, addHeapObject(arg2)); wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1(arg0, arg1, addHeapObject(arg2));
} }
function _assertClass(instance, klass) { function _assertClass(instance, klass) {
@ -369,7 +369,7 @@ function handleError(f, args) {
} }
} }
function __wbg_adapter_84(arg0, arg1, arg2, arg3) { function __wbg_adapter_84(arg0, arg1, arg2, arg3) {
wasm.wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); wasm.wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3));
} }
/** /**
@ -460,6 +460,21 @@ function getImports() {
const ret = getObject(arg0).fetch(getObject(arg1)); const ret = getObject(arg0).fetch(getObject(arg1));
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) {
const ret = getObject(arg0).signal;
return addHeapObject(ret);
};
imports.wbg.__wbg_new_6396e586b56e1dff = function() { return handleError(function () {
const ret = new AbortController();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) {
getObject(arg0).abort();
};
imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_new_2d0053ee81e4dd2a = function() { return handleError(function () { imports.wbg.__wbg_new_2d0053ee81e4dd2a = function() { return handleError(function () {
const ret = new Headers(); const ret = new Headers();
return addHeapObject(ret); return addHeapObject(ret);
@ -496,21 +511,6 @@ function getImports() {
const ret = getObject(arg0).text(); const ret = getObject(arg0).text();
return addHeapObject(ret); return addHeapObject(ret);
}, arguments) }; }, arguments) };
imports.wbg.__wbg_signal_31753ac644b25fbb = function(arg0) {
const ret = getObject(arg0).signal;
return addHeapObject(ret);
};
imports.wbg.__wbg_new_6396e586b56e1dff = function() { return handleError(function () {
const ret = new AbortController();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_abort_064ae59cda5cd244 = function(arg0) {
getObject(arg0).abort();
};
imports.wbg.__wbg_newwithstrandinit_05d7180788420c40 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_new_abda76e883ba8a5f = function() { imports.wbg.__wbg_new_abda76e883ba8a5f = function() {
const ret = new Error(); const ret = new Error();
return addHeapObject(ret); return addHeapObject(ret);
@ -675,8 +675,8 @@ function getImports() {
const ret = wasm.memory; const ret = wasm.memory;
return addHeapObject(ret); return addHeapObject(ret);
}; };
imports.wbg.__wbindgen_closure_wrapper2298 = function(arg0, arg1, arg2) { imports.wbg.__wbindgen_closure_wrapper2245 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 260, __wbg_adapter_24); const ret = makeMutClosure(arg0, arg1, 267, __wbg_adapter_24);
return addHeapObject(ret); return addHeapObject(ret);
}; };

Binary file not shown.

View File

@ -12,8 +12,8 @@ export function gps_from_area(a: number, b: number, c: number, d: number): numbe
export function __wbindgen_malloc(a: number): number; export function __wbindgen_malloc(a: number): number;
export function __wbindgen_realloc(a: number, b: number, c: number): number; export function __wbindgen_realloc(a: number, b: number, c: number): number;
export const __wbindgen_export_2: WebAssembly.Table; export const __wbindgen_export_2: WebAssembly.Table;
export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hab13c10d53cd1c5a(a: number, b: number, c: number): void; export function _dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__heb2f4d39a212d7d1(a: number, b: number, c: number): void;
export function __wbindgen_add_to_stack_pointer(a: number): number; export function __wbindgen_add_to_stack_pointer(a: number): number;
export function __wbindgen_free(a: number, b: number): void; export function __wbindgen_free(a: number, b: number): void;
export function __wbindgen_exn_store(a: number): void; export function __wbindgen_exn_store(a: number): void;
export function wasm_bindgen__convert__closures__invoke2_mut__h26ce002f44a5439b(a: number, b: number, c: number, d: number): void; export function wasm_bindgen__convert__closures__invoke2_mut__h362f82c7669db137(a: number, b: number, c: number, d: number): void;

View File

@ -1,29 +1,66 @@
(function (back) { (function(back) {
var FILE = "gipy.json"; var FILE = "gipy.json";
// Load settings // Load settings
var settings = Object.assign( var settings = Object.assign({
{ lost_distance: 50,
lost_distance: 50, buzz_on_turns: false,
}, disable_bluetooth: true,
require("Storage").readJSON(FILE, true) || {} brightness: 0.5,
); power_lcd_off: false,
},
require("Storage").readJSON(FILE, true) || {}
);
function writeSettings() { function writeSettings() {
require("Storage").writeJSON(FILE, settings); require("Storage").writeJSON(FILE, settings);
} }
// Show the menu // Show the menu
E.showMenu({ E.showMenu({
"": { title: "Gipy" }, "": {
"< Back": () => back(), title: "Gipy"
"lost distance": { },
value: 50 | settings.lost_distance, // 0| converts undefined to 0 "< Back": () => back(),
min: 10, /*LANG*/"buzz on turns": {
max: 500, value: settings.buzz_on_turns == true,
onchange: (v) => { onchange: (v) => {
settings.max_speed = v; settings.buzz_on_turns = v;
writeSettings(); writeSettings();
}, }
}, },
}); /*LANG*/"disable bluetooth": {
value: settings.disable_bluetooth == true,
onchange: (v) => {
settings.disable_bluetooth = v;
writeSettings();
}
},
"lost distance": {
value: settings.lost_distance,
min: 10,
max: 500,
onchange: (v) => {
settings.lost_distance = v;
writeSettings();
},
},
"brightness": {
value: settings.brightness,
min: 0,
max: 1,
step: 0.1,
onchange: (v) => {
settings.brightness = v;
writeSettings();
},
},
/*LANG*/"power lcd off": {
value: settings.power_lcd_off == true,
onchange: (v) => {
settings.power_lcd_off = v;
writeSettings();
}
},
});
}); });

View File

@ -15,4 +15,7 @@
Save state if route or waypoint has been chosen Save state if route or waypoint has been chosen
0.09: Workaround a minifier issue allowing to install gpstrek with minification enabled 0.09: Workaround a minifier issue allowing to install gpstrek with minification enabled
0.10: Adds map view of loaded route 0.10: Adds map view of loaded route
Automatically search for new waypoint if moving away from current target Automatically search for new waypoint if moving away from current target
0.11: Adds configuration
Draws direction arrows on route
Turn of compass when GPS fix is available

View File

@ -7,25 +7,12 @@ const MODE_SLICES = 2;
const STORAGE = require("Storage"); const STORAGE = require("Storage");
const BAT_FULL = require("Storage").readJSON("setting.json").batFullVoltage || 0.3144; const BAT_FULL = require("Storage").readJSON("setting.json").batFullVoltage || 0.3144;
const SETTINGS = {
mapCompass: true,
mapScale:0.2, //initial value const SETTINGS = Object.assign(
mapRefresh:1000, //minimum time in ms between refreshs of the map require('Storage').readJSON("gpstrek.default.json", true) || {},
mapChunkSize: 5, //render this many waypoints at a time require('Storage').readJSON("gpstrek.json", true) || {}
overviewScroll: 30, //scroll this amount on swipe in pixels );
overviewScale: 0.02, //initial value
refresh:500, //general refresh interval in ms
refreshLocked:3000, //general refresh interval when Bangle is locked
cacheMinFreeMem:2000,
cacheMaxEntries:0,
minCourseChange: 5, //course change needed in degrees before redrawing the map
minPosChange: 5, //position change needed in pixels before redrawing the map
waypointChangeDist: 50, //distance in m to next waypoint before advancing automatically
queueWaitingTime: 5, // waiting time during processing of task queue items when running with timeouts
autosearch: true,
maxDistForAutosearch: 300,
autosearchLimit: 3
};
let init = function(){ let init = function(){
global.screen = 1; global.screen = 1;
@ -38,7 +25,6 @@ let init = function(){
Bangle.loadWidgets(); Bangle.loadWidgets();
WIDGETS.gpstrek.start(false); WIDGETS.gpstrek.start(false);
if (!WIDGETS.gpstrek.getState().numberOfSlices) WIDGETS.gpstrek.getState().numberOfSlices = 2;
if (!WIDGETS.gpstrek.getState().mode) WIDGETS.gpstrek.getState().mode = MODE_MENU; if (!WIDGETS.gpstrek.getState().mode) WIDGETS.gpstrek.getState().mode = MODE_MENU;
}; };
@ -184,11 +170,6 @@ let getDoubleLineSlice = function(title1,title2,provider1,provider2){
}; };
}; };
const dot = Graphics.createImage(`
XX
XX
`);
const arrow = Graphics.createImage(` const arrow = Graphics.createImage(`
X X
XXX XXX
@ -198,6 +179,14 @@ const arrow = Graphics.createImage(`
XXX XXX XXX XXX
`); `);
const thinarrow = Graphics.createImage(`
X
XXX
XX XX
XX XX
XX XX
`);
const cross = Graphics.createImage(` const cross = Graphics.createImage(`
XX XX XX XX
XX XX XX XX
@ -459,7 +448,7 @@ let getMapSlice = function(){
if (!isMapOverview){ if (!isMapOverview){
drawCurrentPos(); drawCurrentPos();
} }
if (!isMapOverview && renderInTimeouts){ if (SETTINGS.mapCompass && !isMapOverview && renderInTimeouts){
drawMapCompass(); drawMapCompass();
} }
if (renderInTimeouts) drawInterface(); if (renderInTimeouts) drawInterface();
@ -472,7 +461,8 @@ let getMapSlice = function(){
i:startingIndex, i:startingIndex,
poly:[], poly:[],
maxWaypoints: maxWaypoints, maxWaypoints: maxWaypoints,
breakLoop: false breakLoop: false,
dist: 0
}; };
let drawChunk = function(data){ let drawChunk = function(data){
@ -483,6 +473,7 @@ let getMapSlice = function(){
let last; let last;
let toDraw; let toDraw;
let named = []; let named = [];
let dir = [];
for (let j = 0; j < SETTINGS.mapChunkSize; j++){ for (let j = 0; j < SETTINGS.mapChunkSize; j++){
data.i = data.i + (reverse?-1:1); data.i = data.i + (reverse?-1:1);
let p = get(route, data.i); let p = get(route, data.i);
@ -497,7 +488,17 @@ let getMapSlice = function(){
break; break;
} }
toDraw = Bangle.project(p); toDraw = Bangle.project(p);
if (p.name) named.push({i:data.poly.length,n:p.name});
if (SETTINGS.mapDirection){
let lastWp = get(route, data.i - (reverse?-1:1));
if (lastWp) data.dist+=distance(lastWp,p);
if (!isMapOverview && data.dist > 20/mapScale){
dir.push({i:data.poly.length,b:require("graphics_utils").degreesToRadians(bearing(lastWp,p)-(reverse?0:180))});
data.dist=0;
}
}
if (p.name)
named.push({i:data.poly.length,n:p.name});
data.poly.push(startingPoint.x-toDraw.x); data.poly.push(startingPoint.x-toDraw.x);
data.poly.push((startingPoint.y-toDraw.y)*-1); data.poly.push((startingPoint.y-toDraw.y)*-1);
} }
@ -518,7 +519,11 @@ let getMapSlice = function(){
} }
graphics.drawString(c.n, data.poly[c.i] + 10, data.poly[c.i+1]); graphics.drawString(c.n, data.poly[c.i] + 10, data.poly[c.i+1]);
} }
for (let c of dir){
graphics.drawImage(thinarrow, data.poly[c.i], data.poly[c.i+1], {rotate: c.b});
}
if (finish) if (finish)
graphics.drawImage(finishIcon, data.poly[data.poly.length - 2] -5, data.poly[data.poly.length - 1] - 4); graphics.drawImage(finishIcon, data.poly[data.poly.length - 2] -5, data.poly[data.poly.length - 1] - 4);
else if (last) { else if (last) {
@ -1254,11 +1259,6 @@ let showMenu = function(){
"Background" : showBackgroundMenu, "Background" : showBackgroundMenu,
"Calibration": showCalibrationMenu, "Calibration": showCalibrationMenu,
"Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS.gpstrek.resetState(); removeMenu();} else {E.showMenu(mainmenu);}}).catch(()=>{E.showMenu(mainmenu);});}, "Reset" : ()=>{ E.showPrompt("Do Reset?").then((v)=>{ if (v) {WIDGETS.gpstrek.resetState(); removeMenu();} else {E.showMenu(mainmenu);}}).catch(()=>{E.showMenu(mainmenu);});},
"Info rows" : {
value : WIDGETS.gpstrek.getState().numberOfSlices,
min:1,max:6,step:1,
onchange : v => { WIDGETS.gpstrek.getState().numberOfSlices = v; }
},
}; };
E.showMenu(mainmenu); E.showMenu(mainmenu);
@ -1374,7 +1374,7 @@ const finishData = {
}; };
let getSliceHeight = function(number){ let getSliceHeight = function(number){
return Math.floor(Bangle.appRect.h/WIDGETS.gpstrek.getState().numberOfSlices); return Math.floor(Bangle.appRect.h/SETTINGS.numberOfSlices);
}; };
let compassSlice = getCompassSlice(); let compassSlice = getCompassSlice();
@ -1455,7 +1455,6 @@ let updateRouting = function() {
lastSearch = Date.now(); lastSearch = Date.now();
autosearchCounter++; autosearchCounter++;
} }
let counter = 0;
while (hasNext(s.route) && distance(s.currentPos,get(s.route)) < SETTINGS.waypointChangeDist) { while (hasNext(s.route) && distance(s.currentPos,get(s.route)) < SETTINGS.waypointChangeDist) {
next(s.route); next(s.route);
minimumDistance = Number.MAX_VALUE; minimumDistance = Number.MAX_VALUE;
@ -1479,7 +1478,7 @@ let updateSlices = function(){
slices.push(healthSlice); slices.push(healthSlice);
slices.push(systemSlice); slices.push(systemSlice);
slices.push(system2Slice); slices.push(system2Slice);
maxSlicePages = Math.ceil(slices.length/s.numberOfSlices); maxSlicePages = Math.ceil(slices.length/SETTINGS.numberOfSlices);
}; };
let page_slices = 0; let page_slices = 0;
@ -1515,9 +1514,9 @@ let drawSlices = function(){
if (force){ if (force){
clear(); clear();
} }
let firstSlice = page_slices*s.numberOfSlices; let firstSlice = page_slices*SETTINGS.numberOfSlices;
let sliceHeight = getSliceHeight(); let sliceHeight = getSliceHeight();
let slicesToDraw = slices.slice(firstSlice,firstSlice + s.numberOfSlices); let slicesToDraw = slices.slice(firstSlice,firstSlice + SETTINGS.numberOfSlices);
for (let slice of slicesToDraw) { for (let slice of slicesToDraw) {
g.reset(); g.reset();
if (!slice.refresh || slice.refresh() || force) if (!slice.refresh || slice.refresh() || force)

21
apps/gpstrek/default.json Normal file
View File

@ -0,0 +1,21 @@
{
"mapCompass": true,
"mapScale":0.5,
"mapRefresh":1000,
"mapChunkSize": 15,
"mapDirection": true,
"overviewScroll": 30,
"overviewScale": 0.02,
"refresh":500,
"refreshLocked":3000,
"cacheMinFreeMem":2000,
"cacheMaxEntries":0,
"minCourseChange": 5,
"minPosChange": 5,
"waypointChangeDist": 50,
"queueWaitingTime": 5,
"autosearch": true,
"maxDistForAutosearch": 300,
"autosearchLimit": 3,
"numberOfSlices": 3
}

View File

@ -1,7 +1,7 @@
{ {
"id": "gpstrek", "id": "gpstrek",
"name": "GPS Trekking", "name": "GPS Trekking",
"version": "0.10", "version": "0.11",
"description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!", "description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!",
"icon": "icon.png", "icon": "icon.png",
"screenshots": [{"url":"screenInit.png"},{"url":"screenMenu.png"},{"url":"screenMap.png"},{"url":"screenLost.png"},{"url":"screenOverview.png"},{"url":"screenOverviewScroll.png"},{"url":"screenSlices.png"},{"url":"screenSlices2.png"},{"url":"screenSlices3.png"}], "screenshots": [{"url":"screenInit.png"},{"url":"screenMenu.png"},{"url":"screenMap.png"},{"url":"screenLost.png"},{"url":"screenOverview.png"},{"url":"screenOverviewScroll.png"},{"url":"screenSlices.png"},{"url":"screenSlices2.png"},{"url":"screenSlices3.png"}],
@ -12,8 +12,13 @@
"interface" : "interface.html", "interface" : "interface.html",
"storage": [ "storage": [
{"name":"gpstrek.app.js","url":"app.js"}, {"name":"gpstrek.app.js","url":"app.js"},
{"name":"gpstrek.settings.js","url":"settings.js"},
{"name":"gpstrek.default.json","url":"default.json"},
{"name":"gpstrek.wid.js","url":"widget.js"}, {"name":"gpstrek.wid.js","url":"widget.js"},
{"name":"gpstrek.img","url":"app-icon.js","evaluate":true} {"name":"gpstrek.img","url":"app-icon.js","evaluate":true}
], ],
"data": [{"name":"gpstrek.state.json"}] "data": [
{"name":"gpstrek.state.json"},
{"name":"gpstrek.json"}
]
} }

162
apps/gpstrek/settings.js Normal file
View File

@ -0,0 +1,162 @@
(function(back) {
const FILE="gpstrek.json";
let settings;
function writeSettings(key, value) {
var s = require('Storage').readJSON(FILE, true) || {};
s[key] = value;
require('Storage').writeJSON(FILE, s);
readSettings();
}
function readSettings(){
settings = Object.assign(
require('Storage').readJSON("gpstrek.default.json", true) || {},
require('Storage').readJSON(FILE, true) || {}
);
}
function showMapMenu(){
var menu = {
'': { 'title': 'Map', back: showMainMenu },
'Show compass on map': {
value: !!settings.mapCompass,
onchange: v => {
writeSettings("mapCompass",v);
},
},
'Initial map scale': {
value: settings.mapScale,
min: 0.01,max: 2, step:0.01,
onchange: v => {
writeSettings("mapScale",v);
},
},
'Rendered waypoints': {
value: settings.mapChunkSize,
min: 5,max: 60, step:5,
onchange: v => {
writeSettings("mapChunkSize",v);
}
},
'Overview scroll': {
value: settings.overviewScroll,
min: 10,max: 100, step:10,
format: v => v + "px",
onchange: v => {
writeSettings("overviewScroll",v);
}
},
'Initial overview scale': {
value: settings.overviewScale,
min: 0.005,max: 0.1, step:0.005,
onchange: v => {
writeSettings("overviewScale",v);
}
},
'Show direction': {
value: !!settings.mapDirection,
onchange: v => {
writeSettings("mapDirection",v);
}
}
};
E.showMenu(menu);
}
function showRoutingMenu(){
var menu = {
'': { 'title': 'Routing', back: showMainMenu },
'Auto search closest waypoint': {
value: !!settings.autosearch,
onchange: v => {
writeSettings("autosearch",v);
},
},
'Auto search limit': {
value: settings.autosearchLimit,
onchange: v => {
writeSettings("autosearchLimit",v);
},
},
'Waypoint change distance': {
value: settings.waypointChangeDist,
format: v => v + "m",
min: 5,max: 200, step:5,
onchange: v => {
writeSettings("waypointChangeDist",v);
},
}
};
E.showMenu(menu);
}
function showRefreshMenu(){
var menu = {
'': { 'title': 'Refresh', back: showMainMenu },
'Unlocked refresh': {
value: settings.refresh,
format: v => v + "ms",
min: 250,max: 5000, step:250,
onchange: v => {
writeSettings("refresh",v);
}
},
'Locked refresh': {
value: settings.refreshLocked,
min: 1000,max: 60000, step:1000,
format: v => v + "ms",
onchange: v => {
writeSettings("refreshLocked",v);
}
},
'Minimum refresh': {
value: settings.mapRefresh,
format: v => v + "ms",
min: 250,max: 5000, step:250,
onchange: v => {
writeSettings("mapRefresh",v);
}
},
'Minimum course change': {
value: settings.minCourseChange,
min: 0,max: 180, step:1,
format: v => v + "°",
onchange: v => {
writeSettings("minCourseChange",v);
}
},
'Minimum position change': {
value: settings.minPosChange,
min: 0,max: 50, step:1,
format: v => v + "px",
onchange: v => {
writeSettings("minPosChange",v);
}
}
};
E.showMenu(menu);
}
function showMainMenu(){
var mainmenu = {
'': { 'title': 'GPS Trekking', back: back },
'Map': showMapMenu,
'Routing': showRoutingMenu,
'Refresh': showRefreshMenu,
"Info rows" : {
value : settings.numberOfSlices,
min:1,max:6,step:1,
onchange : v => {
writeSettings("numberOfSlices",v);
}
},
};
E.showMenu(mainmenu);
}
readSettings();
showMainMenu();
})

View File

@ -45,7 +45,15 @@ function onPulse(e){
} }
function onGPS(fix) { function onGPS(fix) {
if(fix.fix) state.currentPos = fix; if(fix.fix) {
state.currentPos = fix;
if (Bangle.isCompassOn()){
Bangle.setCompassPower(0, "gpstrek");
state.compassSamples = new Array(SAMPLES).fill(0)
}
} else {
Bangle.setCompassPower(1, "gpstrek");
}
} }
let radians = function(a) { let radians = function(a) {

View File

@ -22,3 +22,7 @@
0.21: Update boot.min.js. 0.21: Update boot.min.js.
0.22: Fix timeout for heartrate sensor on 3 minute setting (#2435) 0.22: Fix timeout for heartrate sensor on 3 minute setting (#2435)
0.23: Fix HRM logic 0.23: Fix HRM logic
0.24: Correct daily health summary for movement (some logic errors resulted in garbage data being written)
0.25: lib.read* methods now return correctly scaled movement
movement graph in app is now an average, not sum
fix 11pm slot for daily HRM

View File

@ -59,7 +59,7 @@ function hrmPerHour() {
E.showMessage(/*LANG*/"Loading..."); E.showMessage(/*LANG*/"Loading...");
current_selection = "hrmPerHour"; current_selection = "hrmPerHour";
var data = new Uint16Array(24); var data = new Uint16Array(24);
var cnt = new Uint8Array(23); var cnt = new Uint8Array(24);
require("health").readDay(new Date(), h=>{ require("health").readDay(new Date(), h=>{
data[h.hr]+=h.bpm; data[h.hr]+=h.bpm;
if (h.bpm) cnt[h.hr]++; if (h.bpm) cnt[h.hr]++;
@ -87,7 +87,12 @@ function movementPerHour() {
E.showMessage(/*LANG*/"Loading..."); E.showMessage(/*LANG*/"Loading...");
current_selection = "movementPerHour"; current_selection = "movementPerHour";
var data = new Uint16Array(24); var data = new Uint16Array(24);
require("health").readDay(new Date(), h=>data[h.hr]+=h.movement); var cnt = new Uint8Array(24);
require("health").readDay(new Date(), h=>{
data[h.hr]+=h.movement
cnt[h.hr]++;
});
data.forEach((d,i)=>data[i] = d/cnt[i]);
setButton(menuMovement); setButton(menuMovement);
barChart(/*LANG*/"HOUR", data); barChart(/*LANG*/"HOUR", data);
} }
@ -96,7 +101,12 @@ function movementPerDay() {
E.showMessage(/*LANG*/"Loading..."); E.showMessage(/*LANG*/"Loading...");
current_selection = "movementPerDay"; current_selection = "movementPerDay";
var data = new Uint16Array(31); var data = new Uint16Array(31);
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement); var cnt = new Uint8Array(31);
require("health").readDailySummaries(new Date(), h=>{
data[h.hr]+=h.movement
cnt[h.hr]++;
});
data.forEach((d,i)=>data[i] = d/cnt[i]);
setButton(menuMovement); setButton(menuMovement);
barChart(/*LANG*/"DAY", data); barChart(/*LANG*/"DAY", data);
} }

View File

@ -52,7 +52,7 @@ Bangle.on("health", health => {
return String.fromCharCode( return String.fromCharCode(
health.steps>>8,health.steps&255, // 16 bit steps health.steps>>8,health.steps&255, // 16 bit steps
health.bpm, // 8 bit bpm health.bpm, // 8 bit bpm
Math.min(health.movement / 8, 255)); // movement Math.min(health.movement, 255)); // movement
} }
var rec = getRecordIdx(d); var rec = getRecordIdx(d);
@ -68,6 +68,12 @@ Bangle.on("health", health => {
require("Storage").write(fn, "HEALTH1\0", 0, DB_FILE_LEN); // header require("Storage").write(fn, "HEALTH1\0", 0, DB_FILE_LEN); // header
} }
var recordPos = DB_HEADER_LEN+(rec*DB_RECORD_LEN); var recordPos = DB_HEADER_LEN+(rec*DB_RECORD_LEN);
// scale down reported movement value in order to fit it within a
// uint8 DB field
health = Object.assign({}, health);
health.movement /= 8;
require("Storage").write(fn, getRecordData(health), recordPos, DB_FILE_LEN); require("Storage").write(fn, getRecordData(health), recordPos, DB_FILE_LEN);
if (rec%DB_RECORDS_PER_DAY != DB_RECORDS_PER_DAY-2) return; if (rec%DB_RECORDS_PER_DAY != DB_RECORDS_PER_DAY-2) return;
// we're at the end of the day. Read in all of the data for the day and sum it up // we're at the end of the day. Read in all of the data for the day and sum it up
@ -82,10 +88,10 @@ Bangle.on("health", health => {
var dt = f.substr(recordPos, DB_RECORD_LEN); var dt = f.substr(recordPos, DB_RECORD_LEN);
if (dt!="\xFF\xFF\xFF\xFF") { if (dt!="\xFF\xFF\xFF\xFF") {
health.steps += (dt.charCodeAt(0)<<8)+dt.charCodeAt(1); health.steps += (dt.charCodeAt(0)<<8)+dt.charCodeAt(1);
health.movement += dt.charCodeAt(2);
health.movCnt++;
var bpm = dt.charCodeAt(2); var bpm = dt.charCodeAt(2);
health.bpm += bpm; health.bpm += bpm;
health.movement += dt.charCodeAt(3);
health.movCnt++;
if (bpm) health.bpmCnt++; if (bpm) health.bpmCnt++;
} }
recordPos -= DB_RECORD_LEN; recordPos -= DB_RECORD_LEN;

View File

@ -1,5 +1,5 @@
function l(){var a=require("Storage").readJSON("health.json",1)||{},d=Bangle.getHealthStatus("day").steps;a.stepGoalNotification&&0<a.stepGoal&&d>=a.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDate<d)&&(Bangle.buzz(200,.5),require("notify").show({title:a.stepGoal+" steps",body:"You reached your step goal!",icon:atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==")}),a.stepGoalNotificationDate=d,require("Storage").writeJSON("health.json", function m(){var a=require("Storage").readJSON("health.json",1)||{},d=Bangle.getHealthStatus("day").steps;a.stepGoalNotification&&0<a.stepGoal&&d>=a.stepGoal&&(d=(new Date(Date.now())).toISOString().split("T")[0],!a.stepGoalNotificationDate||a.stepGoalNotificationDate<d)&&(Bangle.buzz(200,.5),require("notify").show({title:a.stepGoal+" steps",body:"You reached your step goal!",icon:atob("DAyBABmD6BaBMAsA8BCBCBCBCA8AAA==")}),a.stepGoalNotificationDate=d,require("Storage").writeJSON("health.json",
a))}(function(){var a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){var d=function(){Bangle.setHRMPower(1,"health");setTimeout(function(){return Bangle.setHRMPower(0,"health")},6E4*a);if(1==a){var b=function(){Bangle.setHRMPower(1,"health");setTimeout(function(){Bangle.setHRMPower(0,"health")},6E4)};setTimeout(b,2E5);setTimeout(b,4E5)}};Bangle.on("health",d);Bangle.on("HRM",function(b){90<b.confidence&&1>Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0, a))}(function(){var a=0|(require("Storage").readJSON("health.json",1)||{}).hrm;if(1==a||2==a){function d(){Bangle.setHRMPower(1,"health");setTimeout(()=>Bangle.setHRMPower(0,"health"),6E4*a);if(1==a){function b(){Bangle.setHRMPower(1,"health");setTimeout(()=>{Bangle.setHRMPower(0,"health")},6E4)}setTimeout(b,2E5);setTimeout(b,4E5)}}Bangle.on("health",d);Bangle.on("HRM",b=>{90<b.confidence&&1>Math.abs(Bangle.getHealthStatus().bpm-b.bpm)&&Bangle.setHRMPower(0,"health")});90<Bangle.getHealthStatus().bpmConfidence||
"health")});90<Bangle.getHealthStatus().bpmConfidence||d()}else Bangle.setHRMPower(!!a,"health")})();Bangle.on("health",function(a){function d(c){return String.fromCharCode(c.steps>>8,c.steps&255,c.bpm,Math.min(c.movement/8,255))}var b=new Date(Date.now()-59E4);a&&0<a.steps&&l();var f=function(c){return 145*(c.getDate()-1)+6*c.getHours()+(0|6*c.getMinutes()/60)}(b);b=function(c){return"health-"+c.getFullYear()+"-"+(c.getMonth()+1)+".raw"}(b);var g=require("Storage").read(b);if(g){var e=g.substr(8+ d()}else Bangle.setHRMPower(!!a,"health")})();Bangle.on("health",a=>{function d(c){return String.fromCharCode(c.steps>>8,c.steps&255,c.bpm,Math.min(c.movement,255))}var b=new Date(Date.now()-59E4);a&&0<a.steps&&m();var f=function(c){return 145*(c.getDate()-1)+6*c.getHours()+(0|6*c.getMinutes()/60)}(b);b=function(c){return"health-"+c.getFullYear()+"-"+(c.getMonth()+1)+".raw"}(b);var g=require("Storage").read(b);if(g){var e=g.substr(8+4*f,4);if("\xff\xff\xff\xff"!=e){print("HEALTH ERR: Already written!");
4*f,4);if("\u00ff\u00ff\u00ff\u00ff"!=e){print("HEALTH ERR: Already written!");return}}else require("Storage").write(b,"HEALTH1\x00",0,17988);var h=8+4*f;require("Storage").write(b,d(a),h,17988);if(143==f%145)if(f=h+4,"\u00ff\u00ff\u00ff\u00ff"!=g.substr(f,4))print("HEALTH ERR: Daily summary already written!");else{a={steps:0,bpm:0,movement:0,movCnt:0,bpmCnt:0};for(var k=0;144>k;k++)e=g.substr(h,4),"\u00ff\u00ff\u00ff\u00ff"!=e&&(a.steps+=(e.charCodeAt(0)<<8)+e.charCodeAt(1),a.movement+=e.charCodeAt(2), return}}else require("Storage").write(b,"HEALTH1\x00",0,17988);var h=8+4*f;a=Object.assign({},a);a.movement/=8;require("Storage").write(b,d(a),h,17988);if(143==f%145)if(f=h+4,"\xff\xff\xff\xff"!=g.substr(f,4))print("HEALTH ERR: Daily summary already written!");else{a={steps:0,bpm:0,movement:0,movCnt:0,bpmCnt:0};for(var k=0;144>k;k++){e=g.substr(h,4);if("\xff\xff\xff\xff"!=e){a.steps+=(e.charCodeAt(0)<<8)+e.charCodeAt(1);var l=e.charCodeAt(2);a.bpm+=l;a.movement+=e.charCodeAt(3);a.movCnt++;
a.movCnt++,e=e.charCodeAt(2),a.bpm+=e,e&&a.bpmCnt++),h-=4;a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(b,d(a),f,17988)}}) l&&a.bpmCnt++}h-=4}a.bpmCnt&&(a.bpm/=a.bpmCnt);a.movCnt&&(a.movement/=a.movCnt);require("Storage").write(b,d(a),f,17988)}})

View File

@ -29,7 +29,7 @@ exports.readAllRecords = function(d, cb) {
day:day+1, hr : hr, min:m*10, day:day+1, hr : hr, min:m*10,
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1), steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
bpm : h.charCodeAt(2), bpm : h.charCodeAt(2),
movement : h.charCodeAt(3) movement : h.charCodeAt(3)*8
}); });
} }
idx += DB_RECORD_LEN; idx += DB_RECORD_LEN;
@ -53,7 +53,7 @@ exports.readDailySummaries = function(d, cb) {
day:day+1, day:day+1,
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1), steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
bpm : h.charCodeAt(2), bpm : h.charCodeAt(2),
movement : h.charCodeAt(3) movement : h.charCodeAt(3)*8
}); });
} }
idx += DB_RECORDS_PER_DAY*DB_RECORD_LEN; idx += DB_RECORDS_PER_DAY*DB_RECORD_LEN;
@ -75,7 +75,7 @@ exports.readDay = function(d, cb) {
hr : hr, min:m*10, hr : hr, min:m*10,
steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1), steps : (h.charCodeAt(0)<<8) | h.charCodeAt(1),
bpm : h.charCodeAt(2), bpm : h.charCodeAt(2),
movement : h.charCodeAt(3) movement : h.charCodeAt(3)*8
}); });
} }
idx += DB_RECORD_LEN; idx += DB_RECORD_LEN;

View File

@ -1,3 +1,3 @@
function h(a){return"health-"+a.getFullYear()+"-"+(a.getMonth()+1)+".raw"}function k(a){return 145*(a.getDate()-1)+6*a.getHours()+(0|6*a.getMinutes()/60)}exports.readAllRecords=function(a,f){a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=8,d=0;31>d;d++){for(var b=0;24>b;b++)for(var e=0;6>e;e++){var g=a.substr(c,4);"\u00ff\u00ff\u00ff\u00ff"!=g&&f({day:d+1,hr:b,min:10*e,steps:g.charCodeAt(0)<<8|g.charCodeAt(1),bpm:g.charCodeAt(2),movement:g.charCodeAt(3)});c+= function h(a){return"health-"+a.getFullYear()+"-"+(a.getMonth()+1)+".raw"}function k(a){return 145*(a.getDate()-1)+6*a.getHours()+(0|6*a.getMinutes()/60)}exports.readAllRecords=function(a,f){a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=8,d=0;31>d;d++){for(var b=0;24>b;b++)for(var e=0;6>e;e++){var g=a.substr(c,4);"\xff\xff\xff\xff"!=g&&f({day:d+1,hr:b,min:10*e,steps:g.charCodeAt(0)<<8|g.charCodeAt(1),bpm:g.charCodeAt(2),movement:8*g.charCodeAt(3)});c+=
4}c+=4}};exports.readDailySummaries=function(a,f){k(a);a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=584,d=0;31>d;d++){var b=a.substr(c,4);"\u00ff\u00ff\u00ff\u00ff"!=b&&f({day:d+1,steps:b.charCodeAt(0)<<8|b.charCodeAt(1),bpm:b.charCodeAt(2),movement:b.charCodeAt(3)});c+=580}};exports.readDay=function(a,f){k(a);var c=h(a);c=require("Storage").read(c);if(void 0!==c){a=8+580*(a.getDate()-1);for(var d=0;24>d;d++)for(var b=0;6>b;b++){var e=c.substr(a,4);"\u00ff\u00ff\u00ff\u00ff"!=e&&f({hr:d, 4}c+=4}};exports.readDailySummaries=function(a,f){k(a);a=h(a);a=require("Storage").read(a);if(void 0!==a)for(var c=584,d=0;31>d;d++){var b=a.substr(c,4);"\xff\xff\xff\xff"!=b&&f({day:d+1,steps:b.charCodeAt(0)<<8|b.charCodeAt(1),bpm:b.charCodeAt(2),movement:8*b.charCodeAt(3)});c+=580}};exports.readDay=function(a,f){k(a);var c=h(a);c=require("Storage").read(c);if(void 0!==c){a=8+580*(a.getDate()-1);for(var d=0;24>d;d++)for(var b=0;6>b;b++){var e=c.substr(a,4);"\xff\xff\xff\xff"!=e&&
min:10*b,steps:e.charCodeAt(0)<<8|e.charCodeAt(1),bpm:e.charCodeAt(2),movement:e.charCodeAt(3)});a+=4}}} f({hr:d,min:10*b,steps:e.charCodeAt(0)<<8|e.charCodeAt(1),bpm:e.charCodeAt(2),movement:8*e.charCodeAt(3)});a+=4}}}

View File

@ -2,7 +2,7 @@
"id": "health", "id": "health",
"name": "Health Tracking", "name": "Health Tracking",
"shortName": "Health", "shortName": "Health",
"version": "0.23", "version": "0.25",
"description": "Logs health data and provides an app to view it", "description": "Logs health data and provides an app to view it",
"icon": "app.png", "icon": "app.png",
"tags": "tool,system,health", "tags": "tool,system,health",

View File

@ -21,4 +21,6 @@
0.15: Ensure that we hide widgets if in fullscreen mode 0.15: Ensure that we hide widgets if in fullscreen mode
(So that widgets are still hidden if launcher is fast-loaded) (So that widgets are still hidden if launcher is fast-loaded)
0.16: Use firmware provided E.showScroller method 0.16: Use firmware provided E.showScroller method
0.17: fix fullscreen with oneClickExit 0.17: fix fullscreen with oneClickExit
0.18: Better performance
0.19: Remove 'jit' keyword as 'for(..of..)' is not supported (fix #2937)

View File

@ -9,7 +9,6 @@
timeOut:"Off" timeOut:"Off"
}, s.readJSON("iconlaunch.json", true) || {}); }, s.readJSON("iconlaunch.json", true) || {});
if (!settings.fullscreen) { if (!settings.fullscreen) {
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
@ -19,9 +18,9 @@
let launchCache = s.readJSON("iconlaunch.cache.json", true)||{}; let launchCache = s.readJSON("iconlaunch.cache.json", true)||{};
let launchHash = s.hash(/\.info/); let launchHash = s.hash(/\.info/);
if (launchCache.hash!=launchHash) { if (launchCache.hash!=launchHash) {
launchCache = { launchCache = {
hash : launchHash, hash : launchHash,
apps : s.list(/\.info$/) apps : s.list(/\.info$/)
.map(app=>{let a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};}) .map(app=>{let a=s.readJSON(app,1);return a&&{name:a.name,type:a.type,icon:a.icon,sortorder:a.sortorder,src:a.src};})
.filter(app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || !app.type)) .filter(app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || !app.type))
.sort((a,b)=>{ .sort((a,b)=>{
@ -34,42 +33,65 @@
s.writeJSON("iconlaunch.cache.json", launchCache); s.writeJSON("iconlaunch.cache.json", launchCache);
} }
// cache items
const ICON_MISSING = s.read("iconlaunch.na.img");
let count = 0;
let selectedItem = -1; let selectedItem = -1;
const R = Bangle.appRect; const R = Bangle.appRect;
const iconSize = 48; const iconSize = 48;
const appsN = Math.floor(R.w / iconSize); const appsN = Math.floor(R.w / iconSize);
const whitespace = (R.w - appsN * iconSize) / (appsN + 1); const whitespace = Math.floor((R.w - appsN * iconSize) / (appsN + 1));
const iconYoffset = Math.floor(whitespace/4)-1;
const itemSize = iconSize + whitespace; const itemSize = iconSize + whitespace;
launchCache.items = {};
for (let c of launchCache.apps){
let i = Math.floor(count/appsN);
if (!launchCache.items[i])
launchCache.items[i] = {};
launchCache.items[i][(count%3)] = c;
count++;
}
let texted;
let drawItem = function(itemI, r) { let drawItem = function(itemI, r) {
g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1); let x = whitespace;
let x = 0; let i = itemI * appsN - 1;
for (let i = itemI * appsN; i < appsN * (itemI + 1); i++) { let selectedApp;
if (!launchCache.apps[i]) break; let c;
x += whitespace; let selectedRect;
if (!launchCache.apps[i].icon) { let item = launchCache.items[itemI];
g.setFontAlign(0, 0, 0).setFont("12x20:2").drawString("?", x + r.x + iconSize / 2, r.y + iconSize / 2); if (texted == itemI){
} else { g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1);
if (!launchCache.apps[i].icondata) launchCache.apps[i].icondata = s.read(launchCache.apps[i].icon); texted = undefined;
g.drawImage(launchCache.apps[i].icondata, x + r.x, r.y); }
} for (c of item) {
if (selectedItem == i) { i++;
g.drawRect( let id = c.icondata || (c.iconData = (c.icon ? s.read(c.icon) : ICON_MISSING));
x + r.x - 1, g.drawImage(id,x + r.x - 1, r.y + iconYoffset - 1, x + r.x + iconSize, r.y + iconYoffset + iconSize);
r.y - 1, if (selectedItem == i) {
x + r.x + iconSize + 1, selectedApp = c;
r.y + iconSize + 1 selectedRect = [
); x + r.x - 1,
} r.y + iconYoffset - 1,
x += iconSize; x + r.x + iconSize,
r.y + iconYoffset + iconSize
];
}
x += iconSize + whitespace;
}
if (selectedRect) {
g.drawRect.apply(null, selectedRect);
drawText(itemI, r.y, selectedApp);
texted=itemI;
} }
drawText(itemI, r.y);
}; };
let drawText = function(i, appY) { let drawText = function(i, appY, selectedApp) {
const selectedApp = launchCache.apps[selectedItem]; "jit";
const idy = (selectedItem - (selectedItem % 3)) / 3; const idy = (selectedItem - (selectedItem % 3)) / 3;
if (!selectedApp || i != idy) return; if (i != idy) return;
appY = appY + itemSize/2; appY = appY + itemSize/2;
g.setFontAlign(0, 0, 0); g.setFontAlign(0, 0, 0);
g.setFont("12x20"); g.setFont("12x20");
@ -122,21 +144,21 @@
}, },
btn:Bangle.showClock btn:Bangle.showClock
}; };
//work both the fullscreen and the oneClickExit //work both the fullscreen and the oneClickExit
if( settings.fullscreen && settings.oneClickExit) if( settings.fullscreen && settings.oneClickExit)
{ {
idWatch=setWatch(function(e) { idWatch=setWatch(function(e) {
Bangle.showClock(); Bangle.showClock();
}, BTN, {repeat:false, edge:'rising' }); }, BTN, {repeat:false, edge:'rising' });
} }
else if( settings.oneClickExit ) else if( settings.oneClickExit )
{ {
options.back=Bangle.showClock; options.back=Bangle.showClock;
} }
let scroller = E.showScroller(options); let scroller = E.showScroller(options);
@ -151,7 +173,7 @@
}; };
let swipeHandler = (h,_) => { if(settings.swipeExit && h==1) { Bangle.showClock(); } }; let swipeHandler = (h,_) => { if(settings.swipeExit && h==1) { Bangle.showClock(); } };
Bangle.on("swipe", swipeHandler) Bangle.on("swipe", swipeHandler)
Bangle.on("drag", updateTimeout); Bangle.on("drag", updateTimeout);
Bangle.on("touch", updateTimeout); Bangle.on("touch", updateTimeout);

View File

@ -2,7 +2,7 @@
"id": "iconlaunch", "id": "iconlaunch",
"name": "Icon Launcher", "name": "Icon Launcher",
"shortName" : "Icon launcher", "shortName" : "Icon launcher",
"version": "0.17", "version": "0.19",
"icon": "app.png", "icon": "app.png",
"description": "A launcher inspired by smartphones, with an icon-only scrollable menu.", "description": "A launcher inspired by smartphones, with an icon-only scrollable menu.",
"tags": "tool,system,launcher", "tags": "tool,system,launcher",
@ -10,7 +10,8 @@
"supports": ["BANGLEJS2"], "supports": ["BANGLEJS2"],
"storage": [ "storage": [
{ "name": "iconlaunch.app.js", "url": "app.js" }, { "name": "iconlaunch.app.js", "url": "app.js" },
{ "name": "iconlaunch.settings.js", "url": "settings.js" } { "name": "iconlaunch.settings.js", "url": "settings.js" },
{ "name": "iconlaunch.na.img", "url": "na.img" }
], ],
"data": [{"name":"iconlaunch.json"},{"name":"iconlaunch.cache.json"}], "data": [{"name":"iconlaunch.json"},{"name":"iconlaunch.cache.json"}],
"screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }], "screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }],

BIN
apps/iconlaunch/na.img Normal file

Binary file not shown.

View File

@ -16,8 +16,7 @@
} }
const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"]; const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"];
const appMenu = { const appMenu = {
"": { "title": /*LANG*/"Launcher" }, "": { "title": /*LANG*/"Launcher", back: back },
/*LANG*/"< Back": back,
/*LANG*/"Show Clocks": { /*LANG*/"Show Clocks": {
value: settings.showClocks == true, value: settings.showClocks == true,
onchange: (m) => { onchange: (m) => {

View File

@ -4,3 +4,6 @@
0.04: Allow moving the cursor 0.04: Allow moving the cursor
0.05: Switch swipe directions for Caps Lock and moving cursor. 0.05: Switch swipe directions for Caps Lock and moving cursor.
0.06: Add ability to auto-lowercase after a capital letter insertion. 0.06: Add ability to auto-lowercase after a capital letter insertion.
0.07: Add compatability with `backswipe` app by using `Bangle.prependListener()` and `E.stopEventPropagation`- requires fw 2v19 or cutting
edge versions of 2v18. Falls back on `Bangle.on()` for backwards
compatability.

View File

@ -154,6 +154,7 @@ exports.input = function(options) {
displayText(false); displayText(false);
} }
} }
E.stopEventPropagation&&E.stopEventPropagation();
} }
function onHelp(resolve,reject) { function onHelp(resolve,reject) {
@ -161,7 +162,7 @@ exports.input = function(options) {
E.showPrompt( E.showPrompt(
helpMessage, {title: "Help", buttons : {"Ok":true}} helpMessage, {title: "Help", buttons : {"Ok":true}}
).then(function(v) { ).then(function(v) {
Bangle.on('swipe', onSwipe); if (Bangle.prependListener) {Bangle.prependListener('swipe', onSwipe);} else {Bangle.on('swipe', onSwipe);}
generateLayout(resolve,reject); generateLayout(resolve,reject);
layout.render(); layout.render();
}); });
@ -208,7 +209,7 @@ exports.input = function(options) {
} else { } else {
generateLayout(resolve,reject); generateLayout(resolve,reject);
displayText(false); displayText(false);
Bangle.on('swipe', onSwipe); if (Bangle.prependListener) {Bangle.prependListener('swipe', onSwipe);} else {Bangle.on('swipe', onSwipe);}
layout.render(); layout.render();
} }
}); });

View File

@ -1,6 +1,6 @@
{ "id": "kbmulti", { "id": "kbmulti",
"name": "Multitap keyboard", "name": "Multitap keyboard",
"version":"0.06", "version":"0.07",
"description": "A library for text input via multitap/T9 style keypad", "description": "A library for text input via multitap/T9 style keypad",
"icon": "app.png", "icon": "app.png",
"type":"textinput", "type":"textinput",

View File

@ -6,3 +6,5 @@
0.06: Support input of numbers and uppercase characters. 0.06: Support input of numbers and uppercase characters.
0.07: Support input of symbols. 0.07: Support input of symbols.
0.08: Redone patterns a,e,m,w,z. 0.08: Redone patterns a,e,m,w,z.
0.09: Catch and discard swipe events on fw2v19 and up (as well as some cutting
edge 2v18 ones), allowing compatability with the Back Swipe app.

View File

@ -253,28 +253,38 @@ exports.input = function(options) {
}; };
Bangle.drawWidgets(); Bangle.drawWidgets();
let dragHandlerKB = e=>{
"ram";
if (isInside(R, e)) {
if (lastDrag) g.reset().setColor("#f00").drawLine(lastDrag.x,lastDrag.y,e.x,e.y);
lastDrag = e.b ? e : 0;
}
}
let touchHandlerKB = (n,e) => {
if (WIDGETS.kbswipe && isInside({x: WIDGETS.kbswipe.x, y: WIDGETS.kbswipe.y, w: WIDGETS.kbswipe.width, h: 24}, e)) {
// touch inside widget
cycleInput();
} else if (isInside(R, e)) {
// touch inside app area
show();
}
}
let catchSwipe = ()=>{
E.stopEventPropagation&&E.stopEventPropagation();
};
return new Promise((resolve,reject) => { return new Promise((resolve,reject) => {
Bangle.setUI({mode:"custom", drag:e=>{ Bangle.setUI({mode:"custom", drag:dragHandlerKB, touch:touchHandlerKB, back:()=>{
"ram";
if (isInside(R, e)) {
if (lastDrag) g.reset().setColor("#f00").drawLine(lastDrag.x,lastDrag.y,e.x,e.y);
lastDrag = e.b ? e : 0;
}
},touch:(n,e) => {
if (WIDGETS.kbswipe && isInside({x: WIDGETS.kbswipe.x, y: WIDGETS.kbswipe.y, w: WIDGETS.kbswipe.width, h: 24}, e)) {
// touch inside widget
cycleInput();
} else if (isInside(R, e)) {
// touch inside app area
show();
}
}, back:()=>{
delete WIDGETS.kbswipe; delete WIDGETS.kbswipe;
Bangle.removeListener("stroke", strokeHandler); Bangle.removeListener("stroke", strokeHandler);
Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up).
if (flashInterval) clearInterval(flashInterval); if (flashInterval) clearInterval(flashInterval);
Bangle.setUI(); Bangle.setUI();
g.clearRect(Bangle.appRect); g.clearRect(Bangle.appRect);
resolve(text); resolve(text);
}}); }});
Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
}); });
}; };

View File

@ -1,6 +1,6 @@
{ "id": "kbswipe", { "id": "kbswipe",
"name": "Swipe keyboard", "name": "Swipe keyboard",
"version":"0.08", "version":"0.09",
"description": "A library for text input via PalmOS style swipe gestures (beta!)", "description": "A library for text input via PalmOS style swipe gestures (beta!)",
"icon": "app.png", "icon": "app.png",
"type":"textinput", "type":"textinput",

View File

@ -1,3 +1,5 @@
0.01: New App! 0.01: New App!
0.02: Introduced settings to customize the layout and functionality of the keyboard. 0.02: Introduced settings to customize the layout and functionality of the keyboard.
0.03: Convert Yes/No On/Off in settings to checkboxes 0.03: Convert Yes/No On/Off in settings to checkboxes
0.04: Catch and discard swipe events on fw2v19 and up (as well as some cutting
edge 2v18 ones), allowing compatability with the Back Swipe app.

View File

@ -161,8 +161,11 @@ function draw() {
},back:()=>{ },back:()=>{
clearInterval(flashInterval); clearInterval(flashInterval);
Bangle.setUI(); Bangle.setUI();
Bangle.prependListener&&Bangle.removeListener('swipe', catchSwipe); // Remove swipe lister if it was added with `Bangle.prependListener()` (fw2v19 and up).
g.clearRect(Bangle.appRect); g.clearRect(Bangle.appRect);
resolve(text); resolve(text);
}}); }});
let catchSwipe = ()=>{E.stopEventPropagation&&E.stopEventPropagation();};
Bangle.prependListener&&Bangle.prependListener('swipe', catchSwipe); // Intercept swipes on fw2v19 and later. Should not break on older firmwares.
}); });
}; };

View File

@ -1,6 +1,6 @@
{ "id": "kbtouch", { "id": "kbtouch",
"name": "Touch keyboard", "name": "Touch keyboard",
"version":"0.03", "version":"0.04",
"description": "A library for text input via onscreen keyboard", "description": "A library for text input via onscreen keyboard",
"icon": "app.png", "icon": "app.png",
"type":"textinput", "type":"textinput",

View File

@ -0,0 +1 @@
0.01: New App!

View File

@ -0,0 +1,7 @@
# Kinetic scrolling
This patches the default scroller implementation to use kinetic scrolling. It is based on the original implementation.
## Creator
[halemmerich](https://github.com/halemmerich)

BIN
apps/kineticscroll/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

181
apps/kineticscroll/boot.js Normal file
View File

@ -0,0 +1,181 @@
(function() {
E.showScroller = function(options) {
/* options = {
h = height
c = # of items
scroll = initial scroll position
scrollMin = minimum scroll amount (can be negative)
draw = function(idx, rect)
remove = function()
select = function(idx, touch)
}
returns {
scroll: int // current scroll amount
draw: function() // draw all
drawItem : function(idx) // draw specific item
isActive : function() // is this scroller still active?
}
*/
if (!options) return Bangle.setUI(); // remove existing handlers
const MAX_VELOCITY=100;
let scheduledDraw;
let velocity = 0;
let accDy = 0;
let scheduledBrake = setInterval(()=>{velocity*=0.9;}, 50);
let lastDragStart = 0;
let R = Bangle.appRect;
let menuScrollMin = 0|options.scrollMin;
let menuScrollMax = options.h*options.c - R.h;
if (menuScrollMax<menuScrollMin) menuScrollMax=menuScrollMin;
const touchHandler = (_,e)=>{
if (e.y<R.y-4) return;
velocity = 0;
accDy = 0;
let i = YtoIdx(e.y);
if ((menuScrollMin<0 || i>=0) && i<options.c){
let yAbs = (e.y + rScroll - R.y);
let yInElement = yAbs - i*options.h;
print("selected");
options.select(i, {x:e.x, y:yInElement});
}
};
const uiDraw = () => {
g.reset().clearRect(R).setClipRect(R.x,R.y,R.x2,R.y2);
var a = YtoIdx(R.y);
var b = Math.min(YtoIdx(R.y2),options.c-1);
for (var i=a;i<=b;i++)
options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h});
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
}
const draw = () => {
let dy = velocity;
if (s.scroll - dy > menuScrollMax){
dy = s.scroll - menuScrollMax;
velocity = 0;
}
if (s.scroll - dy < menuScrollMin){
dy = s.scroll - menuScrollMin;
velocity = 0;
}
s.scroll -= dy;
let oldScroll = rScroll;
rScroll = s.scroll &~1;
let d = oldScroll-rScroll;
if (Math.abs(velocity) > 0.01)
scheduledDraw = setTimeout(draw,0);
else
scheduledDraw = undefined;
if (!d) {
return;
}
g.reset().setClipRect(R.x,R.y,R.x2,R.y2).scroll(0,d);
if (d < 0) {
let y = Math.max(R.y2-(1-d), R.y);
g.setClipRect(R.x,y,R.x2,R.y2);
let i = YtoIdx(y);
for (y = idxToY(i);y < R.y2;y+=options.h) {
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
i++;
}
} else { // d>0
let y = Math.min(R.y+d, R.y2);
g.setClipRect(R.x,R.y,R.x2,y);
let i = YtoIdx(y);
y = idxToY(i);
for (y = idxToY(i);y > R.y-options.h;y-=options.h) {
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
i--;
}
}
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
};
const dragHandler = e=>{
if ((velocity <0 && e.dy>0) || (velocity > 0 && e.dy<0)){
velocity *= -1;
accDy = 5 * velocity;
}
//velocity += e.dy * (Date.now() - lastDrag);
if (e.b > 0){
if (!lastDragStart){
lastDragStart = Date.now();
velocity = 0;
accDy = 0;
}
accDy += e.dy;
}
velocity = accDy / (Date.now() - lastDragStart) * MAX_VELOCITY;
if (lastDragStart && e.b == 0){
accDy = 0;
lastDragStart = 0;
}
velocity = E.clip(velocity,-MAX_VELOCITY,MAX_VELOCITY);
lastDrag=Date.now();
if (!scheduledDraw){
scheduledDraw = setTimeout(draw,0);
}
};
let uiOpts = {
mode : "custom",
back : options.back,
drag : dragHandler,
touch : touchHandler,
redraw : uiDraw
}
if (options.remove) uiOpts.remove = () => {
if (scheduledDraw)
clearTimeout(scheduledDraw);
clearInterval(scheduledBrake);
options.remove();
}
Bangle.setUI(uiOpts);
function idxToY(i) {
return i*options.h + R.y - rScroll;
}
function YtoIdx(y) {
return Math.floor((y + rScroll - R.y)/options.h);
}
let s = {
scroll : E.clip(0|options.scroll,menuScrollMin,menuScrollMax),
draw : () => {
g.reset().clearRect(R).setClipRect(R.x,R.y,R.x2,R.y2);
let a = YtoIdx(R.y);
let b = Math.min(YtoIdx(R.y2),options.c-1);
for (let i=a;i<=b;i++)
options.draw(i, {x:R.x,y:idxToY(i),w:R.w,h:options.h});
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
}, drawItem : i => {
let y = idxToY(i);
g.reset().setClipRect(R.x,Math.max(y,R.y),R.x2,Math.min(y+options.h,R.y2));
options.draw(i, {x:R.x,y:y,w:R.w,h:options.h});
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
}, isActive : () => Bangle.uiRedraw == uiDraw
};
let rScroll = s.scroll&~1; // rendered menu scroll (we only shift by 2 because of dither)
s.draw(); // draw the full scroller
g.flip(); // force an update now to make this snappier
return s;
};
})();

4
apps/kineticscroll/boot.min.js vendored Normal file
View File

@ -0,0 +1,4 @@
(function(){E.showScroller=function(c){function k(a){return a*c.h+b.y-l}function h(a){return Math.floor((a+l-b.y)/c.h)}if(!c)return Bangle.setUI();let p,e=0,m=0,w=setInterval(()=>{e*=.9},50),q=0,b=Bangle.appRect,n=0|c.scrollMin,r=c.h*c.c-b.h;r<n&&(r=n);const t=()=>{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);for(var a=h(b.y),d=Math.min(h(b.y2),c.c-1);a<=d;a++)c.draw(a,{x:b.x,y:k(a),w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},u=()=>{var a=e;f.scroll-a>r&&
(a=f.scroll-r,e=0);f.scroll-a<n&&(a=f.scroll-n,e=0);f.scroll-=a;a=l;l=f.scroll&-2;a-=l;p=.01<Math.abs(e)?setTimeout(u,0):void 0;if(a){g.reset().setClipRect(b.x,b.y,b.x2,b.y2).scroll(0,a);if(0>a){a=Math.max(b.y2-(1-a),b.y);g.setClipRect(b.x,a,b.x2,b.y2);var d=h(a);for(a=k(d);a<b.y2;a+=c.h)c.draw(d,{x:b.x,y:a,w:b.w,h:c.h}),d++}else for(a=Math.min(b.y+a,b.y2),g.setClipRect(b.x,b.y,b.x2,a),d=h(a),k(d),a=k(d);a>b.y-c.h;a-=c.h)c.draw(d,{x:b.x,y:a,w:b.w,h:c.h}),d--;g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-
1)}};let v={mode:"custom",back:c.back,drag:a=>{if(0>e&&0<a.dy||0<e&&0>a.dy)e*=-1,m=5*e;0<a.b&&(q||(q=Date.now(),m=e=0),m+=a.dy);e=m/(Date.now()-q)*100;q&&0==a.b&&(q=m=0);e=E.clip(e,-100,100);lastDrag=Date.now();p||(p=setTimeout(u,0))},touch:(a,d)=>{if(!(d.y<b.y-4)&&(m=e=0,a=h(d.y),(0>n||0<=a)&&a<c.c)){let x=d.y+l-b.y-a*c.h;print("selected");c.select(a,{x:d.x,y:x})}},redraw:t};c.remove&&(v.remove=()=>{p&&clearTimeout(p);clearInterval(w);c.remove()});Bangle.setUI(v);let f={scroll:E.clip(0|c.scroll,
n,r),draw:()=>{g.reset().clearRect(b).setClipRect(b.x,b.y,b.x2,b.y2);var a=h(b.y);let d=Math.min(h(b.y2),c.c-1);for(;a<=d;a++)c.draw(a,{x:b.x,y:k(a),w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},drawItem:a=>{let d=k(a);g.reset().setClipRect(b.x,Math.max(d,b.y),b.x2,Math.min(d+c.h,b.y2));c.draw(a,{x:b.x,y:d,w:b.w,h:c.h});g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1)},isActive:()=>Bangle.uiRedraw==t},l=f.scroll&-2;f.draw();g.flip();return f}})()

View File

@ -0,0 +1,14 @@
{ "id": "kineticscroll",
"name": "Kinetic Scroll",
"shortName":"Kinetic Scroll",
"version":"0.01",
"description": "Replacement for the system scroller with kinetic scrolling.",
"icon": "app.png",
"type": "bootloader",
"tags": "system",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"storage": [
{"name":"kineticscroll.boot.js","url":"boot.min.js"}
]
}

1
apps/lunaclock/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: New App!

9
apps/lunaclock/README.md Normal file
View File

@ -0,0 +1,9 @@
# Luna Clock
![](screenshot.png)
Simple clock face inspired by the moon.
## Creator
NovaDawn999

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwkEIf4A/AH4AJiIABiAVRiUz/4AB+cyDJ8TCoX/mc/AQMgCxkjCwgAB+YCBDBcDCwYXCn4CC+ZKJgQWEFYQWCA4MyC5EvC5HzA4U/JI4uEIAQWBD4k/+QuKLYZDCDwQFCGAsBIgoBBFIIGBPIf/GAqMFDIQvDHARjBSQp1EFAIpCL4YGEmBGEE4akEIYYIDPIhGBiYwDmTSEgUiAwMiAAMzJAUf+ZgD+cCCwX/iMjCQMxiczmUDSIUvmIXDmIXBmchmcTn8jmcgJAUgC4U/iLXC+ciC4UgCgMyOwMRC4UwiJ2BLAIXDAgJHBCgReCEAQXBiUACgURWoQEBdAYvBCoIXL+TDBiAWCegUgicBC4cTC4IPBkE/OoM/AgK6EBIMQC4ixBH4ISCmUSkEvBoJ3BJAR9CBIMvC4KHCl4YCiESiYXBSAUBWwIRBNgIXEkZbCmLIBRYchgUykMjAILwBgLeBkAhCLAIEBUwK3BiEikUykRJCiEAiQDBAQJYDAgRABiQVBCwcyd4MCiIHCAAMhT4YACCwIXCkUhC4MBiQgDCAQFCkYVCAAhGBC4MBEIQZEFggWHAARrCkQVELYMhMAICBiJFCC4gMBJYI0CCoInCiAlBCooAEDIIUBHwsBFwIXKDAITBQAIECBAIWMJYSBDGAYXNIAaFFACAtELZpUBHoIBBDAIZBBYJ4CEA0TawMyaQQXBmUwAQMgiYKBmIXFgTjEkMgA4QXBcgQbBNCoAEA=="))

72
apps/lunaclock/app.js Normal file

File diff suppressed because one or more lines are too long

BIN
apps/lunaclock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,17 @@
{
"id": "lunaclock",
"name": "Luna Clock",
"version": "0.01",
"description": "Simple clock face inspired by the moon.",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",
"tags": "clock",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"allow_emulator": true,
"storage": [
{"name":"lunaclock.app.js","url":"app.js"},
{"name":"lunaclock.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -2,3 +2,6 @@
0.02: Fix touch/drag/swipe handlers not being restored correctly if a message is removed 0.02: Fix touch/drag/swipe handlers not being restored correctly if a message is removed
0.03: Scroll six lines per swipe, leaving the previous top/bottom row visible. 0.03: Scroll six lines per swipe, leaving the previous top/bottom row visible.
0.04: Use the event mechanism for getting messages 0.04: Use the event mechanism for getting messages
0.05: Fix the overlay keeping the LCD on
0.06: Better low memory handling
Fix first message beeing displayed again on unlock

View File

@ -1,3 +1,5 @@
const MIN_FREE_MEM = 1000;
const LOW_MEM = 2000;
const ovrx = 10; const ovrx = 10;
const ovry = 10; const ovry = 10;
const ovrw = g.getWidth()-2*ovrx; const ovrw = g.getWidth()-2*ovrx;
@ -28,6 +30,7 @@ let callInProgress = false;
let show = function(ovr){ let show = function(ovr){
let img = ovr; let img = ovr;
LOG("show", img.getBPP());
if (ovr.getBPP() == 1) { if (ovr.getBPP() == 1) {
img = ovr.asImage(); img = ovr.asImage();
img.palette = new Uint16Array([_g.theme.fg,_g.theme.bg]); img.palette = new Uint16Array([_g.theme.fg,_g.theme.bg]);
@ -162,7 +165,9 @@ let showMessage = function(ovr, msg) {
drawMessage(ovr, msg); drawMessage(ovr, msg);
}; };
let drawBorder = function(ovr) { let drawBorder = function(img) {
LOG("drawBorder", isQuiet());
if (img) ovr=img;
if (Bangle.isLocked()) if (Bangle.isLocked())
ovr.setColor(ovr.theme.fgH); ovr.setColor(ovr.theme.fgH);
else else
@ -170,7 +175,6 @@ let drawBorder = function(ovr) {
ovr.drawRect(0,0,ovr.getWidth()-1,ovr.getHeight()-1); ovr.drawRect(0,0,ovr.getWidth()-1,ovr.getHeight()-1);
ovr.drawRect(1,1,ovr.getWidth()-2,ovr.getHeight()-2); ovr.drawRect(1,1,ovr.getWidth()-2,ovr.getHeight()-2);
show(ovr); show(ovr);
if (!isQuiet()) Bangle.setLCDPower(1);
}; };
let showCall = function(ovr, msg) { let showCall = function(ovr, msg) {
@ -232,13 +236,6 @@ let next = function(ovr) {
showMessage(ovr, eventQueue[0]); showMessage(ovr, eventQueue[0]);
}; };
let showMapMessage = function(ovr, msg) {
ovr.clearRect(2,2,ovr.getWidth()-3,ovr.getHeight()-3);
drawMessage(ovr, {
body: "Not implemented!"
});
};
let callBuzzTimer = null; let callBuzzTimer = null;
let stopCallBuzz = function() { let stopCallBuzz = function() {
if (callBuzzTimer) { if (callBuzzTimer) {
@ -407,7 +404,7 @@ let main = function(ovr, event) {
if (!lockListener) { if (!lockListener) {
lockListener = function (){ lockListener = function (){
drawBorder(ovr); drawBorder();
}; };
Bangle.on('lock', lockListener); Bangle.on('lock', lockListener);
} }
@ -432,15 +429,22 @@ let main = function(ovr, event) {
let ovr; let ovr;
exports.message = function(type, event) { exports.message = function(type, event) {
LOG("Got message", type, event);
// only handle some event types // only handle some event types
if(!(type=="text" || type == "call")) return; if(!(type=="text" || type == "call")) return;
if(type=="text" && event.id == "nav") return; if(type=="text" && event.id == "nav") return;
if(event.handled) return; if(event.handled) return;
bpp = 4; bpp = 4;
if (process.memory().free < 2000) bpp = 1; if (process.memory().free < LOW_MEM)
bpp = 1;
if (!ovr) { while (process.memory().free < MIN_FREE_MEM && eventQueue.length > 0){
let dropped = eventQueue.pop();
print("Dropped message because of memory constraints", dropped);
}
if (!ovr || ovr.getBPP() != bpp) {
ovr = Graphics.createArrayBuffer(ovrw, ovrh, bpp, { ovr = Graphics.createArrayBuffer(ovrw, ovrh, bpp, {
msb: true msb: true
}); });
@ -456,6 +460,7 @@ exports.message = function(type, event) {
ovr.theme = { fg:0, bg:1, fg2:1, bg2:0, fgH:1, bgH:0 }; ovr.theme = { fg:0, bg:1, fg2:1, bg2:0, fgH:1, bgH:0 };
main(ovr, event); main(ovr, event);
if (!isQuiet()) Bangle.setLCDPower(1);
event.handled = true; event.handled = true;
g = _g; g = _g;
}; };

View File

@ -1,7 +1,7 @@
{ {
"id": "messagesoverlay", "id": "messagesoverlay",
"name": "Messages Overlay", "name": "Messages Overlay",
"version": "0.04", "version": "0.06",
"description": "An overlay based implementation of a messages UI (display notifications from iOS and Gadgetbridge/Android)", "description": "An overlay based implementation of a messages UI (display notifications from iOS and Gadgetbridge/Android)",
"icon": "app.png", "icon": "app.png",
"type": "bootloader", "type": "bootloader",

View File

@ -1,3 +1,4 @@
0.01: New App! 0.01: New App!
0.02: Redraw only when seconds change 0.02: Redraw only when seconds change
0.03: Fix typo in redraw check 0.03: Fix typo in redraw check
0.04: Register as clock and implement fast loading

View File

@ -1,29 +1,30 @@
// Code based on the original Mixed Clock // Code based on the original Mixed Clock
{
/* jshint esversion: 6 */ /* jshint esversion: 6 */
var locale = require("locale"); const locale = require("locale");
const Radius = { "center": 7, "hour": 60, "min": 80, "dots": 88 }; const Radius = { "center": 7, "hour": 60, "min": 80, "dots": 88 };
const Center = { "x": 120, "y": 96 }; const Center = { "x": 120, "y": 96 };
const Widths = { hour: 2, minute: 2 }; const Widths = { hour: 2, minute: 2 };
var buf = Graphics.createArrayBuffer(240,192,1,{msb:true}); const buf = Graphics.createArrayBuffer(240,192,1,{msb:true});
var lastDate = new Date(); let timeoutId;
function rotatePoint(x, y, d) { const rotatePoint = function(x, y, d, center, res) {
rad = -1 * d / 180 * Math.PI; "jit";
var sin = Math.sin(rad); const rad = -1 * d / 180 * Math.PI;
var cos = Math.cos(rad); const sin = Math.sin(rad);
xn = ((Center.x + x * cos - y * sin) + 0.5) | 0; const cos = Math.cos(rad);
yn = ((Center.y + x * sin - y * cos) + 0.5) | 0; res[0] = ((center.x + x * cos - y * sin) + 0.5) | 0;
p = [xn, yn]; res[1] = ((center.y + x * sin - y * cos) + 0.5) | 0;
return p; };
}
// from https://github.com/espruino/Espruino/issues/1702 // from https://github.com/espruino/Espruino/issues/1702
function setLineWidth(x1, y1, x2, y2, lw) { const setLineWidth = function(x1, y1, x2, y2, lw) {
var dx = x2 - x1; "ram";
var dy = y2 - y1; let dx = x2 - x1;
var d = Math.sqrt(dx * dx + dy * dy); let dy = y2 - y1;
let d = Math.sqrt(dx * dx + dy * dy);
dx = dx * lw / d; dx = dx * lw / d;
dy = dy * lw / d; dy = dy * lw / d;
@ -44,71 +45,84 @@ function setLineWidth(x1, y1, x2, y2, lw) {
x2 - dy, y2 + dx, x2 - dy, y2 + dx,
x1 - dy, y1 + dx x1 - dy, y1 + dx
]; ];
} };
function drawMixedClock(force) { const drawMixedClock = function() {
var date = new Date(); const date = new Date();
if ((force || Bangle.isLCDOn()) && buf.buffer && date.getSeconds() !== lastDate.getSeconds()) { const dateArray = date.toString().split(" ");
lastDate = date; const isEn = locale.name.startsWith("en");
var dateArray = date.toString().split(" "); let point = [0, 0];
var isEn = locale.name.startsWith("en"); const minute = date.getMinutes();
var point = []; const hour = date.getHours();
var minute = date.getMinutes(); let radius;
var hour = date.getHours();
var radius;
g.reset();
buf.clear();
// draw date
buf.setFont("6x8", 2);
buf.setFontAlign(-1, 0);
buf.drawString(locale.dow(date,true) + ' ', 4, 16, true);
buf.drawString(isEn?(' ' + dateArray[2]):locale.month(date,true), 4, 176, true);
buf.setFontAlign(1, 0);
buf.drawString(isEn?locale.month(date,true):(' ' + dateArray[2]), 237, 16, true);
buf.drawString(dateArray[3], 237, 176, true);
// draw hour and minute dots g.reset();
for (i = 0; i < 60; i++) { buf.clear();
radius = (i % 5) ? 2 : 4;
point = rotatePoint(0, Radius.dots, i * 6);
buf.fillCircle(point[0], point[1], radius);
}
// draw digital time // draw date
buf.setFont("6x8", 3); buf.setFont("6x8", 2);
buf.setFontAlign(0, 0); buf.setFontAlign(-1, 0);
buf.drawString(dateArray[4], 120, 120, true); buf.drawString(locale.dow(date,true) + ' ', 4, 16, true);
buf.drawString(isEn?(' ' + dateArray[2]):locale.month(date,true), 4, 176, true);
buf.setFontAlign(1, 0);
buf.drawString(isEn?locale.month(date,true):(' ' + dateArray[2]), 237, 16, true);
buf.drawString(dateArray[3], 237, 176, true);
// draw new minute hand // draw hour and minute dots
point = rotatePoint(0, Radius.min, minute * 6); for (i = 0; i < 60; i++) {
buf.drawLine(Center.x, Center.y, point[0], point[1]); radius = (i % 5) ? 2 : 4;
buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.minute)); rotatePoint(0, Radius.dots, i * 6, Center, point);
// draw new hour hand buf.fillCircle(point[0], point[1], radius);
point = rotatePoint(0, Radius.hour, hour % 12 * 30 + date.getMinutes() / 2 | 0);
buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.hour));
// draw center
buf.fillCircle(Center.x, Center.y, Radius.center);
g.drawImage({width:buf.getWidth(),height:buf.getHeight(),bpp:1,buffer:buf.buffer},0,24);
} }
}
Bangle.on('lcdPower', function(on) { // draw digital time
if (on) buf.setFont("6x8", 3);
drawMixedClock(true); buf.setFontAlign(0, 0);
Bangle.drawWidgets(); buf.drawString(dateArray[4], 120, 120, true);
});
setInterval(() => drawMixedClock(true), 30000); // force an update every 30s even screen is off // draw new minute hand
rotatePoint(0, Radius.min, minute * 6, Center, point);
buf.drawLine(Center.x, Center.y, point[0], point[1]);
buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.minute));
// draw new hour hand
rotatePoint(0, Radius.hour, hour % 12 * 30 + date.getMinutes() / 2 | 0, Center, point);
buf.fillPoly(setLineWidth(Center.x, Center.y, point[0], point[1], Widths.hour));
// draw center
buf.fillCircle(Center.x, Center.y, Radius.center);
g.drawImage({width:buf.getWidth(),height:buf.getHeight(),bpp:1,buffer:buf.buffer},0,24);
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
const period = (Bangle.isLCDOn() ? 1000 : 60000); // Update every second if display is on else every minute
let timeout = period - (Date.now() % period);
timeoutId = setTimeout(()=>{
timeoutId = undefined;
drawMixedClock();
}, timeout);
};
const onLCDPower = function(on) {
if (on) {
drawMixedClock();
Bangle.drawWidgets();
}
};
Bangle.on('lcdPower', onLCDPower);
Bangle.setUI({mode:"clock", remove:function() {
if (timeoutId !== undefined) {
delete buf.buffer;
clearTimeout(timeoutId);
timeoutId = undefined;
Bangle.removeListener('lcdPower',onLCDPower);
}
}});
g.clear(); g.clear();
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
drawMixedClock(); // immediately draw drawMixedClock(); // immediately draw
setInterval(drawMixedClock, 500); // update twice a second }
// Show launcher when middle button pressed after freeing memory first
setWatch(() => {delete buf.buffer; Bangle.showLauncher()}, BTN2, {repeat:false,edge:"falling"});

View File

@ -1,7 +1,7 @@
{ {
"id": "miclock2", "id": "miclock2",
"name": "Mixed Clock 2", "name": "Mixed Clock 2",
"version": "0.03", "version": "0.04",
"description": "White color variant of the Mixed Clock with thicker clock hands for better readability in the bright sunlight, extra space under the clock for widgets and seconds in the digital clock.", "description": "White color variant of the Mixed Clock with thicker clock hands for better readability in the bright sunlight, extra space under the clock for widgets and seconds in the digital clock.",
"icon": "clock-mixed.png", "icon": "clock-mixed.png",
"type": "clock", "type": "clock",

View File

@ -3,3 +3,6 @@
0.03: Use default Bangle formatter for booleans 0.03: Use default Bangle formatter for booleans
0.04: Remove copied sched alarm.js & import newer features (oneshot alarms) 0.04: Remove copied sched alarm.js & import newer features (oneshot alarms)
0.05: Fix creating new alarms/timers in hardmode 0.05: Fix creating new alarms/timers in hardmode
0.06: Support fastloading
0.07: Fix fastloading support - ensure drag handler's restored after
menu display/fastload removes it

View File

@ -1,11 +1,12 @@
{
Bangle.loadWidgets(); Bangle.loadWidgets();
Bangle.drawWidgets(); Bangle.drawWidgets();
var R = Bangle.appRect; const R = Bangle.appRect;
var layer; let layer;
var drag; let drag;
var timerInt1 = []; let timerInt1 = [];
var timerInt2 = []; let timerInt2 = [];
function getCurrentTime() { function getCurrentTime() {
let time = new Date(); let time = new Date();
@ -66,8 +67,7 @@ function setHM(alarm, on) {
function drawTimers() { function drawTimers() {
layer = 0; layer = 0;
var timers = require("sched").getAlarms().filter(a => a.timer && a.appid == "multitimer"); const timers = require("sched").getAlarms().filter(a => a.timer && a.appid == "multitimer");
var alarms = require("sched").getAlarms();
function updateTimers(idx) { function updateTimers(idx) {
if (!timerInt1[idx]) timerInt1[idx] = setTimeout(function() { if (!timerInt1[idx]) timerInt1[idx] = setTimeout(function() {
@ -78,15 +78,15 @@ function drawTimers() {
}, 1000 - (timers[idx].t % 1000)); }, 1000 - (timers[idx].t % 1000));
} }
var s = E.showScroller({ E.showScroller({
h : 40, c : timers.length+2, h : 40, c : timers.length+2,
back : function() {load();}, back : function() {load();},
draw : (idx, r) => { draw : (idx, r) => {
function drawMenuItem(a) { function drawMenuItem(a) {
let msg = "";
g.setClipRect(R.x,R.y,R.x2,R.y2); g.setClipRect(R.x,R.y,R.x2,R.y2);
if (idx > 0 && timers[idx-1].msg) msg = "\n"+(timers[idx-1].msg.length > 10 ? if (idx > 0 && timers[idx-1].msg) msg = "\n"+(timers[idx-1].msg.length > 10 ?
timers[idx-1].msg.substring(0, 10)+"..." : timers[idx-1].msg); timers[idx-1].msg.substring(0, 10)+"..." : timers[idx-1].msg);
else msg = "";
return g.setColor(g.theme.bg2).fillRect({x:r.x+4,y:r.y+2,w:r.w-8, h:r.h-4, r:5}) return g.setColor(g.theme.bg2).fillRect({x:r.x+4,y:r.y+2,w:r.w-8, h:r.h-4, r:5})
.setColor(g.theme.fg2).setFont("6x8:2").setFontAlign(-1,0).drawString(a+msg,r.x+12,r.y+(r.h/2)); .setColor(g.theme.fg2).setFont("6x8:2").setFontAlign(-1,0).drawString(a+msg,r.x+12,r.y+(r.h/2));
} }
@ -112,22 +112,22 @@ function drawTimers() {
else if (idx > 0 && idx < timers.length+1) timerMenu(idx-1); else if (idx > 0 && idx < timers.length+1) timerMenu(idx-1);
} }
}); });
setUI();
} }
function timerMenu(idx) { function timerMenu(idx) {
layer = -1; layer = -1;
var timers = require("sched").getAlarms(); const timers = require("sched").getAlarms();
var timerIdx = []; const timerIdx = [];
var j = 0; let a;
for (let i = 0; i < timers.length; i++) { for (let i = 0; i < timers.length; i++) {
if (timers[i].timer && timers[i].appid == "multitimer") { if (timers[i].timer && timers[i].appid == "multitimer") {
a = i; a = i;
timerIdx.push(a); timerIdx.push(a);
j++;
} }
} }
var a = timers[timerIdx[idx]]; a = timers[timerIdx[idx]];
var msg = "";
function updateTimer() { function updateTimer() {
if (timerInt1[0] == undefined) timerInt1[0] = setTimeout(function() { if (timerInt1[0] == undefined) timerInt1[0] = setTimeout(function() {
@ -138,7 +138,7 @@ function timerMenu(idx) {
}, 1000 - (a.t % 1000)); }, 1000 - (a.t % 1000));
} }
var s = E.showScroller({ E.showScroller({
h : 40, c : 5, h : 40, c : 5,
back : function() { back : function() {
clearInt(); clearInt();
@ -153,6 +153,7 @@ function timerMenu(idx) {
} }
if (i == 0) { if (i == 0) {
let msg = "";
if (a.msg) msg = "\n"+(a.msg.length > 10 ? a.msg.substring(0, 10)+"..." : a.msg); if (a.msg) msg = "\n"+(a.msg.length > 10 ? a.msg.substring(0, 10)+"..." : a.msg);
if (a.on == true) { if (a.on == true) {
drawMenuItem(formatTime(a.t-getCurrentTime())+msg); drawMenuItem(formatTime(a.t-getCurrentTime())+msg);
@ -216,19 +217,17 @@ function timerMenu(idx) {
} }
} }
}); });
setUI();
} }
function editTimer(idx, a) { function editTimer(idx, a) {
layer = -1; layer = -1;
var timers = require("sched").getAlarms().filter(a => a.timer && a.appid == "multitimer"); const timers = require("sched").getAlarms().filter(a => a.timer && a.appid == "multitimer");
var alarms = require("sched").getAlarms(); const alarms = require("sched").getAlarms();
var timerIdx = []; const timerIdx = [];
var j = 0;
for (let i = 0; i < alarms.length; i++) { for (let i = 0; i < alarms.length; i++) {
if (alarms[i].timer && alarms[i].appid == "multitimer") { if (alarms[i].timer && alarms[i].appid == "multitimer") {
b = i; timerIdx.push(i);
timerIdx.push(b);
j++;
} }
} }
if (!a) { if (!a) {
@ -238,9 +237,10 @@ function editTimer(idx, a) {
if (!a.data) { if (!a.data) {
a.data = { hm: false }; a.data = { hm: false };
} }
var t = decodeTime(a.timer); const t = decodeTime(a.timer);
function editMsg(idx, a) { function editMsg(idx, a) {
let msg;
g.clear(); g.clear();
idx < 0 ? msg = "" : msg = a.msg; idx < 0 ? msg = "" : msg = a.msg;
require("textinput").input({text:msg}).then(result => { require("textinput").input({text:msg}).then(result => {
@ -256,9 +256,10 @@ function editTimer(idx, a) {
E.showAlert("Must install keyboard app").then(function() { E.showAlert("Must install keyboard app").then(function() {
editTimer(idx, a); editTimer(idx, a);
}); });
setUI();
} }
var menu = { const menu = {
"": { "title": "Timer" }, "": { "title": "Timer" },
"< Back": () => { "< Back": () => {
a.t = getCurrentTime() + a.timer; a.t = getCurrentTime() + a.timer;
@ -312,7 +313,7 @@ function editTimer(idx, a) {
value: !a.msg ? "" : a.msg.length > 6 ? a.msg.substring(0, 6)+"..." : a.msg, value: !a.msg ? "" : a.msg.length > 6 ? a.msg.substring(0, 6)+"..." : a.msg,
//menu glitch? setTimeout required here //menu glitch? setTimeout required here
onchange: () => { onchange: () => {
var kbapp = require("Storage").read("textinput"); const kbapp = require("Storage").read("textinput");
if (kbapp != undefined) setTimeout(editMsg, 0, idx, a); if (kbapp != undefined) setTimeout(editMsg, 0, idx, a);
else setTimeout(kbAlert, 0); else setTimeout(kbAlert, 0);
} }
@ -324,11 +325,12 @@ function editTimer(idx, a) {
}; };
E.showMenu(menu); E.showMenu(menu);
setUI();
} }
function drawSw() { function drawSw() {
layer = 1; layer = 1;
var sw = require("Storage").readJSON("multitimer.json", true) || []; const sw = require("Storage").readJSON("multitimer.json", true) || [];
function updateTimers(idx) { function updateTimers(idx) {
if (!timerInt1[idx]) timerInt1[idx] = setTimeout(function() { if (!timerInt1[idx]) timerInt1[idx] = setTimeout(function() {
@ -339,12 +341,13 @@ function drawSw() {
}, 1000 - (sw[idx].t % 1000)); }, 1000 - (sw[idx].t % 1000));
} }
var s = E.showScroller({ E.showScroller({
h : 40, c : sw.length+2, h : 40, c : sw.length+2,
back : function() {load();}, back : function() {load();},
draw : (idx, r) => { draw : (idx, r) => {
function drawMenuItem(a) { function drawMenuItem(a) {
let msg;
g.setClipRect(R.x,R.y,R.x2,R.y2); g.setClipRect(R.x,R.y,R.x2,R.y2);
if (idx > 0 && sw[idx-1].msg) msg = "\n"+(sw[idx-1].msg.length > 10 ? if (idx > 0 && sw[idx-1].msg) msg = "\n"+(sw[idx-1].msg.length > 10 ?
sw[idx-1].msg.substring(0, 10)+"..." : sw[idx-1].msg); sw[idx-1].msg.substring(0, 10)+"..." : sw[idx-1].msg);
@ -374,11 +377,12 @@ function drawSw() {
else if (idx > 0 && idx < sw.length+1) swMenu(idx-1); else if (idx > 0 && idx < sw.length+1) swMenu(idx-1);
} }
}); });
setUI();
} }
function swMenu(idx, a) { function swMenu(idx, a) {
layer = -1; layer = -1;
var sw = require("Storage").readJSON("multitimer.json", true) || []; const sw = require("Storage").readJSON("multitimer.json", true) || [];
if (sw[idx]) a = sw[idx]; if (sw[idx]) a = sw[idx];
else { else {
a = {"t" : 0, "on" : false, "msg" : ""}; a = {"t" : 0, "on" : false, "msg" : ""};
@ -397,7 +401,7 @@ function swMenu(idx, a) {
function editMsg(idx, a) { function editMsg(idx, a) {
g.clear(); g.clear();
msg = a.msg; const msg = a.msg;
require("textinput").input({text:msg}).then(result => { require("textinput").input({text:msg}).then(result => {
if (result != "") { if (result != "") {
a.msg = result; a.msg = result;
@ -413,9 +417,10 @@ function swMenu(idx, a) {
E.showAlert("Must install keyboard app").then(function() { E.showAlert("Must install keyboard app").then(function() {
swMenu(idx, a); swMenu(idx, a);
}); });
setUI();
} }
var s = E.showScroller({ E.showScroller({
h : 40, c : 5, h : 40, c : 5,
back : function() { back : function() {
clearInt(); clearInt();
@ -430,6 +435,7 @@ function swMenu(idx, a) {
} }
if (i == 0) { if (i == 0) {
let msg;
if (a.msg) msg = "\n"+(a.msg.length > 10 ? a.msg.substring(0, 10)+"..." : a.msg); if (a.msg) msg = "\n"+(a.msg.length > 10 ? a.msg.substring(0, 10)+"..." : a.msg);
else msg = ""; else msg = "";
if (a.on == true) { if (a.on == true) {
@ -482,7 +488,7 @@ function swMenu(idx, a) {
//edit message //edit message
if (i == 3) { if (i == 3) {
clearInt(); clearInt();
var kbapp = require("Storage").read("textinput"); const kbapp = require("Storage").read("textinput");
if (kbapp != undefined) editMsg(idx, a); if (kbapp != undefined) editMsg(idx, a);
else kbAlert(); else kbAlert();
} }
@ -495,21 +501,22 @@ function swMenu(idx, a) {
} }
} }
}); });
setUI();
} }
function drawAlarms() { function drawAlarms() {
layer = 2; layer = 2;
var alarms = require("sched").getAlarms().filter(a => !a.timer); const alarms = require("sched").getAlarms().filter(a => !a.timer);
var s = E.showScroller({ E.showScroller({
h : 40, c : alarms.length+2, h : 40, c : alarms.length+2,
back : function() {load();}, back : function() {load();},
draw : (idx, r) => { draw : (idx, r) => {
function drawMenuItem(a) { function drawMenuItem(a) {
g.setClipRect(R.x,R.y,R.x2,R.y2); g.setClipRect(R.x,R.y,R.x2,R.y2);
var on = ""; let on = "";
var dow = ""; let dow = "";
if (idx > 0 && alarms[idx-1].on == true) on = " - on"; if (idx > 0 && alarms[idx-1].on == true) on = " - on";
else if (idx > 0 && alarms[idx-1].on == false) on = " - off"; else if (idx > 0 && alarms[idx-1].on == false) on = " - off";
if (idx > 0 && idx < alarms.length+1) dow = "\n"+"SMTWTFS".split("").map((d,n)=>alarms[idx-1].dow&(1<<n)?d:".").join(""); if (idx > 0 && idx < alarms.length+1) dow = "\n"+"SMTWTFS".split("").map((d,n)=>alarms[idx-1].dow&(1<<n)?d:".").join("");
@ -525,7 +532,7 @@ function drawAlarms() {
.setColor(g.theme.fg).setFont("6x8:2").setFontAlign(0,0).drawString("< Swipe >",r.x+(r.w/2),r.y+(r.h/2)); .setColor(g.theme.fg).setFont("6x8:2").setFontAlign(0,0).drawString("< Swipe >",r.x+(r.w/2),r.y+(r.h/2));
} }
else if (idx > 0 && idx < alarms.length+1){ else if (idx > 0 && idx < alarms.length+1){
var str = formatTime(alarms[idx-1].t); const str = formatTime(alarms[idx-1].t);
drawMenuItem(str.slice(0, -3)); drawMenuItem(str.slice(0, -3));
} }
}, },
@ -535,6 +542,7 @@ function drawAlarms() {
else if (idx > 0 && idx < alarms.length+1) editAlarm(idx-1); else if (idx > 0 && idx < alarms.length+1) editAlarm(idx-1);
} }
}); });
setUI();
} }
function editDOW(dow, onchange) { function editDOW(dow, onchange) {
@ -542,26 +550,24 @@ function editDOW(dow, onchange) {
'': { 'title': 'Days of Week' }, '': { 'title': 'Days of Week' },
'< Back' : () => onchange(dow) '< Back' : () => onchange(dow)
}; };
for (var i = 0; i < 7; i++) (i => { for (let i = 0; i < 7; i++) (i => {
var dayOfWeek = require("locale").dow({ getDay: () => i }); const dayOfWeek = require("locale").dow({ getDay: () => i });
menu[dayOfWeek] = { menu[dayOfWeek] = {
value: !!(dow&(1<<i)), value: !!(dow&(1<<i)),
onchange: v => v ? dow |= 1<<i : dow &= ~(1<<i), onchange: v => v ? dow |= 1<<i : dow &= ~(1<<i),
}; };
})(i); })(i);
E.showMenu(menu); E.showMenu(menu);
setUI();
} }
function editAlarm(idx, a) { function editAlarm(idx, a) {
layer = -1; layer = -1;
var alarms = require("sched").getAlarms(); const alarms = require("sched").getAlarms();
var alarmIdx = []; const alarmIdx = [];
var j = 0;
for (let i = 0; i < alarms.length; i++) { for (let i = 0; i < alarms.length; i++) {
if (!alarms[i].timer) { if (!alarms[i].timer) {
b = i; alarmIdx.push(i);
alarmIdx.push(b);
j++;
} }
} }
if (!a) { if (!a) {
@ -571,9 +577,10 @@ function editAlarm(idx, a) {
if (!a.data) { if (!a.data) {
a.data = { hm: false }; a.data = { hm: false };
} }
var t = decodeTime(a.t); const t = decodeTime(a.t);
function editMsg(idx, a) { function editMsg(idx, a) {
let msg;
g.clear(); g.clear();
idx < 0 ? msg = "" : msg = a.msg; idx < 0 ? msg = "" : msg = a.msg;
require("textinput").input({text:msg}).then(result => { require("textinput").input({text:msg}).then(result => {
@ -589,9 +596,10 @@ function editAlarm(idx, a) {
E.showAlert("Must install keyboard app").then(function() { E.showAlert("Must install keyboard app").then(function() {
editAlarm(idx, a); editAlarm(idx, a);
}); });
setUI();
} }
var menu = { const menu = {
"": { "title": "Alarm" }, "": { "title": "Alarm" },
"< Back": () => { "< Back": () => {
if (idx >= 0) alarms[alarmIdx[idx]] = a; if (idx >= 0) alarms[alarmIdx[idx]] = a;
@ -646,7 +654,7 @@ function editAlarm(idx, a) {
value: !a.msg ? "" : a.msg.length > 6 ? a.msg.substring(0, 6)+"..." : a.msg, value: !a.msg ? "" : a.msg.length > 6 ? a.msg.substring(0, 6)+"..." : a.msg,
//menu glitch? setTimeout required here //menu glitch? setTimeout required here
onchange: () => { onchange: () => {
var kbapp = require("Storage").read("textinput"); const kbapp = require("Storage").read("textinput");
if (kbapp != undefined) setTimeout(editMsg, 0, idx, a); if (kbapp != undefined) setTimeout(editMsg, 0, idx, a);
else setTimeout(kbAlert, 0); else setTimeout(kbAlert, 0);
} }
@ -662,11 +670,21 @@ function editAlarm(idx, a) {
}; };
E.showMenu(menu); E.showMenu(menu);
setUI();
} }
drawTimers(); function setUI() {
// E.showMenu/E.showScroller/E.showAlert call setUI, so we register onDrag() separately
// and tack on uiRemove after the fact to avoid interfering
Bangle.on("drag", onDrag);
Bangle.uiRemove = () => {
Bangle.removeListener("drag", onDrag);
Object.values(timerInt1).forEach(clearTimeout);
Object.values(timerInt2).forEach(clearTimeout);
};
}
Bangle.on("drag", e=>{ function onDrag(e) {
if (layer < 0) return; if (layer < 0) return;
if (!drag) { // start dragging if (!drag) { // start dragging
drag = {x: e.x, y: e.y}; drag = {x: e.x, y: e.y};
@ -687,4 +705,7 @@ Bangle.on("drag", e=>{
else if (layer == 2) drawAlarms(); else if (layer == 2) drawAlarms();
} }
} }
}); }
drawTimers();
}

View File

@ -1,7 +1,7 @@
{ {
"id": "multitimer", "id": "multitimer",
"name": "Multi Timer", "name": "Multi Timer",
"version": "0.05", "version": "0.07",
"description": "Set timers and chronographs (stopwatches) and watch them count down in real time. Pause, create, edit, and delete timers and chronos, and add custom labels/messages. Also sets alarms.", "description": "Set timers and chronographs (stopwatches) and watch them count down in real time. Pause, create, edit, and delete timers and chronos, and add custom labels/messages. Also sets alarms.",
"icon": "app.png", "icon": "app.png",
"screenshots": [ "screenshots": [

View File

@ -16,11 +16,16 @@
Support for zooming in on map Support for zooming in on map
Satellite count moved to widget bar to leave more room for the map Satellite count moved to widget bar to leave more room for the map
0.15: Make track drawing an option (default off) 0.15: Make track drawing an option (default off)
0.16: Draw waypoints, too. 0.16: Draw waypoints, too
0.17: With new Recorder app allow track to be drawn in the background 0.17: With new Recorder app allow track to be drawn in the background
Switch tile layer URL for faster/more reliable map tiles Switch tile layer URL for faster/more reliable map tiles
0.18: Prefer map with highest resolution 0.18: Prefer map with highest resolution
0.19: Remember latitude, longitude & scale 0.19: Remember latitude, longitude & scale
0.20: Make Satellite counter widget 24px wide (was 48) 0.20: Make Satellite counter widget 24px wide (was 48)
Move 'Center GPS' to the top of the menu Move 'Center GPS' to the top of the menu
If 'Recorder' app installed, add a 'Record' menu item If 'Recorder' app installed, add a 'Record' menu item
0.21: Draw a current position marker (Bangle.js 2 only)
Enable/Disable previous position marker in new setting "Draw cont. position"
0.22: Replace position marker with direction arrow
0.23: Bugfix: Enable Compass if needed
0.24: Allow zooming by clicking the screen

View File

@ -29,6 +29,7 @@ and marks the path that you've been travelling (if enabled), and
displays waypoints in the watch (if dependencies exist). displays waypoints in the watch (if dependencies exist).
* Drag on the screen to move the map * Drag on the screen to move the map
* Click bottom left to zoom in, bottom right to zoom out
* Press the button to bring up a menu, where you can zoom, go to GPS location, * Press the button to bring up a menu, where you can zoom, go to GPS location,
put the map back in its default location, or choose whether to draw the currently put the map back in its default location, or choose whether to draw the currently
recording GPS track (from the `Recorder` app). recording GPS track (from the `Recorder` app).

View File

@ -7,6 +7,15 @@ var hasScrolled = false;
var settings = require("Storage").readJSON("openstmap.json",1)||{}; var settings = require("Storage").readJSON("openstmap.json",1)||{};
var plotTrack; var plotTrack;
let checkMapPos = false; // Do we need to check the if the coordinates we have are valid let checkMapPos = false; // Do we need to check the if the coordinates we have are valid
var startDrag = 0;
if (Bangle.setLCDOverlay) {
// Icon for current location+direction: https://icons8.com/icon/11932/gps 24x24, 1 Bit + transparency + inverted
var imgLoc = require("heatshrink").decompress(atob("jEYwINLAQk8AQl+AQn/AQcB/+AAQUD//AAQUH//gAQUP//wAQUf//4j8AvA9IA=="));
// overlay buffer for current location, a bit bigger then image so we can rotate
const ovSize = Math.ceil(Math.sqrt(imgLoc[0]*imgLoc[0]+imgLoc[1]*imgLoc[1]));
var ovLoc = Graphics.createArrayBuffer(ovSize,ovSize,imgLoc[2] & 0x7f,{msb:true});
}
if (settings.lat !== undefined && settings.lon !== undefined && settings.scale !== undefined) { if (settings.lat !== undefined && settings.lon !== undefined && settings.scale !== undefined) {
// restore last view // restore last view
@ -15,6 +24,9 @@ if (settings.lat !== undefined && settings.lon !== undefined && settings.scale !
m.scale = settings.scale; m.scale = settings.scale;
checkMapPos = true; checkMapPos = true;
} }
if (settings.dirSrc === undefined) {
settings.dirSrc = 1; // Default=GPS
}
// Redraw the whole page // Redraw the whole page
function redraw() { function redraw() {
@ -27,8 +39,10 @@ function redraw() {
m.scale = m.map.scale; m.scale = m.map.scale;
m.draw(); m.draw();
} }
checkMapPos = false;
drawPOI(); drawPOI();
drawMarker(); drawMarker();
drawLocation();
// if track drawing is enabled... // if track drawing is enabled...
if (settings.drawTrack) { if (settings.drawTrack) {
if (HASWIDGETS && WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) { if (HASWIDGETS && WIDGETS["gpsrec"] && WIDGETS["gpsrec"].plotTrack) {
@ -65,20 +79,63 @@ function drawPOI() {
}) })
} }
// Draw the marker for where we are function isInside(rect, e, w, h) {
return e.x-w/2>=rect.x && e.x+w/2<rect.x+rect.w
&& e.y-h/2>=rect.y && e.y+h/2<=rect.y+rect.h;
}
// Draw the location & direction marker for where we are
function drawMarker() { function drawMarker() {
if (!fix.fix) return; if (!fix.fix || !settings.drawMarker) return;
var p = m.latLonToXY(fix.lat, fix.lon); var p = m.latLonToXY(fix.lat, fix.lon);
g.setColor(1,0,0); if (isInside(R, p, 4, 4)) { // avoid drawing over widget area
g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2); g.setColor(1,0,0);
g.fillRect(p.x-2, p.y-2, p.x+2, p.y+2);
}
}
// Draw current location+direction with LCD Overlay (Bangle.js 2 only)
function drawLocation() {
if (!Bangle.setLCDOverlay) {
return; // Overlay not supported
}
if (!fix.fix || !mapVisible || settings.dirSrc === 0) {
if (this.hasOverlay) {
Bangle.setLCDOverlay(); // clear if map is not visible or no fix
this.hasOverlay = false;
}
return;
}
var p = m.latLonToXY(fix.lat, fix.lon);
ovLoc.clear();
if (isInside(R, p, ovLoc.getWidth(), ovLoc.getHeight())) { // avoid drawing over widget area
const angle = settings.dirSrc === 1 ? fix.course : Bangle.getCompass().heading;
if (!isNaN(angle)) {
ovLoc.drawImage(imgLoc, ovLoc.getWidth()/2, ovLoc.getHeight()/2, {rotate: angle*Math.PI/180});
}
}
Bangle.setLCDOverlay({width:ovLoc.getWidth(), height:ovLoc.getHeight(),
bpp:ovLoc.getBPP(), transparent:0,
palette:new Uint16Array([0, g.toColor("#00F")]),
buffer:ovLoc.buffer
}, p.x-ovLoc.getWidth()/2, p.y-ovLoc.getHeight()/2);
this.hasOverlay = true;
} }
Bangle.on('GPS',function(f) { Bangle.on('GPS',function(f) {
fix=f; fix=f;
if (HASWIDGETS && WIDGETS["sats"]) WIDGETS["sats"].draw(WIDGETS["sats"]); if (HASWIDGETS && WIDGETS["sats"]) WIDGETS["sats"].draw(WIDGETS["sats"]);
if (mapVisible) drawMarker(); if (mapVisible) {
drawMarker();
drawLocation();
}
}); });
Bangle.setGPSPower(1, "app"); Bangle.setGPSPower(1, "app");
Bangle.setCompassPower(settings.dirSrc === 2, "openstmap");
if (HASWIDGETS) { if (HASWIDGETS) {
Bangle.loadWidgets(); Bangle.loadWidgets();
@ -105,6 +162,7 @@ function showMenu() {
if (plotTrack && plotTrack.stop) if (plotTrack && plotTrack.stop)
plotTrack.stop(); plotTrack.stop();
mapVisible = false; mapVisible = false;
drawLocation();
var menu = { var menu = {
"":{title:/*LANG*/"Map"}, "":{title:/*LANG*/"Map"},
"< Back": ()=> showMap(), "< Back": ()=> showMap(),
@ -128,13 +186,36 @@ function showMenu() {
value : !!settings.drawTrack, value : !!settings.drawTrack,
onchange : v => { settings.drawTrack=v; writeSettings(); } onchange : v => { settings.drawTrack=v; writeSettings(); }
}, },
/*LANG*/"Center Map": () =>{ /*LANG*/"Draw cont. position": {
value : !!settings.drawMarker,
onchange : v => { settings.drawMarker=v; writeSettings(); }
},
});
if (Bangle.setLCDOverlay) {
menu[/*LANG*/"Direction source"] = {
value: settings.dirSrc,
min: 0, max: 2,
format: v => [/*LANG*/"None", /*LANG*/"GPS", /*LANG*/"Compass"][v],
onchange: v => {
settings.dirSrc = v;
Bangle.setCompassPower(settings.dirSrc === 2, "openstmap");
writeSettings();
}
};
menu[/*LANG*/"Reset compass"] = () => {
Bangle.resetCompass();
showMap();
};
}
menu[/*LANG*/"Center Map"] = () =>{
m.lat = m.map.lat; m.lat = m.map.lat;
m.lon = m.map.lon; m.lon = m.map.lon;
m.scale = m.map.scale; m.scale = m.map.scale;
showMap(); showMap();
} };
});
// If we have the recorder widget, add a menu item to start/stop recording // If we have the recorder widget, add a menu item to start/stop recording
if (WIDGETS.recorder) { if (WIDGETS.recorder) {
menu[/*LANG*/"Record"] = { menu[/*LANG*/"Record"] = {
@ -145,6 +226,7 @@ function showMenu() {
} }
}; };
} }
menu[/*LANG*/"Exit"] = () => load();
E.showMenu(menu); E.showMenu(menu);
} }
@ -155,13 +237,28 @@ function showMap() {
Bangle.setUI({mode:"custom",drag:e=>{ Bangle.setUI({mode:"custom",drag:e=>{
if (plotTrack && plotTrack.stop) plotTrack.stop(); if (plotTrack && plotTrack.stop) plotTrack.stop();
if (e.b) { if (e.b) {
if (!startDrag)
startDrag = getTime();
g.setClipRect(R.x,R.y,R.x2,R.y2); g.setClipRect(R.x,R.y,R.x2,R.y2);
g.scroll(e.dx,e.dy); g.scroll(e.dx,e.dy);
m.scroll(e.dx,e.dy); m.scroll(e.dx,e.dy);
g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1); g.setClipRect(0,0,g.getWidth()-1,g.getHeight()-1);
hasScrolled = true; hasScrolled = true;
drawLocation();
} else if (hasScrolled) { } else if (hasScrolled) {
delta = getTime() - startDrag;
startDrag = 0;
hasScrolled = false; hasScrolled = false;
if (delta < 0.2) {
if (e.y > g.getHeight() / 2) {
if (e.x < g.getWidth() / 2) {
m.scale /= 2;
} else {
m.scale *= 2;
}
}
g.reset().clearRect(R);
}
redraw(); redraw();
} }
}, btn: () => showMenu() }); }, btn: () => showMenu() });

View File

@ -2,7 +2,7 @@
"id": "openstmap", "id": "openstmap",
"name": "OpenStreetMap", "name": "OpenStreetMap",
"shortName": "OpenStMap", "shortName": "OpenStMap",
"version": "0.20", "version": "0.24",
"description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps", "description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps",
"readme": "README.md", "readme": "README.md",
"icon": "app.png", "icon": "app.png",

Some files were not shown because too many files have changed in this diff Show More