Merge branch 'master' of github.com:nxdefiant/BangleApps into widbaroalarm
12
android.html
|
@ -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>
|
||||||
|
|
|
@ -44,3 +44,6 @@
|
||||||
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
|
||||||
|
0.43: New settings: Show confirm, Show Overflow, Show Type.
|
||||||
|
0.44: Add "delete timer after expiration" setting to events.
|
||||||
|
|
|
@ -13,7 +13,7 @@ It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master
|
||||||
- `Repeat` → Select when the alarm will fire. You can select a predefined option (_Once_, _Every Day_, _Workdays_ or _Weekends_ or you can configure the days freely)
|
- `Repeat` → Select when the alarm will fire. You can select a predefined option (_Once_, _Every Day_, _Workdays_ or _Weekends_ or you can configure the days freely)
|
||||||
- `New Timer` → Configure a new timer (triggered based on amount of time elapsed in hours/minutes/seconds)
|
- `New Timer` → Configure a new timer (triggered based on amount of time elapsed in hours/minutes/seconds)
|
||||||
- `New Event` → Configure a new event (triggered based on time and date)
|
- `New Event` → Configure a new event (triggered based on time and date)
|
||||||
- `Repeat` → Alarm can be be fired only once or repeated (every X number of _days_, _weeks_, _months_ or _years_)
|
- `Repeat` → Alarm can be fired only once or repeated (every X number of _days_, _weeks_, _months_ or _years_)
|
||||||
- `Advanced`
|
- `Advanced`
|
||||||
- `Scheduler settings` → Open the [Scheduler](https://github.com/espruino/BangleApps/tree/master/apps/sched) settings page, see its [README](https://github.com/espruino/BangleApps/blob/master/apps/sched/README.md) for details
|
- `Scheduler settings` → Open the [Scheduler](https://github.com/espruino/BangleApps/tree/master/apps/sched) settings page, see its [README](https://github.com/espruino/BangleApps/blob/master/apps/sched/README.md) for details
|
||||||
- `Enable All` → Enable _all_ disabled alarms & timers
|
- `Enable All` → Enable _all_ disabled alarms & timers
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
Bangle.drawWidgets();
|
Bangle.drawWidgets();
|
||||||
|
|
||||||
|
const settings = Object.assign({
|
||||||
|
showConfirm : true,
|
||||||
|
showAutoSnooze : true,
|
||||||
|
showHidden : true
|
||||||
|
}, require('Storage').readJSON('alarm.json',1)||{});
|
||||||
// 0 = Sunday (default), 1 = Monday
|
// 0 = Sunday (default), 1 = Monday
|
||||||
const firstDayOfWeek = (require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0;
|
const firstDayOfWeek = (require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0;
|
||||||
const WORKDAYS = 62;
|
const WORKDAYS = 62;
|
||||||
|
@ -51,12 +56,14 @@ function getLabel(e) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function trimLabel(label, maxLength) {
|
function trimLabel(label, maxLength) {
|
||||||
|
if(settings.showOverflow) return label;
|
||||||
return (label.length > maxLength
|
return (label.length > maxLength
|
||||||
? label.substring(0,maxLength-3) + "..."
|
? label.substring(0,maxLength-3) + "..."
|
||||||
: label.substring(0,maxLength));
|
: label.substring(0,maxLength));
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatAlarmMessage(msg) {
|
function formatAlarmProperty(msg) {
|
||||||
|
if(settings.showOverflow) return msg;
|
||||||
if (msg == null) {
|
if (msg == null) {
|
||||||
return msg;
|
return msg;
|
||||||
} else if (msg.length > 7) {
|
} else if (msg.length > 7) {
|
||||||
|
@ -99,6 +106,9 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
|
||||||
var isNew = alarmIndex === undefined;
|
var isNew = alarmIndex === undefined;
|
||||||
|
|
||||||
var alarm = require("sched").newDefaultAlarm();
|
var alarm = require("sched").newDefaultAlarm();
|
||||||
|
if (withDate || selectedAlarm.date) {
|
||||||
|
alarm.del = require("sched").getSettings().defaultDeleteExpiredTimers;
|
||||||
|
}
|
||||||
alarm.dow = handleFirstDayOfWeek(alarm.dow);
|
alarm.dow = handleFirstDayOfWeek(alarm.dow);
|
||||||
|
|
||||||
if (selectedAlarm) {
|
if (selectedAlarm) {
|
||||||
|
@ -155,7 +165,7 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
|
||||||
},
|
},
|
||||||
/*LANG*/"Message": {
|
/*LANG*/"Message": {
|
||||||
value: alarm.msg,
|
value: alarm.msg,
|
||||||
format: formatAlarmMessage,
|
format: formatAlarmProperty,
|
||||||
onchange: () => {
|
onchange: () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
keyboard.input({text:alarm.msg}).then(result => {
|
keyboard.input({text:alarm.msg}).then(result => {
|
||||||
|
@ -166,6 +176,19 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/*LANG*/"Group": {
|
||||||
|
value: alarm.group,
|
||||||
|
format: formatAlarmProperty,
|
||||||
|
onchange: () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
keyboard.input({text:alarm.group}).then(result => {
|
||||||
|
alarm.group = result;
|
||||||
|
prepareAlarmForSave(alarm, alarmIndex, time, date, true);
|
||||||
|
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex, withDate);
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
/*LANG*/"Enabled": {
|
/*LANG*/"Enabled": {
|
||||||
value: alarm.on,
|
value: alarm.on,
|
||||||
onchange: v => alarm.on = v
|
onchange: v => alarm.on = v
|
||||||
|
@ -173,6 +196,9 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
|
||||||
/*LANG*/"Repeat": {
|
/*LANG*/"Repeat": {
|
||||||
value: decodeRepeat(alarm),
|
value: decodeRepeat(alarm),
|
||||||
onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, date || alarm.dow, (repeat, dow) => {
|
onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, date || alarm.dow, (repeat, dow) => {
|
||||||
|
if (repeat) {
|
||||||
|
alarm.del = false; // do not auto delete a repeated alarm
|
||||||
|
}
|
||||||
alarm.rp = repeat;
|
alarm.rp = repeat;
|
||||||
alarm.dow = dow;
|
alarm.dow = dow;
|
||||||
prepareAlarmForSave(alarm, alarmIndex, time, date, true);
|
prepareAlarmForSave(alarm, alarmIndex, time, date, true);
|
||||||
|
@ -184,23 +210,32 @@ function showEditAlarmMenu(selectedAlarm, alarmIndex, withDate) {
|
||||||
value: alarm.as,
|
value: alarm.as,
|
||||||
onchange: v => alarm.as = v
|
onchange: v => alarm.as = v
|
||||||
},
|
},
|
||||||
|
/*LANG*/"Delete After Expiration": {
|
||||||
|
value: alarm.del,
|
||||||
|
onchange: v => alarm.del = v
|
||||||
|
},
|
||||||
/*LANG*/"Hidden": {
|
/*LANG*/"Hidden": {
|
||||||
value: alarm.hidden || false,
|
value: alarm.hidden || false,
|
||||||
onchange: v => alarm.hidden = v
|
onchange: v => alarm.hidden = v
|
||||||
},
|
},
|
||||||
/*LANG*/"Cancel": () => showMainMenu(),
|
/*LANG*/"Cancel": () => showMainMenu(),
|
||||||
/*LANG*/"Confirm": () => {
|
/*LANG*/"Confirm": () => {
|
||||||
prepareAlarmForSave(alarm, alarmIndex, time);
|
prepareAlarmForSave(alarm, alarmIndex, time, date);
|
||||||
saveAndReload();
|
saveAndReload();
|
||||||
showMainMenu();
|
showMainMenu();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!keyboard) delete menu[/*LANG*/"Message"];
|
if (!keyboard) delete menu[/*LANG*/"Message"];
|
||||||
|
if (!keyboard || !settings.showGroup) delete menu[/*LANG*/"Group"];
|
||||||
|
if (!settings.showConfirm) delete menu[/*LANG*/"Confirm"];
|
||||||
|
if (!settings.showAutoSnooze) delete menu[/*LANG*/"Auto Snooze"];
|
||||||
|
if (!settings.showHidden) delete menu[/*LANG*/"Hidden"];
|
||||||
if (!alarm.date) {
|
if (!alarm.date) {
|
||||||
delete menu[/*LANG*/"Day"];
|
delete menu[/*LANG*/"Day"];
|
||||||
delete menu[/*LANG*/"Month"];
|
delete menu[/*LANG*/"Month"];
|
||||||
delete menu[/*LANG*/"Year"];
|
delete menu[/*LANG*/"Year"];
|
||||||
|
delete menu[/*LANG*/"Delete After Expiration"];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isNew) {
|
if (!isNew) {
|
||||||
|
@ -259,7 +294,6 @@ function decodeRepeat(alarm) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function showEditRepeatMenu(repeat, day, dowChangeCallback) {
|
function showEditRepeatMenu(repeat, day, dowChangeCallback) {
|
||||||
var originalRepeat = repeat;
|
|
||||||
var dow;
|
var dow;
|
||||||
|
|
||||||
const menu = {
|
const menu = {
|
||||||
|
@ -292,26 +326,32 @@ function showEditRepeatMenu(repeat, day, dowChangeCallback) {
|
||||||
},
|
},
|
||||||
/*LANG*/"Custom": {
|
/*LANG*/"Custom": {
|
||||||
value: isCustom ? decodeRepeat({ rp: true, dow: dow }) : false,
|
value: isCustom ? decodeRepeat({ rp: true, dow: dow }) : false,
|
||||||
onchange: () => setTimeout(showCustomDaysMenu, 10, dow, dowChangeCallback, originalRepeat, originalDow)
|
onchange: () => setTimeout(showCustomDaysMenu, 10, dow, dowChangeCallback, repeat, originalDow)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// var date = day; // eventually: detect day of date and configure a repeat e.g. 3rd Monday of Month
|
// var date = day; // eventually: detect day of date and configure a repeat e.g. 3rd Monday of Month
|
||||||
dow = EVERY_DAY;
|
dow = EVERY_DAY;
|
||||||
repeat = repeat || {interval: "month", num: 1};
|
const repeatObj = repeat || {interval: "month", num: 1};
|
||||||
|
|
||||||
restOfMenu = {
|
restOfMenu = {
|
||||||
/*LANG*/"Every": {
|
/*LANG*/"Every": {
|
||||||
value: repeat.num,
|
value: repeatObj.num,
|
||||||
min: 1,
|
min: 1,
|
||||||
onchange: v => repeat.num = v
|
onchange: v => {
|
||||||
|
repeat = repeatObj;
|
||||||
|
repeat.num = v;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
/*LANG*/"Interval": {
|
/*LANG*/"Interval": {
|
||||||
value: INTERVALS.indexOf(repeat.interval),
|
value: INTERVALS.indexOf(repeatObj.interval),
|
||||||
format: v => INTERVAL_LABELS[v],
|
format: v => INTERVAL_LABELS[v],
|
||||||
min: 0,
|
min: 0,
|
||||||
max: INTERVALS.length - 1,
|
max: INTERVALS.length - 1,
|
||||||
onchange: v => repeat.interval = INTERVALS[v]
|
onchange: v => {
|
||||||
|
repeat = repeatObj;
|
||||||
|
repeat.interval = INTERVALS[v];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -387,7 +427,7 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
|
||||||
},
|
},
|
||||||
/*LANG*/"Message": {
|
/*LANG*/"Message": {
|
||||||
value: timer.msg,
|
value: timer.msg,
|
||||||
format: formatAlarmMessage,
|
format: formatAlarmProperty,
|
||||||
onchange: () => {
|
onchange: () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
keyboard.input({text:timer.msg}).then(result => {
|
keyboard.input({text:timer.msg}).then(result => {
|
||||||
|
@ -420,6 +460,8 @@ function showEditTimerMenu(selectedTimer, timerIndex) {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!keyboard) delete menu[/*LANG*/"Message"];
|
if (!keyboard) delete menu[/*LANG*/"Message"];
|
||||||
|
if (!settings.showConfirm) delete menu[/*LANG*/"Confirm"];
|
||||||
|
if (!settings.showHidden) delete menu[/*LANG*/"Hidden"];
|
||||||
if (!isNew) {
|
if (!isNew) {
|
||||||
menu[/*LANG*/"Delete"] = () => {
|
menu[/*LANG*/"Delete"] = () => {
|
||||||
E.showPrompt(getLabel(timer) + "\n" + /*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => {
|
E.showPrompt(getLabel(timer) + "\n" + /*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "alarm",
|
"id": "alarm",
|
||||||
"name": "Alarms & Timers",
|
"name": "Alarms & Timers",
|
||||||
"shortName": "Alarms",
|
"shortName": "Alarms",
|
||||||
"version": "0.41",
|
"version": "0.44",
|
||||||
"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",
|
||||||
|
@ -11,7 +11,8 @@
|
||||||
"dependencies": { "scheduler":"type", "alarm":"widget" },
|
"dependencies": { "scheduler":"type", "alarm":"widget" },
|
||||||
"storage": [
|
"storage": [
|
||||||
{ "name": "alarm.app.js", "url": "app.js" },
|
{ "name": "alarm.app.js", "url": "app.js" },
|
||||||
{ "name": "alarm.img", "url": "app-icon.js", "evaluate": true }
|
{ "name": "alarm.img", "url": "app-icon.js", "evaluate": true },
|
||||||
|
{ "name": "alarm.settings.js", "url":"settings.js" }
|
||||||
],
|
],
|
||||||
"screenshots": [
|
"screenshots": [
|
||||||
{ "url": "screenshot-1.png" },
|
{ "url": "screenshot-1.png" },
|
||||||
|
@ -25,5 +26,6 @@
|
||||||
{ "url": "screenshot-9.png" },
|
{ "url": "screenshot-9.png" },
|
||||||
{ "url": "screenshot-10.png" },
|
{ "url": "screenshot-10.png" },
|
||||||
{ "url": "screenshot-11.png" }
|
{ "url": "screenshot-11.png" }
|
||||||
]
|
],
|
||||||
|
"data":[ {"name":"alarm.settings.json"} ]
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
(function(back) {
|
||||||
|
let settings = Object.assign({
|
||||||
|
showConfirm : true,
|
||||||
|
showAutoSnooze : true,
|
||||||
|
showHidden : true
|
||||||
|
}, require('Storage').readJSON('alarm.json',1)||{});
|
||||||
|
|
||||||
|
const save = () => require('Storage').write('alarm.json', settings);
|
||||||
|
const DATE_FORMATS = ['default', 'mmdd'];
|
||||||
|
const DATE_FORMATS_LABELS = [/*LANG*/'Default', /*LANG*/'MMDD'];
|
||||||
|
|
||||||
|
const appMenu = {
|
||||||
|
'': {title: 'alarm'}, '< Back': back,
|
||||||
|
/*LANG*/'Menu Date Format': {
|
||||||
|
value: DATE_FORMATS.indexOf(settings.menuDateFormat || 'default'),
|
||||||
|
format: v => DATE_FORMATS_LABELS[v],
|
||||||
|
min: 0,
|
||||||
|
max: DATE_FORMATS.length - 1,
|
||||||
|
onchange : v => {
|
||||||
|
if(v > 0) {
|
||||||
|
settings.menuDateFormat=DATE_FORMATS[v];
|
||||||
|
} else {
|
||||||
|
delete settings.menuDateFormat;
|
||||||
|
}
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/*LANG*/'Show Menu Auto Snooze': {
|
||||||
|
value : !!settings.showAutoSnooze,
|
||||||
|
onchange : v => { settings.showAutoSnooze=v; save();}
|
||||||
|
},
|
||||||
|
/*LANG*/'Show Menu Confirm': {
|
||||||
|
value : !!settings.showConfirm,
|
||||||
|
onchange : v => { settings.showConfirm=v; save();}
|
||||||
|
},
|
||||||
|
/*LANG*/'Show Menu Hidden': {
|
||||||
|
value : !!settings.showHidden,
|
||||||
|
onchange : v => { settings.showHidden=v; save();}
|
||||||
|
},
|
||||||
|
/*LANG*/'Show Menu Group': {
|
||||||
|
value : !!settings.showGroup,
|
||||||
|
onchange : v => { settings.showGroup=v; save();}
|
||||||
|
},
|
||||||
|
/*LANG*/'Show Text Overflow': {
|
||||||
|
value : !!settings.showOverflow,
|
||||||
|
onchange : v => { settings.showOverflow=v; save();}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
E.showMenu(appMenu);
|
||||||
|
});
|
|
@ -28,3 +28,7 @@
|
||||||
0.27: Issue newline before GB commands (solves issue with console.log and ignored commands)
|
0.27: Issue newline before GB commands (solves issue with console.log and ignored commands)
|
||||||
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
|
||||||
|
Allow alarm enable/disable
|
||||||
|
0.31: Implement API for activity fetching
|
||||||
|
0.32: Added support for loyalty cards from gadgetbridge
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -193,10 +193,37 @@
|
||||||
Bangle.on('HRM',actHRMHandler);
|
Bangle.on('HRM',actHRMHandler);
|
||||||
actInterval = setInterval(function() {
|
actInterval = setInterval(function() {
|
||||||
var steps = Bangle.getStepCount();
|
var steps = Bangle.getStepCount();
|
||||||
gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM });
|
gbSend({ t: "act", stp: steps-lastSteps, hrm: lastBPM, rt:1 });
|
||||||
lastSteps = steps;
|
lastSteps = steps;
|
||||||
}, event.int*1000);
|
}, event.int*1000);
|
||||||
},
|
},
|
||||||
|
// {t:"actfetch", ts:long}
|
||||||
|
"actfetch": function() {
|
||||||
|
gbSend({t: "actfetch", state: "start"});
|
||||||
|
var actCount = 0;
|
||||||
|
var actCb = function(r) {
|
||||||
|
// The health lib saves the samples at the start of the 10-minute block
|
||||||
|
// However, GB expects them at the end of the block, so let's offset them
|
||||||
|
// here to keep a consistent API in the health lib
|
||||||
|
var sampleTs = r.date.getTime() + 600000;
|
||||||
|
if (sampleTs >= event.ts) {
|
||||||
|
gbSend({
|
||||||
|
t: "act",
|
||||||
|
ts: sampleTs,
|
||||||
|
stp: r.steps,
|
||||||
|
hrm: r.bpm,
|
||||||
|
mov: r.movement
|
||||||
|
});
|
||||||
|
actCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (event.ts != 0) {
|
||||||
|
require("health").readAllRecordsSince(new Date(event.ts - 600000), actCb);
|
||||||
|
} else {
|
||||||
|
require("health").readFullDatabase(actCb);
|
||||||
|
}
|
||||||
|
gbSend({t: "actfetch", state: "end", count: actCount});
|
||||||
|
},
|
||||||
"nav": function() {
|
"nav": function() {
|
||||||
event.id="nav";
|
event.id="nav";
|
||||||
if (event.instr) {
|
if (event.instr) {
|
||||||
|
@ -209,6 +236,11 @@
|
||||||
event.t="remove";
|
event.t="remove";
|
||||||
}
|
}
|
||||||
require("messages").pushMessage(event);
|
require("messages").pushMessage(event);
|
||||||
|
},
|
||||||
|
"cards" : function() {
|
||||||
|
// we receive all, just override what we have
|
||||||
|
if (Array.isArray(event.d))
|
||||||
|
require("Storage").writeJSON("android.cards.json", event.d);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var h = HANDLERS[event.t];
|
var h = HANDLERS[event.t];
|
||||||
|
@ -253,6 +285,7 @@
|
||||||
Bangle.on("charging", sendBattery);
|
Bangle.on("charging", sendBattery);
|
||||||
NRF.on("connect", () => setTimeout(function() {
|
NRF.on("connect", () => setTimeout(function() {
|
||||||
sendBattery();
|
sendBattery();
|
||||||
|
gbSend({t: "ver", fw: process.env.VERSION, hw: process.env.HWVERSION});
|
||||||
GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process
|
GB({t:"force_calendar_sync_start"}); // send a list of our calendar entries to start off the sync process
|
||||||
}, 2000));
|
}, 2000));
|
||||||
NRF.on("disconnect", () => {
|
NRF.on("disconnect", () => {
|
||||||
|
@ -264,10 +297,9 @@
|
||||||
require("messages").clearAll();
|
require("messages").clearAll();
|
||||||
});
|
});
|
||||||
setInterval(sendBattery, 10*60*1000);
|
setInterval(sendBattery, 10*60*1000);
|
||||||
// Health tracking
|
// Health tracking - if 'realtime' data is sent with 'rt:1', but let's still send our activity log every 10 mins
|
||||||
Bangle.on('health', health=>{
|
Bangle.on('health', h=>{
|
||||||
if (actInterval===undefined) // if 'realtime' we do it differently
|
gbSend({ t: "act", stp: h.steps, hrm: h.bpm, mov: h.movement });
|
||||||
gbSend({ t: "act", stp: health.steps, hrm: health.bpm });
|
|
||||||
});
|
});
|
||||||
// Music control
|
// Music control
|
||||||
Bangle.musicControl = cmd => {
|
Bangle.musicControl = cmd => {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "android",
|
"id": "android",
|
||||||
"name": "Android Integration",
|
"name": "Android Integration",
|
||||||
"shortName": "Android",
|
"shortName": "Android",
|
||||||
"version": "0.29",
|
"version": "0.32",
|
||||||
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
|
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "tool,system,messages,notifications,gadgetbridge",
|
"tags": "tool,system,messages,notifications,gadgetbridge",
|
||||||
|
@ -15,6 +15,6 @@
|
||||||
{"name":"android.img","url":"app-icon.js","evaluate":true},
|
{"name":"android.img","url":"app-icon.js","evaluate":true},
|
||||||
{"name":"android.boot.js","url":"boot.js"}
|
{"name":"android.boot.js","url":"boot.js"}
|
||||||
],
|
],
|
||||||
"data": [{"name":"android.settings.json"}, {"name":"android.calendar.json"}],
|
"data": [{"name":"android.settings.json"}, {"name":"android.calendar.json"}, {"name":"android.cards.json"}],
|
||||||
"sortorder": -8
|
"sortorder": -8
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Select which GNSS system you want.</label>
|
<label class="form-label">Select which GNSS system you want.</label>
|
||||||
<label class="form-radio">
|
<label class="form-radio">
|
||||||
<input type="radio" name="gnss_select" value="1" checked><i class="form-icon"></i> GPS
|
<input type="radio" name="gnss_select" value="1" checked><i class="form-icon"></i> GPS (fastest to get a fix)
|
||||||
</label>
|
</label>
|
||||||
<label class="form-radio">
|
<label class="form-radio">
|
||||||
<input type="radio" name="gnss_select" value="2"><i class="form-icon"></i> BDS
|
<input type="radio" name="gnss_select" value="2"><i class="form-icon"></i> BDS
|
||||||
|
|
|
@ -2,3 +2,5 @@
|
||||||
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
|
||||||
|
0.06: Fix azimuth (bug #2651), only show degrees
|
||||||
|
|
|
@ -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;
|
||||||
|
@ -141,22 +140,21 @@ function drawData(title, obj, startX, startY) {
|
||||||
function drawMoonPositionPage(gps, title) {
|
function drawMoonPositionPage(gps, title) {
|
||||||
const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon);
|
const pos = SunCalc.getMoonPosition(new Date(), gps.lat, gps.lon);
|
||||||
const moonColor = g.theme.dark ? {r: 1, g: 1, b: 1} : {r: 0, g: 0, b: 0};
|
const moonColor = g.theme.dark ? {r: 1, g: 1, b: 1} : {r: 0, g: 0, b: 0};
|
||||||
|
const azimuth = pos.azimuth + Math.PI; // 0 is south, we want 0 to be north
|
||||||
|
|
||||||
const pageData = {
|
const pageData = {
|
||||||
Azimuth: pos.azimuth.toFixed(2),
|
Azimuth: parseInt(azimuth * 180 / Math.PI + 0.5) + '°',
|
||||||
Altitude: pos.altitude.toFixed(2),
|
Altitude: parseInt(pos.altitude * 180 / Math.PI + 0.5) + '°',
|
||||||
Distance: `${pos.distance.toFixed(0)} km`,
|
Distance: `${pos.distance.toFixed(0)} km`,
|
||||||
"Parallactic Ang": pos.parallacticAngle.toFixed(2),
|
"Parallactic Ang": parseInt(pos.parallacticAngle * 180 / Math.PI + 0.5) + '°',
|
||||||
};
|
};
|
||||||
const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI);
|
const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5);
|
||||||
|
|
||||||
drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20);
|
drawData(title, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20);
|
||||||
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 +172,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"});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -194,17 +190,17 @@ function drawMoonTimesPage(gps, title) {
|
||||||
|
|
||||||
// Draw the moon rise position
|
// Draw the moon rise position
|
||||||
const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon);
|
const risePos = SunCalc.getMoonPosition(times.rise, gps.lat, gps.lon);
|
||||||
const riseAzimuthDegrees = parseInt(risePos.azimuth * 180 / Math.PI);
|
const riseAzimuth = risePos.azimuth + Math.PI; // 0 is south, we want 0 to be north
|
||||||
|
const riseAzimuthDegrees = parseInt(riseAzimuth * 180 / Math.PI);
|
||||||
drawPoint(riseAzimuthDegrees, 8, moonColor);
|
drawPoint(riseAzimuthDegrees, 8, moonColor);
|
||||||
|
|
||||||
// Draw the moon set position
|
// Draw the moon set position
|
||||||
const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon);
|
const setPos = SunCalc.getMoonPosition(times.set, gps.lat, gps.lon);
|
||||||
const setAzimuthDegrees = parseInt(setPos.azimuth * 180 / Math.PI);
|
const setAzimuth = setPos.azimuth + Math.PI; // 0 is south, we want 0 to be north
|
||||||
|
const setAzimuthDegrees = parseInt(setAzimuth * 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) {
|
||||||
|
@ -214,16 +210,15 @@ function drawSunShowPage(gps, key, date) {
|
||||||
const mins = ("0" + date.getMinutes()).substr(-2);
|
const mins = ("0" + date.getMinutes()).substr(-2);
|
||||||
const secs = ("0" + date.getMinutes()).substr(-2);
|
const secs = ("0" + date.getMinutes()).substr(-2);
|
||||||
const time = `${hrs}:${mins}:${secs}`;
|
const time = `${hrs}:${mins}:${secs}`;
|
||||||
|
const azimuth = pos.azimuth + Math.PI; // 0 is south, we want 0 to be north
|
||||||
|
|
||||||
const azimuth = Number(pos.azimuth.toFixed(2));
|
const azimuthDegrees = parseInt(azimuth * 180 / Math.PI + 0.5) + '°';
|
||||||
const azimuthDegrees = parseInt(pos.azimuth * 180 / Math.PI);
|
const altitude = parseInt(pos.altitude * 180 / Math.PI + 0.5) + '°';
|
||||||
const altitude = Number(pos.altitude.toFixed(2));
|
|
||||||
|
|
||||||
const pageData = {
|
const pageData = {
|
||||||
Time: time,
|
Time: time,
|
||||||
Altitude: altitude,
|
Altitude: altitude,
|
||||||
Azimumth: azimuth,
|
Azimuth: azimuthDegrees,
|
||||||
Degrees: azimuthDegrees
|
|
||||||
};
|
};
|
||||||
|
|
||||||
drawData(key, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5);
|
drawData(key, pageData, null, g.getHeight()/2 - Object.keys(pageData).length/2*20 + 5);
|
||||||
|
@ -233,9 +228,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 +307,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;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"id": "astrocalc",
|
"id": "astrocalc",
|
||||||
"name": "Astrocalc",
|
"name": "Astrocalc",
|
||||||
"version": "0.04",
|
"version": "0.06",
|
||||||
"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,outdoors",
|
||||||
"supports": ["BANGLEJS", "BANGLEJS2"],
|
"supports": ["BANGLEJS", "BANGLEJS2"],
|
||||||
"allow_emulator": true,
|
"allow_emulator": true,
|
||||||
"dependencies": {"mylocation":"app"},
|
"dependencies": {"mylocation":"app"},
|
||||||
|
|
|
@ -1,2 +1,5 @@
|
||||||
0.02: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast
|
0.02: Add "ram" keyword to allow 2v06 Espruino builds to cache function that needs to be fast
|
||||||
0.03: Bangle 2 support
|
0.03: Bangle 2 support
|
||||||
|
0.04: Increase size of ship, asteroids and fonts for better readability
|
||||||
|
0.05: improve collision detect for larger ship v astroid
|
||||||
|
0.06: added, 7 point asteroid ploygon, made ship solid, rather than outline
|
||||||
|
|
|
@ -18,6 +18,22 @@ if (process.env.HWVERSION==2) {
|
||||||
}
|
}
|
||||||
var W = g.getWidth();
|
var W = g.getWidth();
|
||||||
var H = g.getHeight();
|
var H = g.getHeight();
|
||||||
|
var SS = W/11; // ship back length
|
||||||
|
var SL = W/15; // ship side length
|
||||||
|
var AS = W/18; // asteroid radius
|
||||||
|
// radius of ship, assumed a circle inside equilateral traingle of side SS
|
||||||
|
// r = a / root 3 where a is length of equilateral triangle
|
||||||
|
var SR = SS / Math.sqrt(3);
|
||||||
|
var AST = [ // asteroid polygon as X/Y pairs
|
||||||
|
0 ,-1.5,
|
||||||
|
1 , 0,
|
||||||
|
0.5, 0,
|
||||||
|
0.5, 0.5,
|
||||||
|
0 , 1,
|
||||||
|
-1 , 0,
|
||||||
|
-1 , -1
|
||||||
|
];
|
||||||
|
|
||||||
g.clear().setFontAlign(0,-1);
|
g.clear().setFontAlign(0,-1);
|
||||||
|
|
||||||
function newAst(x,y) {
|
function newAst(x,y) {
|
||||||
|
@ -25,7 +41,7 @@ function newAst(x,y) {
|
||||||
x:x,y:y,
|
x:x,y:y,
|
||||||
vx:Math.random()-0.5,
|
vx:Math.random()-0.5,
|
||||||
vy:Math.random()-0.5,
|
vy:Math.random()-0.5,
|
||||||
rad:3+Math.random()*5
|
rad:3+Math.random()*AS
|
||||||
};
|
};
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
@ -41,8 +57,9 @@ var lastFrame;
|
||||||
|
|
||||||
function gameStop() {
|
function gameStop() {
|
||||||
running = false;
|
running = false;
|
||||||
g.clear();
|
g.setFont('Vector', W/7);
|
||||||
g.drawString("Game Over!",120,(H-6)/2);
|
g.setFontAlign(0,0);
|
||||||
|
g.drawString("Game Over", W/2, H/2);
|
||||||
g.flip();
|
g.flip();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,12 +121,13 @@ function onFrame() {
|
||||||
}
|
}
|
||||||
|
|
||||||
g.clear();
|
g.clear();
|
||||||
g.drawString(score,W-20,0);
|
g.setFont('Vector', 16);
|
||||||
|
g.drawString(score,W-20,16);
|
||||||
var rs = Math.PI*0.8;
|
var rs = Math.PI*0.8;
|
||||||
g.drawPoly([
|
g.fillPoly([
|
||||||
ship.x+Math.cos(ship.r)*4, ship.y+Math.sin(ship.r)*4,
|
ship.x+Math.cos(ship.r)*SS, ship.y+Math.sin(ship.r)*SS,
|
||||||
ship.x+Math.cos(ship.r+rs)*3, ship.y+Math.sin(ship.r+rs)*3,
|
ship.x+Math.cos(ship.r+rs)*SL, ship.y+Math.sin(ship.r+rs)*SL,
|
||||||
ship.x+Math.cos(ship.r-rs)*3, ship.y+Math.sin(ship.r-rs)*3,
|
ship.x+Math.cos(ship.r-rs)*SL, ship.y+Math.sin(ship.r-rs)*SL,
|
||||||
],true);
|
],true);
|
||||||
var na = [];
|
var na = [];
|
||||||
ammo.forEach(function(a) {
|
ammo.forEach(function(a) {
|
||||||
|
@ -137,7 +155,10 @@ function onFrame() {
|
||||||
ast.forEach(function(a) {
|
ast.forEach(function(a) {
|
||||||
a.x += a.vx*d;
|
a.x += a.vx*d;
|
||||||
a.y += a.vy*d;
|
a.y += a.vy*d;
|
||||||
g.drawCircle(a.x, a.y, a.rad);
|
//g.drawCircle(a.x, a.y, a.rad);
|
||||||
|
// a 7 point asteroid with rough circle radius of scale 2
|
||||||
|
g.drawPoly(g.transformVertices(AST,{x:a.x,y:a.y,scale:a.rad,rotate:t}),true);
|
||||||
|
|
||||||
if (a.x<0) a.x+=W;
|
if (a.x<0) a.x+=W;
|
||||||
if (a.y<0) a.y+=H;
|
if (a.y<0) a.y+=H;
|
||||||
if (a.x>=W) a.x-=W;
|
if (a.x>=W) a.x-=W;
|
||||||
|
@ -165,7 +186,7 @@ function onFrame() {
|
||||||
var dx = a.x-ship.x;
|
var dx = a.x-ship.x;
|
||||||
var dy = a.y-ship.y;
|
var dy = a.y-ship.y;
|
||||||
var d = Math.sqrt(dx*dx+dy*dy);
|
var d = Math.sqrt(dx*dx+dy*dy);
|
||||||
if (d < a.rad) crashed = true;
|
if (d < a.rad + SR) crashed = true;
|
||||||
});
|
});
|
||||||
ast=na;
|
ast=na;
|
||||||
if (!ast.length) {
|
if (!ast.length) {
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"id": "astroid",
|
"id": "astroid",
|
||||||
"name": "Asteroids!",
|
"name": "Asteroids!",
|
||||||
"version": "0.03",
|
"version": "0.06",
|
||||||
"description": "Retro asteroids game",
|
"description": "Retro asteroids game",
|
||||||
"icon": "asteroids.png",
|
"icon": "asteroids.png",
|
||||||
"screenshots": [{"url":"screenshot_asteroids.png"}],
|
"screenshots": [{"url":"screenshot.png"}],
|
||||||
"tags": "game",
|
"tags": "game",
|
||||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
"allow_emulator": true,
|
"allow_emulator": true,
|
||||||
|
|
After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1,2 +1,3 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
0.02: Don't fire if the app uses swipes already.
|
0.02: Don't fire if the app uses swipes already.
|
||||||
|
0.03: Only count defined handlers in the handler array.
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
if (Bangle["#on"+eventType] === undefined) {
|
if (Bangle["#on"+eventType] === undefined) {
|
||||||
return 0;
|
return 0;
|
||||||
} else if (Bangle["#on"+eventType] instanceof Array) {
|
} else if (Bangle["#on"+eventType] instanceof Array) {
|
||||||
return Bangle["#on"+eventType].length;
|
return Bangle["#on"+eventType].filter(x=>x).length;
|
||||||
} else if (Bangle["#on"+eventType] !== undefined) {
|
} else if (Bangle["#on"+eventType] !== undefined) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{ "id": "backswipe",
|
{ "id": "backswipe",
|
||||||
"name": "Back Swipe",
|
"name": "Back Swipe",
|
||||||
"shortName":"BackSwipe",
|
"shortName":"BackSwipe",
|
||||||
"version":"0.02",
|
"version":"0.03",
|
||||||
"description": "Service that allows you to use an app's back button using left to right swipe gesture",
|
"description": "Service that allows you to use an app's back button using left to right swipe gesture",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "back,gesture,swipe",
|
"tags": "back,gesture,swipe",
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
1.00: Initial release of Bangle Blobs Clock!
|
|
@ -0,0 +1,35 @@
|
||||||
|
# Bangle Blobs Clock
|
||||||
|
What if every time you checked the time, you could play a turn of a turn-based puzzle game?
|
||||||
|
You check the time dozens, maybe hundreds of times per day, and Bangle Blobs Clock wants to add a splash of fun to each of these moments!
|
||||||
|
Bangle Blobs Clock is a fully featured watch face with a turn-based puzzle game right next to the clock.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
## Clock Features
|
||||||
|
- Hour and minute
|
||||||
|
- Seconds (only while the screen is unlocked to save power)
|
||||||
|
- Month, day, and day of week
|
||||||
|
- Battery percentage. Blue while charging, red when low, green otherwise.
|
||||||
|
- Respects your 24-hour/12-hour time setting in Locale
|
||||||
|
- Press the pause button to access your Widgets
|
||||||
|
- Supports Fast Loading
|
||||||
|
|
||||||
|
## The Game
|
||||||
|
This is a turn-based puzzle game based on Puyo Puyo, an addictive puzzle game franchise by SEGA.
|
||||||
|
Blobs arrive in pairs that you can move, rotate, and place. When at least four Blobs of the same color touch, they pop, causing Blobs above them to fall.
|
||||||
|
If this causes another pop, it's called a chain! Build a massive chain reaction of popping Blobs!
|
||||||
|
- Drag left and right to move the pair
|
||||||
|
- Tap the left or right half of the screen to rotate the pair
|
||||||
|
- Swipe down to place the pair
|
||||||
|
|
||||||
|
## More Info
|
||||||
|
If you're confused about the functionality of the clock or want a better explanation of how to play the game, I wrote up a user manual here: https://docs.google.com/document/d/1watPzChawBu4iM0lXypreejs3wvf2_8C-x5V2MWJQBc/edit?usp=sharing
|
||||||
|
|
||||||
|
## Special Thanks
|
||||||
|
I'm Pasta Rhythm, computer scientist and aspiring game developer. I would like to say thank you to the people who inspired me while I was making this app:
|
||||||
|
- [nxdefiant, who made a Tetris game.](https://github.com/espruino/BangleApps/tree/master/apps/tetris) Bangle Blobs is my first Bangle app and my first time using JavaScript, so this was a daunting project. This Tetris game served as a great example that helped me get started.
|
||||||
|
- [gfwilliams for Anton Clock](https://github.com/espruino/BangleApps/tree/master/apps/antonclk) and [Targor for Kanagawa Clock.](https://github.com/espruino/BangleApps/tree/master/apps/kanagsec) These were good examples for how to make a watch face for the Bangle.js 2.
|
||||||
|
- Thanks to Gordon Williams and to everyone who contributes to Espruino and the Bangle.js 2 projects!
|
||||||
|
- SEGA, owners of the Puyo Puyo franchise that Bangle Blobs is based on. Please check out official Puyo Puyo games!
|
||||||
|
- Compile, the original creators of Puyo Puyo. The company went bankrupt long ago, but the people who worked for them continue to make games.
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwxH+HGm56+5BQ4JBAJItXAAoMMCJQAPJ5pfhJApPQL65HHKIbTU2nXAAu0I5xQNBo4tC2gAFGIxHIL5oNGEoItGGIgwDL6oMGFxgwFL6oVFFxwwEL7YuPGARfVBYwvUL6YLGL84THL84KHL7YHCL6AeBFx+0JggAGLx4wQFwa3DAIwvHNJQwMFwhgIEQ7ILGAYxHBAQWJADUeFAIAEjwtnjwAFGMglBFowxEGA/XgrgICJouMGA4aBAIgvMB4ouOGAouGMZgNGFx4wCPQ5hMN44vTK44wLNo5fUcRwuHL67iOHAxfhFxYJBBooeBFx8ecRY4KBowwOFxDgHM5BtHGBguZfhIkBGI4ICFyILFAIxBHAAoOGXIgLHBowBGFo0FAAoxHFxhfPAoQAJCIguNGxRtGABYpDQB72LFxwwEcCJfJFx4wCL7gvTADYv/F/4APYoQuOaoYwpFz4wOF0IwDGI4ICF0IxFAAgtFA="))
|
|
@ -0,0 +1,768 @@
|
||||||
|
{
|
||||||
|
// ~~ Variables for clock ~~
|
||||||
|
let clockDrawTimeout;
|
||||||
|
let twelveHourTime = require('Storage').readJSON('setting.json', 1)['12hour'];
|
||||||
|
let updateSeconds = !Bangle.isLocked();
|
||||||
|
let batteryLevel = E.getBattery();
|
||||||
|
|
||||||
|
// ~~ Variables for game logic ~~
|
||||||
|
const NUM_COLORS = 6;
|
||||||
|
const NUISANCE_COLOR = 7;
|
||||||
|
let grid = [
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0])
|
||||||
|
];
|
||||||
|
let hiddenRow = new Uint8Array([0, 0, 0, 0, 0, 0]);
|
||||||
|
let nextQueue = [{pivot: 1, leaf: 1}, {pivot: 1, leaf: 1}];
|
||||||
|
let currentPair = {pivot: 0, leaf: 0};
|
||||||
|
let dropCoordinates = {pivotX: 2, pivotY: 11, leafX: 2, leafY: 10};
|
||||||
|
let pairX = 2;
|
||||||
|
let pairOrientation = 0; //0 is up, 1 is right, 2 is down, 3 is left
|
||||||
|
let slotsToCheck = [];
|
||||||
|
let selectedColors;
|
||||||
|
let lastChain = 0;
|
||||||
|
let gameLost = false;
|
||||||
|
let gamePaused = false;
|
||||||
|
let midChain = false;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Sets up a new game.
|
||||||
|
Must be called once before the first round.
|
||||||
|
*/
|
||||||
|
let restartGame = function() {
|
||||||
|
grid = [
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0])
|
||||||
|
];
|
||||||
|
hiddenRow = new Uint8Array([0, 0, 0, 0, 0, 0]);
|
||||||
|
currentPair = {pivot: 0, leaf: 0};
|
||||||
|
pairX = 2;
|
||||||
|
pairOrientation = 0; //0 is up, 1 is right, 2 is down, 3 is left
|
||||||
|
slotsToCheck = [];
|
||||||
|
gameLost = false;
|
||||||
|
lastChain = 0;
|
||||||
|
|
||||||
|
//Set up random colors
|
||||||
|
selectedColors = new Uint8Array([1, 2, 3, 4, 5, 6]);
|
||||||
|
for (let i = NUM_COLORS - 1; i > 0; i--) {
|
||||||
|
let swap = selectedColors[i];
|
||||||
|
let swapIndex = Math.floor(Math.random() * (i + 1));
|
||||||
|
selectedColors[i] = selectedColors[swapIndex];
|
||||||
|
selectedColors[swapIndex] = swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Create the first two pairs (Always in the first three colors)
|
||||||
|
nextQueue[0].pivot = selectedColors[Math.floor(Math.random() * 3)];
|
||||||
|
nextQueue[0].leaf = selectedColors[Math.floor(Math.random() * 3)];
|
||||||
|
nextQueue[1].pivot = selectedColors[Math.floor(Math.random() * 3)];
|
||||||
|
nextQueue[1].leaf = selectedColors[Math.floor(Math.random() * 3)];
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Readies the next pair and generates a new one for the queue.
|
||||||
|
*/
|
||||||
|
let newPair = function() {
|
||||||
|
currentPair.pivot = nextQueue[0].pivot;
|
||||||
|
currentPair.leaf = nextQueue[0].leaf;
|
||||||
|
|
||||||
|
nextQueue[0].pivot = nextQueue[1].pivot;
|
||||||
|
nextQueue[0].leaf = nextQueue[1].leaf;
|
||||||
|
|
||||||
|
nextQueue[1].pivot = selectedColors[Math.floor(Math.random() * 4)];
|
||||||
|
nextQueue[1].leaf = selectedColors[Math.floor(Math.random() * 4)];
|
||||||
|
|
||||||
|
pairX = 2;
|
||||||
|
pairOrientation = 0;
|
||||||
|
|
||||||
|
calcDropCoordinates();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Calculates the coordinates at which the current pair will be placed when quick dropped.
|
||||||
|
*/
|
||||||
|
let calcDropCoordinates = function() {
|
||||||
|
dropCoordinates.pivotX = pairX;
|
||||||
|
|
||||||
|
//Find Y coordinate of pivot
|
||||||
|
dropCoordinates.pivotY = -2;
|
||||||
|
for (let i = 11; i >= 0; i--) {
|
||||||
|
if (grid[i][pairX] == 0) {
|
||||||
|
dropCoordinates.pivotY = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dropCoordinates.pivotY == -2 && hiddenRow[pairX] == 0)
|
||||||
|
dropCoordinates.pivotY = -1;
|
||||||
|
|
||||||
|
//Find coordinates of leaf
|
||||||
|
if (pairOrientation == 1) {
|
||||||
|
dropCoordinates.leafX = pairX + 1;
|
||||||
|
|
||||||
|
dropCoordinates.leafY = -2;
|
||||||
|
for (let i = 11; i >= 0; i--) {
|
||||||
|
if (grid[i][pairX + 1] == 0) {
|
||||||
|
dropCoordinates.leafY = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dropCoordinates.leafY == -2 && hiddenRow[pairX + 1] == 0)
|
||||||
|
dropCoordinates.leafY = -1;
|
||||||
|
} else if (pairOrientation == 3) {
|
||||||
|
dropCoordinates.leafX = pairX - 1;
|
||||||
|
|
||||||
|
dropCoordinates.leafY = -2;
|
||||||
|
for (let i = 11; i >= 0; i--) {
|
||||||
|
if (grid[i][pairX - 1] == 0) {
|
||||||
|
dropCoordinates.leafY = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dropCoordinates.leafY == -2 && hiddenRow[pairX - 1] == 0)
|
||||||
|
dropCoordinates.leafY = -1;
|
||||||
|
} else if (pairOrientation == 2) {
|
||||||
|
dropCoordinates.leafX = pairX;
|
||||||
|
dropCoordinates.leafY = dropCoordinates.pivotY;
|
||||||
|
dropCoordinates.pivotY--;
|
||||||
|
} else {
|
||||||
|
dropCoordinates.leafX = pairX;
|
||||||
|
dropCoordinates.leafY = dropCoordinates.pivotY - 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Moves the current pair a certain number of slots.
|
||||||
|
*/
|
||||||
|
let movePair = function(dx) {
|
||||||
|
pairX += dx;
|
||||||
|
|
||||||
|
if (dx < 0) {
|
||||||
|
if (pairX < (pairOrientation == 3 ? 1 : 0))
|
||||||
|
pairX = (pairOrientation == 3 ? 1 : 0);
|
||||||
|
}
|
||||||
|
if (dx > 0) {
|
||||||
|
if (pairX > (pairOrientation == 1 ? 4 : 5))
|
||||||
|
pairX = (pairOrientation == 1 ? 4 : 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
calcDropCoordinates();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Rotates the pair in the given direction around the pivot.
|
||||||
|
*/
|
||||||
|
let rotatePair = function(clockwise) {
|
||||||
|
pairOrientation += (clockwise ? 1 : -1);
|
||||||
|
if (pairOrientation > 3)
|
||||||
|
pairOrientation = 0;
|
||||||
|
if (pairOrientation < 0)
|
||||||
|
pairOrientation = 3;
|
||||||
|
|
||||||
|
if (pairOrientation == 1 && pairX == 5)
|
||||||
|
pairX = 4;
|
||||||
|
if (pairOrientation == 3 && pairX == 0)
|
||||||
|
pairX = 1;
|
||||||
|
|
||||||
|
calcDropCoordinates();
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Places the current pair at the drop coordinates.
|
||||||
|
*/
|
||||||
|
let quickDrop = function() {
|
||||||
|
if (dropCoordinates.pivotY == -1) {
|
||||||
|
hiddenRow[dropCoordinates.pivotX] = currentPair.pivot;
|
||||||
|
} else if (dropCoordinates.pivotY > -1) {
|
||||||
|
grid[dropCoordinates.pivotY][dropCoordinates.pivotX] = currentPair.pivot;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dropCoordinates.leafY == -1) {
|
||||||
|
hiddenRow[dropCoordinates.leafX] = currentPair.leaf;
|
||||||
|
} else if (dropCoordinates.leafY > -1) {
|
||||||
|
grid[dropCoordinates.leafY][dropCoordinates.leafX] = currentPair.leaf;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPair.pivot = 0;
|
||||||
|
currentPair.leaf = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Makes all blobs fall to the lowest available slot.
|
||||||
|
All blobs that fall will be added to slotsToCheck.
|
||||||
|
*/
|
||||||
|
let settleBlobs = function() {
|
||||||
|
for (let x = 0; x < 6; x++) {
|
||||||
|
let lowestOpen = 11;
|
||||||
|
for (let y = 11; y >= 0; y--) {
|
||||||
|
if (grid[y][x] != 0) {
|
||||||
|
if (y != lowestOpen) {
|
||||||
|
grid[lowestOpen][x] = grid[y][x];
|
||||||
|
grid[y][x] = 0;
|
||||||
|
addSlotToCheck(x, lowestOpen);
|
||||||
|
}
|
||||||
|
lowestOpen--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lowestOpen >= 0 && hiddenRow[x] != 0) {
|
||||||
|
grid[lowestOpen][x] = hiddenRow[x];
|
||||||
|
hiddenRow[x] = 0;
|
||||||
|
addSlotToCheck(x, lowestOpen);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Adds a slot to slotsToCheck. This slot will be checked for a pop
|
||||||
|
next time popAll is called.
|
||||||
|
*/
|
||||||
|
let addSlotToCheck = function(x, y) {
|
||||||
|
slotsToCheck.push({x: x, y: y});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Checks for a pop at every slot in slotsToCheck.
|
||||||
|
Pops at all locations.
|
||||||
|
*/
|
||||||
|
let popAll = function() {
|
||||||
|
let result = {pops: 0};
|
||||||
|
while(slotsToCheck.length > 0) {
|
||||||
|
let coord = slotsToCheck.pop();
|
||||||
|
if (grid[coord.y][coord.x] != 0 && grid[coord.y][coord.x] != NUISANCE_COLOR) {
|
||||||
|
if (checkSlotForPop(coord.x, coord.y))
|
||||||
|
result.pops += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Checks a specific slot for a pop.
|
||||||
|
If there are four or more adjacent blobs of the same color, they are removed.
|
||||||
|
*/
|
||||||
|
let checkSlotForPop = function(x, y) {
|
||||||
|
let toDelete = [
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0]),
|
||||||
|
new Uint8Array([0, 0, 0, 0, 0, 0])
|
||||||
|
];
|
||||||
|
let blobsInClump = 0;
|
||||||
|
let color = grid[y][x];
|
||||||
|
let toCheck = [{x: x, y: y}];
|
||||||
|
|
||||||
|
//Count every blob in this clump
|
||||||
|
while (toCheck.length > 0) {
|
||||||
|
let coord = toCheck.pop();
|
||||||
|
if (grid[coord.y][coord.x] == color && toDelete[coord.y][coord.x] == 0) {
|
||||||
|
blobsInClump++;
|
||||||
|
toDelete[coord.y][coord.x] = 1;
|
||||||
|
if (coord.x > 0) toCheck.push({x: coord.x - 1, y: coord.y});
|
||||||
|
if (coord.x < 5) toCheck.push({x: coord.x + 1, y: coord.y});
|
||||||
|
if (coord.y > 0) toCheck.push({x: coord.x, y: coord.y - 1});
|
||||||
|
if (coord.y < 11) toCheck.push({x: coord.x, y: coord.y + 1});
|
||||||
|
}
|
||||||
|
if (grid[coord.y][coord.x] == NUISANCE_COLOR && toDelete[coord.y][coord.x] == 0)
|
||||||
|
toDelete[coord.y][coord.x] = 1; //For erasing garbage
|
||||||
|
}
|
||||||
|
|
||||||
|
//If there are at least four blobs in this clump, remove them from the grid and draw a pop.
|
||||||
|
if (blobsInClump >= 4) {
|
||||||
|
for (let y = 0; y < 12; y++) {
|
||||||
|
for (let x = 0; x < 6; x++) {
|
||||||
|
if (toDelete[y][x] == 1) {
|
||||||
|
grid[y][x] = 0;
|
||||||
|
|
||||||
|
//Clear the blob out of the slot
|
||||||
|
g.setBgColor(0, 0, 0);
|
||||||
|
g.clearRect((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
|
||||||
|
|
||||||
|
//Draw the pop
|
||||||
|
let colorInfo = getColor(color);
|
||||||
|
g.setColor(colorInfo.r, colorInfo.g, colorInfo.b);
|
||||||
|
if (color < NUISANCE_COLOR) {
|
||||||
|
//A fancy pop for popped colors!
|
||||||
|
g.drawEllipse((x*18)+36, (y*14)+7, (x*18)+50, (y*14)+21);
|
||||||
|
g.drawEllipse((x*18)+27, (y*14)-2, (x*18)+59, (y*14)+30);
|
||||||
|
} else if (color == NUISANCE_COLOR) {
|
||||||
|
//Nuisance Blobs are simply crossed out.
|
||||||
|
//TODO: Nuisance Blobs are currently unusued, but also untested. Test before use.
|
||||||
|
g.drawLine((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Variables for graphics
|
||||||
|
let oldGhost = {pivotX: 0, pivotY: 0, leafX: 0, leafY: 0};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Draws the time on the side.
|
||||||
|
*/
|
||||||
|
let drawTime = function(scheduleNext) {
|
||||||
|
//Change this to alter the y-coordinate of the top edge.
|
||||||
|
let dy = 25;
|
||||||
|
|
||||||
|
g.setBgColor(0, 0, 0);
|
||||||
|
g.clearRect(2, dy, 30, dy + 121);
|
||||||
|
|
||||||
|
//Draw the time
|
||||||
|
let d = new Date();
|
||||||
|
let h = d.getHours(), m = d.getMinutes();
|
||||||
|
if (twelveHourTime) {
|
||||||
|
let mer = 'A';
|
||||||
|
if (h >= 12) mer = 'P';
|
||||||
|
if (h >= 13) h -= 12;
|
||||||
|
if (h == 0) h = 12;
|
||||||
|
|
||||||
|
g.setColor(1, 1, 1);
|
||||||
|
g.setFont("Vector", 12);
|
||||||
|
g.drawString(mer, 23, dy + 63);
|
||||||
|
}
|
||||||
|
let hs = h.toString().padStart(2, 0);
|
||||||
|
let ms = m.toString().padStart(2, 0);
|
||||||
|
g.setFont("Vector", 24);
|
||||||
|
g.setColor(1, 0.2, 1);
|
||||||
|
g.drawString(hs, 3, dy + 21);
|
||||||
|
g.setColor(0.5, 0.5, 1);
|
||||||
|
g.drawString(ms, 3, dy + 42);
|
||||||
|
|
||||||
|
//Draw seconds
|
||||||
|
let s = d.getSeconds();
|
||||||
|
if (updateSeconds) {
|
||||||
|
let ss = s.toString().padStart(2, 0);
|
||||||
|
g.setFont("Vector", 12);
|
||||||
|
g.setColor(0.2, 1, 0.2);
|
||||||
|
g.drawString(ss, 3, dy + 63);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Draw the date
|
||||||
|
let dayString = d.getDate().toString();
|
||||||
|
let dayNames = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
|
||||||
|
let dayName = dayNames[d.getDay()];
|
||||||
|
let monthNames = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JLY", "AUG", "SEP", "OCT", "NOV", "DEC"];
|
||||||
|
let monthName = monthNames[d.getMonth()];
|
||||||
|
g.setColor(1, 1, 1);
|
||||||
|
g.setFont("Vector", 12);
|
||||||
|
g.drawString(monthName, 3, dy + 84);
|
||||||
|
g.drawString(dayString, 3, dy + 97);
|
||||||
|
g.setColor(0.5, 0.5, 0.5);
|
||||||
|
g.drawString(dayName, 3, dy + 110);
|
||||||
|
|
||||||
|
//Draw battery
|
||||||
|
if (s == 0) batteryLevel = E.getBattery();
|
||||||
|
if (Bangle.isCharging()) {
|
||||||
|
g.setColor(0, 0, 1);
|
||||||
|
} else if (batteryLevel <= 15) {
|
||||||
|
g.setColor(1, 0, 0);
|
||||||
|
} else {
|
||||||
|
g.setColor(0, 1, 0);
|
||||||
|
}
|
||||||
|
g.drawString(batteryLevel + "%", 3, dy + 1);
|
||||||
|
|
||||||
|
//Schedule the next draw if requested.
|
||||||
|
if (!scheduleNext) return;
|
||||||
|
if (clockDrawTimeout) clearTimeout(clockDrawTimeout);
|
||||||
|
let interval = updateSeconds ? 1000 : 60000;
|
||||||
|
clockDrawTimeout = setTimeout(function() {
|
||||||
|
clockDrawTimeout = undefined;
|
||||||
|
drawTime(true);
|
||||||
|
}, interval - (Date.now() % interval));
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Returns a tuple in the format {r, g, b} with the color
|
||||||
|
of the blob with the given ID.
|
||||||
|
This saves memory compared to having the colors stored in an array.
|
||||||
|
*/
|
||||||
|
let getColor = function(color) {
|
||||||
|
if (color == 1)
|
||||||
|
return {r: 1, g: 0, b: 0};
|
||||||
|
if (color == 2)
|
||||||
|
return {r: 0, g: 1, b: 0};
|
||||||
|
if (color == 3)
|
||||||
|
return {r: 0, g: 0, b: 1};
|
||||||
|
if (color == 4)
|
||||||
|
return {r: 1, g: 1, b: 0};
|
||||||
|
if (color == 5)
|
||||||
|
return {r: 1, g: 0, b: 1};
|
||||||
|
if (color == 6)
|
||||||
|
return {r: 0, g: 1, b: 1};
|
||||||
|
if (color == 7)
|
||||||
|
return {r: 0.5, g: 0.5, b: 0.5};
|
||||||
|
return {r: 1, g: 1, b: 1};
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Clears the screen and draws the background.
|
||||||
|
*/
|
||||||
|
let drawBackground = function() {
|
||||||
|
//Background
|
||||||
|
g.setBgColor(0.5, 0.2, 0.1);
|
||||||
|
g.clear();
|
||||||
|
g.setBgColor(0, 0, 0);
|
||||||
|
g.clearRect(33, 0, 142, 176);
|
||||||
|
g.setBgColor(0.5, 0.5, 0.5);
|
||||||
|
g.clearRect(33, 4, 142, 6);
|
||||||
|
|
||||||
|
//Reset button
|
||||||
|
g.setBgColor(0.5, 0.5, 0.5);
|
||||||
|
g.setColor(0, 0, 0);
|
||||||
|
g.clearRect(143, 150, 175, 175);
|
||||||
|
g.setFont("Vector", 30);
|
||||||
|
g.drawString("R", 152, 150);
|
||||||
|
|
||||||
|
//Pause button
|
||||||
|
g.clearRect(0, 150, 32, 175);
|
||||||
|
g.fillRect(9, 154, 13, 171);
|
||||||
|
g.fillRect(18, 154, 22, 171);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Draws a box under the next queue that displays
|
||||||
|
the current value of lastChain.
|
||||||
|
*/
|
||||||
|
let drawChainCount = function() {
|
||||||
|
g.setBgColor(0, 0, 0);
|
||||||
|
g.setColor(1, 0.2, 0.2);
|
||||||
|
g.setFont("Vector", 23);
|
||||||
|
g.clearRect(145, 42, 173, 64);
|
||||||
|
|
||||||
|
if (lastChain > 0) {
|
||||||
|
if (lastChain < 10) g.drawString(lastChain, 154, 44);
|
||||||
|
if (lastChain >= 10) g.drawString(lastChain, 147, 44);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Draws the blob at the given slot.
|
||||||
|
*/
|
||||||
|
let drawBlobAtSlot = function(x, y) {
|
||||||
|
//If this blob is in the hidden row, clear it out and stop.
|
||||||
|
if (y < 0) {
|
||||||
|
g.setBgColor(0, 0, 0);
|
||||||
|
g.clearRect((x*18)+34, 0, (x*18)+52, 3);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//First, clear what was in that slot.
|
||||||
|
g.setBgColor(0, 0, 0);
|
||||||
|
g.clearRect((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
|
||||||
|
|
||||||
|
let color = grid[y][x];
|
||||||
|
|
||||||
|
if (color != 0) {
|
||||||
|
let myColor = getColor(color);
|
||||||
|
g.setColor(myColor.r, myColor.g, myColor.b);
|
||||||
|
g.fillEllipse((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
|
||||||
|
g.setColor(1, 1, 1);
|
||||||
|
g.drawEllipse((x*18)+34, (y*14)+7, (x*18)+52, (y*14)+21);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Draws the ghost piece.
|
||||||
|
clearOld: if the previous location of the ghost piece should be cleared.
|
||||||
|
*/
|
||||||
|
let drawGhostPiece = function(clearOld) {
|
||||||
|
if (clearOld) {
|
||||||
|
g.setColor(0, 0, 0);
|
||||||
|
g.fillRect((oldGhost.pivotX*18)+38, (oldGhost.pivotY*14)+8, (oldGhost.pivotX*18)+47, (oldGhost.pivotY*14)+17);
|
||||||
|
g.fillRect((oldGhost.leafX*18)+38, (oldGhost.leafY*14)+8, (oldGhost.leafX*18)+47, (oldGhost.leafY*14)+17);
|
||||||
|
}
|
||||||
|
|
||||||
|
let pivotX = dropCoordinates.pivotX;
|
||||||
|
let pivotY = dropCoordinates.pivotY;
|
||||||
|
let leafX = dropCoordinates.leafX;
|
||||||
|
let leafY = dropCoordinates.leafY;
|
||||||
|
let pivotColor = getColor(currentPair.pivot);
|
||||||
|
let leafColor = getColor(currentPair.leaf);
|
||||||
|
|
||||||
|
g.setColor(pivotColor.r, pivotColor.g, pivotColor.b);
|
||||||
|
g.fillRect((pivotX*18)+40, (pivotY*14)+10, (pivotX*18)+45, (pivotY*14)+15);
|
||||||
|
g.setColor(1, 1, 1);
|
||||||
|
g.drawRect((pivotX*18)+38, (pivotY*14)+8, (pivotX*18)+47, (pivotY*14)+17);
|
||||||
|
g.setColor(leafColor.r, leafColor.g, leafColor.b);
|
||||||
|
g.fillRect((leafX*18)+40, (leafY*14)+10, (leafX*18)+45, (leafY*14)+15);
|
||||||
|
|
||||||
|
oldGhost = {pivotX: pivotX, pivotY: pivotY, leafX: leafX, leafY: leafY};
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Draws the next queue.
|
||||||
|
*/
|
||||||
|
let drawNextQueue = function() {
|
||||||
|
g.setBgColor(0, 0, 0);
|
||||||
|
g.clearRect(145, 4, 173, 28);
|
||||||
|
|
||||||
|
let p1 = nextQueue[0].pivot;
|
||||||
|
let l1 = nextQueue[0].leaf;
|
||||||
|
let p2 = nextQueue[1].pivot;
|
||||||
|
let l2 = nextQueue[1].leaf;
|
||||||
|
let p1C = getColor(p1);
|
||||||
|
let l1C = getColor(l1);
|
||||||
|
let p2C = getColor(p2);
|
||||||
|
let l2C = getColor(l2);
|
||||||
|
|
||||||
|
g.setColor(p1C.r, p1C.g, p1C.b);
|
||||||
|
g.fillEllipse(146, 17, 157, 28);
|
||||||
|
g.setColor(l1C.r, l1C.g, l1C.b);
|
||||||
|
g.fillEllipse(146, 5, 157, 16);
|
||||||
|
g.setColor(p2C.r, p2C.g, p2C.b);
|
||||||
|
g.fillEllipse(162, 17, 173, 28);
|
||||||
|
g.setColor(l2C.r, l2C.g, l2C.b);
|
||||||
|
g.fillEllipse(162, 5, 173, 16);
|
||||||
|
|
||||||
|
g.setColor(1, 1, 1);
|
||||||
|
g.drawLine(159, 4, 159, 28);
|
||||||
|
g.drawEllipse(146, 17, 157, 28);
|
||||||
|
g.drawEllipse(146, 5, 157, 16);
|
||||||
|
g.drawEllipse(162, 17, 173, 28);
|
||||||
|
g.drawEllipse(162, 5, 173, 16);
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Redraws the screen, except for the ghost piece.
|
||||||
|
*/
|
||||||
|
let redrawBoard = function() {
|
||||||
|
drawBackground();
|
||||||
|
drawNextQueue();
|
||||||
|
drawChainCount();
|
||||||
|
drawTime(false);
|
||||||
|
for (let y = 0; y < 12; y++) {
|
||||||
|
for (let x = 0; x < 6; x++) {
|
||||||
|
drawBlobAtSlot(x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
Toggles the pause screen.
|
||||||
|
*/
|
||||||
|
let togglePause = function() {
|
||||||
|
gamePaused = !gamePaused;
|
||||||
|
|
||||||
|
if (gamePaused) {
|
||||||
|
g.setBgColor(0.5, 0.2, 0.1);
|
||||||
|
g.clear();
|
||||||
|
drawTime(false);
|
||||||
|
|
||||||
|
g.setBgColor(0, 0, 0);
|
||||||
|
g.setColor(1, 1, 1);
|
||||||
|
g.clearRect(48, 66, 157, 110);
|
||||||
|
g.setFont("Vector", 20);
|
||||||
|
g.drawString("Tap here\nto unpause", 50, 68);
|
||||||
|
|
||||||
|
require("widget_utils").show();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
} else {
|
||||||
|
require("widget_utils").hide();
|
||||||
|
|
||||||
|
redrawBoard();
|
||||||
|
drawGhostPiece(false);
|
||||||
|
|
||||||
|
//Display the loss text if the game is lost.
|
||||||
|
if (gameLost) {
|
||||||
|
g.setBgColor(0, 0, 0);
|
||||||
|
g.setColor(1, 1, 1);
|
||||||
|
g.clearRect(33, 73, 142, 103);
|
||||||
|
g.setFont("Vector", 20);
|
||||||
|
g.drawString("You Lose", 43, 80);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ~~ Events ~~
|
||||||
|
let dragAmnt = 0;
|
||||||
|
|
||||||
|
let onTouch = (z, e) => {
|
||||||
|
if (midChain) return;
|
||||||
|
|
||||||
|
if (gamePaused) {
|
||||||
|
if (e.x >= 40 && e.y >= 58 && e.x <= 165 && e.y <= 118) {
|
||||||
|
g.setBgColor(1, 1, 1);
|
||||||
|
g.clearRect(48, 66, 157, 110);
|
||||||
|
g.flip();
|
||||||
|
togglePause();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//Tap reset button
|
||||||
|
if (e.x >= 143 && e.y >= 150) {
|
||||||
|
restartGame();
|
||||||
|
newPair();
|
||||||
|
redrawBoard();
|
||||||
|
drawGhostPiece(false);
|
||||||
|
g.flip();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Tap pause button
|
||||||
|
if (e.x <= 32 && e.y >= 150) {
|
||||||
|
togglePause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//While playing, rotate pieces.
|
||||||
|
if (!gameLost && !gamePaused) {
|
||||||
|
if (e.x < 88) {
|
||||||
|
rotatePair(false);
|
||||||
|
drawGhostPiece(true);
|
||||||
|
} else {
|
||||||
|
rotatePair(true);
|
||||||
|
drawGhostPiece(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Bangle.on("touch", onTouch);
|
||||||
|
|
||||||
|
let onDrag = (e) => {
|
||||||
|
if (gameLost || gamePaused || midChain) return;
|
||||||
|
|
||||||
|
//Do nothing if the user is dragging down so that they don't accidentally move while dropping
|
||||||
|
if (e.dy >= 5) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragAmnt += e.dx;
|
||||||
|
if (e.b == 0) {
|
||||||
|
dragAmnt = 0;
|
||||||
|
}
|
||||||
|
if (dragAmnt >= 20) {
|
||||||
|
movePair(Math.floor(dragAmnt / 20));
|
||||||
|
drawGhostPiece(true);
|
||||||
|
dragAmnt = dragAmnt % 20;
|
||||||
|
}
|
||||||
|
if (dragAmnt <= -20) {
|
||||||
|
movePair(Math.ceil(dragAmnt / 20));
|
||||||
|
drawGhostPiece(true);
|
||||||
|
dragAmnt = dragAmnt % 20;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Bangle.on("drag", onDrag);
|
||||||
|
|
||||||
|
let onSwipe = (x, y) => {
|
||||||
|
if (gameLost || gamePaused || midChain) return;
|
||||||
|
|
||||||
|
if (y > 0) {
|
||||||
|
let pivotX = dropCoordinates.pivotX;
|
||||||
|
let pivotY = dropCoordinates.pivotY;
|
||||||
|
let leafX = dropCoordinates.leafX;
|
||||||
|
let leafY = dropCoordinates.leafY;
|
||||||
|
|
||||||
|
if (pivotY < -1 && leafY < -1) return;
|
||||||
|
|
||||||
|
quickDrop();
|
||||||
|
drawBlobAtSlot(pivotX, pivotY);
|
||||||
|
drawBlobAtSlot(leafX, leafY);
|
||||||
|
g.flip();
|
||||||
|
|
||||||
|
//Check for pops
|
||||||
|
if (pivotY >= 0) addSlotToCheck(pivotX, pivotY);
|
||||||
|
if (leafY >= 0) addSlotToCheck(leafX, leafY);
|
||||||
|
midChain = true;
|
||||||
|
let currentChain = 0;
|
||||||
|
while (popAll().pops > 0) {
|
||||||
|
currentChain++;
|
||||||
|
lastChain = currentChain;
|
||||||
|
drawChainCount();
|
||||||
|
g.flip();
|
||||||
|
settleBlobs();
|
||||||
|
redrawBoard();
|
||||||
|
g.flip();
|
||||||
|
}
|
||||||
|
|
||||||
|
newPair();
|
||||||
|
drawNextQueue();
|
||||||
|
drawGhostPiece(false);
|
||||||
|
|
||||||
|
//If the top slot of the third column is taken, lose the game.
|
||||||
|
if (grid[0][2] != 0) {
|
||||||
|
gameLost = true;
|
||||||
|
g.setBgColor(0, 0, 0);
|
||||||
|
g.setColor(1, 1, 1);
|
||||||
|
g.clearRect(33, 73, 142, 103);
|
||||||
|
g.setFont("Vector", 20);
|
||||||
|
g.drawString("You Lose", 43, 80);
|
||||||
|
}
|
||||||
|
|
||||||
|
midChain = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Bangle.on("swipe", onSwipe);
|
||||||
|
|
||||||
|
let onLock = on => {
|
||||||
|
updateSeconds = !on;
|
||||||
|
drawTime(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
Bangle.on('lock', onLock);
|
||||||
|
|
||||||
|
let onCharging = charging => {
|
||||||
|
drawTime(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
Bangle.on('charging', onCharging);
|
||||||
|
|
||||||
|
Bangle.setUI({mode:"clock", remove:function() {
|
||||||
|
//Remove listeners
|
||||||
|
Bangle.removeListener("touch", onTouch);
|
||||||
|
Bangle.removeListener("drag", onDrag);
|
||||||
|
Bangle.removeListener("swipe", onSwipe);
|
||||||
|
Bangle.removeListener('lock', onLock);
|
||||||
|
Bangle.removeListener('charging', onCharging);
|
||||||
|
|
||||||
|
if (clockDrawTimeout) clearTimeout(clockDrawTimeout);
|
||||||
|
require("widget_utils").show();
|
||||||
|
}});
|
||||||
|
|
||||||
|
g.reset();
|
||||||
|
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
require("widget_utils").hide();
|
||||||
|
|
||||||
|
drawBackground();
|
||||||
|
drawTime(true);
|
||||||
|
|
||||||
|
restartGame();
|
||||||
|
|
||||||
|
newPair();
|
||||||
|
drawGhostPiece(false);
|
||||||
|
|
||||||
|
drawNextQueue();
|
||||||
|
drawChainCount();
|
||||||
|
}
|
After Width: | Height: | Size: 691 B |
|
@ -0,0 +1,15 @@
|
||||||
|
{ "id": "bblobface",
|
||||||
|
"name": "Bangle Blobs Clock",
|
||||||
|
"shortName":"BBClock",
|
||||||
|
"icon": "app.png",
|
||||||
|
"version": "1.00",
|
||||||
|
"description": "A fully featured watch face with a playable game on the side.",
|
||||||
|
"readme":"README.md",
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock, game",
|
||||||
|
"supports" : ["BANGLEJS2"],
|
||||||
|
"storage": [
|
||||||
|
{"name":"bblobface.app.js","url":"app.js"},
|
||||||
|
{"name":"bblobface.img","url":"app-icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
}
|
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 4.7 KiB |
|
@ -3,3 +3,4 @@
|
||||||
0.03: Use default Bangle formatter for booleans
|
0.03: Use default Bangle formatter for booleans
|
||||||
0.04: Add options for units in locale and recording GPS
|
0.04: Add options for units in locale and recording GPS
|
||||||
0.05: Allow toggling of "max" values (screen tap) and recording (button press)
|
0.05: Allow toggling of "max" values (screen tap) and recording (button press)
|
||||||
|
0.06: Fix local unit setting
|
||||||
|
|
|
@ -420,7 +420,7 @@ function updateClock() {
|
||||||
// Read settings.
|
// Read settings.
|
||||||
let cfg = require('Storage').readJSON('bikespeedo.json',1)||{};
|
let cfg = require('Storage').readJSON('bikespeedo.json',1)||{};
|
||||||
|
|
||||||
cfg.spd = !cfg.localeUnits; // Multiplier for speed unit conversions. 0 = use the locale values for speed
|
cfg.spd = cfg.localeUnits ? 0 : 1; // Multiplier for speed unit conversions. 0 = use the locale values for speed
|
||||||
cfg.spd_unit = 'km/h'; // Displayed speed unit
|
cfg.spd_unit = 'km/h'; // Displayed speed unit
|
||||||
cfg.alt = 1; // Multiplier for altitude unit conversions. (feet:'0.3048')
|
cfg.alt = 1; // Multiplier for altitude unit conversions. (feet:'0.3048')
|
||||||
cfg.alt_unit = 'm'; // Displayed altitude units ('feet')
|
cfg.alt_unit = 'm'; // Displayed altitude units ('feet')
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "bikespeedo",
|
"id": "bikespeedo",
|
||||||
"name": "Bike Speedometer (beta)",
|
"name": "Bike Speedometer (beta)",
|
||||||
"shortName": "Bike Speedometer",
|
"shortName": "Bike Speedometer",
|
||||||
"version": "0.05",
|
"version": "0.06",
|
||||||
"description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources",
|
"description": "Shows GPS speed, GPS heading, Compass heading, GPS altitude and Barometer altitude from internal sources",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"screenshots": [{"url":"Screenshot.png"}],
|
"screenshots": [{"url":"Screenshot.png"}],
|
||||||
|
|
|
@ -67,3 +67,4 @@
|
||||||
0.56: Settings.log = 0,1,2,3 for off,display, log, both
|
0.56: Settings.log = 0,1,2,3 for off,display, log, both
|
||||||
0.57: Handle the whitelist being disabled
|
0.57: Handle the whitelist being disabled
|
||||||
0.58: "Make Connectable" temporarily bypasses the whitelist
|
0.58: "Make Connectable" temporarily bypasses the whitelist
|
||||||
|
0.59: Whitelist: Try to resolve peer addresses using NRF.resolveAddress() - for 2v19 or 2v18 cutting edge builds
|
||||||
|
|
|
@ -79,7 +79,7 @@ if (global.save) boot += `global.save = function() { throw new Error("You can't
|
||||||
if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
|
if (s.options) boot+=`Bangle.setOptions(${E.toJS(s.options)});\n`;
|
||||||
if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
|
if (s.brightness && s.brightness!=1) boot+=`Bangle.setLCDBrightness(${s.brightness});\n`;
|
||||||
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`;
|
if (s.passkey!==undefined && s.passkey.length==6) boot+=`NRF.setSecurity({passkey:${E.toJS(s.passkey.toString())}, mitm:1, display:1});\n`;
|
||||||
if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist && !(require('Storage').readJSON('setting.json',1)||{}).whitelist.includes(addr)) NRF.disconnect(); });\n`;
|
if (s.whitelist && !s.whitelist_disabled) boot+=`NRF.on('connect', function(addr) { if (!NRF.ignoreWhitelist) { let whitelist = (require('Storage').readJSON('setting.json',1)||{}).whitelist; if (NRF.resolveAddress !== undefined) { let resolvedAddr = NRF.resolveAddress(addr); if (resolvedAddr !== undefined) addr = resolvedAddr + " (resolved)"; } if (!whitelist.includes(addr)) NRF.disconnect(); }});\n`;
|
||||||
if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
|
if (s.rotate) boot+=`g.setRotation(${s.rotate&3},${s.rotate>>2});\n` // screen rotation
|
||||||
// ================================================== FIXING OLDER FIRMWARES
|
// ================================================== FIXING OLDER FIRMWARES
|
||||||
if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted.
|
if (FWVERSION<215.068) // 2v15.68 and before had compass heading inverted.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "boot",
|
"id": "boot",
|
||||||
"name": "Bootloader",
|
"name": "Bootloader",
|
||||||
"version": "0.58",
|
"version": "0.59",
|
||||||
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
|
||||||
"icon": "bootloader.png",
|
"icon": "bootloader.png",
|
||||||
"type": "bootloader",
|
"type": "bootloader",
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
0.01: Initial release.
|
0.01: Initial release.
|
||||||
|
0.02: Handle the case where other apps have set bleAdvert to an array
|
||||||
|
|
|
@ -1,6 +1,22 @@
|
||||||
(() => {
|
(() => {
|
||||||
function advertiseBattery() {
|
function advertiseBattery() {
|
||||||
Bangle.bleAdvert[0x180F] = [E.getBattery()];
|
if(Array.isArray(Bangle.bleAdvert)){
|
||||||
|
// ensure we're in the cycle
|
||||||
|
var found = false;
|
||||||
|
for(var ad in Bangle.bleAdvert){
|
||||||
|
if(ad[0x180F]){
|
||||||
|
ad[0x180F] = [E.getBattery()];
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!found)
|
||||||
|
Bangle.bleAdvert.push({ 0x180F: [E.getBattery()] });
|
||||||
|
}else{
|
||||||
|
// simple object
|
||||||
|
Bangle.bleAdvert[0x180F] = [E.getBattery()];
|
||||||
|
}
|
||||||
|
|
||||||
NRF.setAdvertising(Bangle.bleAdvert);
|
NRF.setAdvertising(Bangle.bleAdvert);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "bootgattbat",
|
"id": "bootgattbat",
|
||||||
"name": "BLE GATT Battery Service",
|
"name": "BLE GATT Battery Service",
|
||||||
"shortName": "BLE Battery Service",
|
"shortName": "BLE Battery Service",
|
||||||
"version": "0.01",
|
"version": "0.02",
|
||||||
"description": "Adds the GATT Battery Service to advertise the percentage of battery currently remaining over Bluetooth.\n",
|
"description": "Adds the GATT Battery Service to advertise the percentage of battery currently remaining over Bluetooth.\n",
|
||||||
"icon": "bluetooth.png",
|
"icon": "bluetooth.png",
|
||||||
"type": "bootloader",
|
"type": "bootloader",
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
0.01: New app!
|
0.01: New app!
|
||||||
|
0.02: Advertise accelerometer data and sensor location
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
|
var _a;
|
||||||
{
|
{
|
||||||
var __assign = Object.assign;
|
var __assign = Object.assign;
|
||||||
var Layout_1 = require("Layout");
|
var Layout_1 = require("Layout");
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
Bangle.drawWidgets();
|
Bangle.drawWidgets();
|
||||||
var HRM_MIN_CONFIDENCE_1 = 75;
|
var HRM_MIN_CONFIDENCE_1 = 75;
|
||||||
var services_1 = ["0x180d", "0x181a", "0x1819"];
|
var services_1 = [
|
||||||
|
"0x180d",
|
||||||
|
"0x181a",
|
||||||
|
"0x1819",
|
||||||
|
"E95D0753251D470AA062FA1922DFA9A8",
|
||||||
|
];
|
||||||
var acc_1;
|
var acc_1;
|
||||||
var bar_1;
|
var bar_1;
|
||||||
var gps_1;
|
var gps_1;
|
||||||
|
@ -21,7 +27,6 @@
|
||||||
mag: false,
|
mag: false,
|
||||||
};
|
};
|
||||||
var idToName = {
|
var idToName = {
|
||||||
acc: "Acceleration",
|
|
||||||
bar: "Barometer",
|
bar: "Barometer",
|
||||||
gps: "GPS",
|
gps: "GPS",
|
||||||
hrm: "HRM",
|
hrm: "HRM",
|
||||||
|
@ -69,7 +74,6 @@
|
||||||
{
|
{
|
||||||
type: "h",
|
type: "h",
|
||||||
c: [
|
c: [
|
||||||
__assign(__assign({ type: "btn", label: idToName.acc, id: "acc", cb: function () { } }, btnStyle), { col: colour_1.on, btnBorder: colour_1.on }),
|
|
||||||
__assign({ type: "btn", label: "Back", cb: function () {
|
__assign({ type: "btn", label: "Back", cb: function () {
|
||||||
setBtnsShown_1(false);
|
setBtnsShown_1(false);
|
||||||
} }, btnStyle),
|
} }, btnStyle),
|
||||||
|
@ -222,6 +226,13 @@
|
||||||
return [x[0], x[1], y[0], y[1], z[0], z[1]];
|
return [x[0], x[1], y[0], y[1], z[0], z[1]];
|
||||||
};
|
};
|
||||||
encodeMag_1.maxLen = 6;
|
encodeMag_1.maxLen = 6;
|
||||||
|
var encodeAcc_1 = function (data) {
|
||||||
|
var x = toByteArray_1(data.x * 1000, 2, true);
|
||||||
|
var y = toByteArray_1(data.y * 1000, 2, true);
|
||||||
|
var z = toByteArray_1(data.z * 1000, 2, true);
|
||||||
|
return [x[0], x[1], y[0], y[1], z[0], z[1]];
|
||||||
|
};
|
||||||
|
encodeAcc_1.maxLen = 6;
|
||||||
var toByteArray_1 = function (value, numberOfBytes, isSigned) {
|
var toByteArray_1 = function (value, numberOfBytes, isSigned) {
|
||||||
var byteArray = new Array(numberOfBytes);
|
var byteArray = new Array(numberOfBytes);
|
||||||
if (isSigned && (value < 0)) {
|
if (isSigned && (value < 0)) {
|
||||||
|
@ -251,6 +262,7 @@
|
||||||
case "0x180d": return !!hrm_1;
|
case "0x180d": return !!hrm_1;
|
||||||
case "0x181a": return !!(bar_1 || mag_1);
|
case "0x181a": return !!(bar_1 || mag_1);
|
||||||
case "0x1819": return !!(gps_1 && gps_1.lat && gps_1.lon || mag_1);
|
case "0x1819": return !!(gps_1 && gps_1.lat && gps_1.lon || mag_1);
|
||||||
|
case "E95D0753251D470AA062FA1922DFA9A8": return !!acc_1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var serviceToAdvert_1 = function (serv, initial) {
|
var serviceToAdvert_1 = function (serv, initial) {
|
||||||
|
@ -264,11 +276,20 @@
|
||||||
readable: true,
|
readable: true,
|
||||||
notify: true,
|
notify: true,
|
||||||
};
|
};
|
||||||
|
var os = {
|
||||||
|
maxLen: 1,
|
||||||
|
readable: true,
|
||||||
|
notify: true,
|
||||||
|
};
|
||||||
if (hrm_1) {
|
if (hrm_1) {
|
||||||
o.value = encodeHrm_1(hrm_1);
|
o.value = encodeHrm_1(hrm_1);
|
||||||
|
os.value = [2];
|
||||||
hrm_1 = undefined;
|
hrm_1 = undefined;
|
||||||
}
|
}
|
||||||
return _a = {}, _a["0x2a37"] = o, _a;
|
return _a = {},
|
||||||
|
_a["0x2a37"] = o,
|
||||||
|
_a["0x2a38"] = os,
|
||||||
|
_a;
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
case "0x1819":
|
case "0x1819":
|
||||||
|
@ -331,6 +352,21 @@
|
||||||
}
|
}
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
case "E95D0753251D470AA062FA1922DFA9A8": {
|
||||||
|
var o = {};
|
||||||
|
if (acc_1 || initial) {
|
||||||
|
o["E95DCA4B251D470AA062FA1922DFA9A8"] = {
|
||||||
|
maxLen: encodeAcc_1.maxLen,
|
||||||
|
readable: true,
|
||||||
|
notify: true,
|
||||||
|
};
|
||||||
|
if (acc_1) {
|
||||||
|
o["E95DCA4B251D470AA062FA1922DFA9A8"].value = encodeAcc_1(acc_1);
|
||||||
|
acc_1 = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
var getBleAdvert_1 = function (map, all) {
|
var getBleAdvert_1 = function (map, all) {
|
||||||
|
@ -402,12 +438,23 @@
|
||||||
enableSensors_1();
|
enableSensors_1();
|
||||||
{
|
{
|
||||||
var ad = getBleAdvert_1(function (serv) { return serviceToAdvert_1(serv, true); }, true);
|
var ad = getBleAdvert_1(function (serv) { return serviceToAdvert_1(serv, true); }, true);
|
||||||
var adServices = Object
|
|
||||||
.keys(ad)
|
|
||||||
.map(function (k) { return k.replace("0x", ""); });
|
|
||||||
NRF.setServices(ad, {
|
NRF.setServices(ad, {
|
||||||
advertise: adServices,
|
|
||||||
uart: false,
|
uart: false,
|
||||||
});
|
});
|
||||||
|
var bangle2 = Bangle;
|
||||||
|
var cycle = Array.isArray(bangle2.bleAdvert) ? bangle2.bleAdvert : [];
|
||||||
|
for (var id in ad) {
|
||||||
|
var serv = ad[id];
|
||||||
|
var value = void 0;
|
||||||
|
for (var ch in serv) {
|
||||||
|
value = serv[ch].value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cycle.push((_a = {}, _a[id] = value || [], _a));
|
||||||
|
}
|
||||||
|
bangle2.bleAdvert = cycle;
|
||||||
|
NRF.setAdvertising(cycle, {
|
||||||
|
interval: 100,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,16 +33,27 @@ const enum BleServ {
|
||||||
// contains: LocationAndSpeed
|
// contains: LocationAndSpeed
|
||||||
LocationAndNavigation = "0x1819",
|
LocationAndNavigation = "0x1819",
|
||||||
|
|
||||||
// Acc // none known for this
|
// org.microbit.service.accelerometer
|
||||||
|
// contains: Acc
|
||||||
|
Acc = "E95D0753251D470AA062FA1922DFA9A8",
|
||||||
}
|
}
|
||||||
|
|
||||||
const services = [BleServ.HRM, BleServ.EnvSensing, BleServ.LocationAndNavigation];
|
const services = [
|
||||||
|
BleServ.HRM,
|
||||||
|
BleServ.EnvSensing,
|
||||||
|
BleServ.LocationAndNavigation,
|
||||||
|
BleServ.Acc,
|
||||||
|
];
|
||||||
|
|
||||||
const enum BleChar {
|
const enum BleChar {
|
||||||
// org.bluetooth.characteristic.heart_rate_measurement
|
// org.bluetooth.characteristic.heart_rate_measurement
|
||||||
// <see encode function>
|
// <see encode function>
|
||||||
HRM = "0x2a37",
|
HRM = "0x2a37",
|
||||||
|
|
||||||
|
// org.bluetooth.characteristic.body_sensor_location
|
||||||
|
// u8
|
||||||
|
SensorLocation = "0x2a38",
|
||||||
|
|
||||||
// org.bluetooth.characteristic.elevation
|
// org.bluetooth.characteristic.elevation
|
||||||
// s24, meters 0.01
|
// s24, meters 0.01
|
||||||
Elevation = "0x2a6c",
|
Elevation = "0x2a6c",
|
||||||
|
@ -65,6 +76,11 @@ const enum BleChar {
|
||||||
// org.bluetooth.characteristic.magnetic_flux_density_3d
|
// org.bluetooth.characteristic.magnetic_flux_density_3d
|
||||||
// s16: x, y, z, tesla (10^-7)
|
// s16: x, y, z, tesla (10^-7)
|
||||||
MagneticFlux3D = "0x2aa1",
|
MagneticFlux3D = "0x2aa1",
|
||||||
|
|
||||||
|
// org.microbit.characteristic.accelerometer_data
|
||||||
|
// s16 x3, -1024 .. 1024
|
||||||
|
// docs: https://lancaster-university.github.io/microbit-docs/ble/accelerometer-service/
|
||||||
|
Acc = "E95DCA4B251D470AA062FA1922DFA9A8",
|
||||||
}
|
}
|
||||||
|
|
||||||
type BleCharAdvert = {
|
type BleCharAdvert = {
|
||||||
|
@ -84,6 +100,16 @@ type LenFunc<T> = {
|
||||||
maxLen: number,
|
maxLen: number,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enum SensorLocations {
|
||||||
|
Other = 0,
|
||||||
|
Chest = 1,
|
||||||
|
Wrist = 2,
|
||||||
|
Finger = 3,
|
||||||
|
Hand = 4,
|
||||||
|
EarLobe = 5,
|
||||||
|
Foot = 6,
|
||||||
|
}
|
||||||
|
|
||||||
let acc: undefined | AccelData;
|
let acc: undefined | AccelData;
|
||||||
let bar: undefined | PressureData;
|
let bar: undefined | PressureData;
|
||||||
let gps: undefined | GPSFix;
|
let gps: undefined | GPSFix;
|
||||||
|
@ -104,8 +130,7 @@ const settings: BtAdvMap<boolean> = {
|
||||||
mag: false,
|
mag: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const idToName: BtAdvMap<string, true> = {
|
const idToName: BtAdvMap<string> = {
|
||||||
acc: "Acceleration",
|
|
||||||
bar: "Barometer",
|
bar: "Barometer",
|
||||||
gps: "GPS",
|
gps: "GPS",
|
||||||
hrm: "HRM",
|
hrm: "HRM",
|
||||||
|
@ -197,15 +222,6 @@ const btnLayout = new Layout(
|
||||||
{
|
{
|
||||||
type: "h",
|
type: "h",
|
||||||
c: [
|
c: [
|
||||||
{
|
|
||||||
type: "btn",
|
|
||||||
label: idToName.acc,
|
|
||||||
id: "acc",
|
|
||||||
cb: () => {},
|
|
||||||
...btnStyle,
|
|
||||||
col: colour.on,
|
|
||||||
btnBorder: colour.on,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "btn",
|
type: "btn",
|
||||||
label: "Back",
|
label: "Back",
|
||||||
|
@ -464,6 +480,15 @@ const encodeMag: LenFunc<CompassData> = (data: CompassData) => {
|
||||||
};
|
};
|
||||||
encodeMag.maxLen = 6;
|
encodeMag.maxLen = 6;
|
||||||
|
|
||||||
|
const encodeAcc: LenFunc<AccelData> = (data: AccelData) => {
|
||||||
|
const x = toByteArray(data.x * 1000, 2, true);
|
||||||
|
const y = toByteArray(data.y * 1000, 2, true);
|
||||||
|
const z = toByteArray(data.z * 1000, 2, true);
|
||||||
|
|
||||||
|
return [ x[0]!, x[1]!, y[0]!, y[1]!, z[0]!, z[1]! ];
|
||||||
|
};
|
||||||
|
encodeAcc.maxLen = 6;
|
||||||
|
|
||||||
const toByteArray = (value: number, numberOfBytes: number, isSigned: boolean) => {
|
const toByteArray = (value: number, numberOfBytes: number, isSigned: boolean) => {
|
||||||
const byteArray: Array<number> = new Array(numberOfBytes);
|
const byteArray: Array<number> = new Array(numberOfBytes);
|
||||||
|
|
||||||
|
@ -503,6 +528,7 @@ const haveServiceData = (serv: BleServ): boolean => {
|
||||||
case BleServ.HRM: return !!hrm;
|
case BleServ.HRM: return !!hrm;
|
||||||
case BleServ.EnvSensing: return !!(bar || mag);
|
case BleServ.EnvSensing: return !!(bar || mag);
|
||||||
case BleServ.LocationAndNavigation: return !!(gps && gps.lat && gps.lon || mag);
|
case BleServ.LocationAndNavigation: return !!(gps && gps.lat && gps.lon || mag);
|
||||||
|
case BleServ.Acc: return !!acc;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -515,12 +541,22 @@ const serviceToAdvert = (serv: BleServ, initial = false): BleServAdvert => {
|
||||||
readable: true,
|
readable: true,
|
||||||
notify: true,
|
notify: true,
|
||||||
};
|
};
|
||||||
|
const os: BleCharAdvert = {
|
||||||
|
maxLen: 1,
|
||||||
|
readable: true,
|
||||||
|
notify: true,
|
||||||
|
};
|
||||||
|
|
||||||
if (hrm) {
|
if (hrm) {
|
||||||
o.value = encodeHrm(hrm);
|
o.value = encodeHrm(hrm);
|
||||||
|
os.value = [SensorLocations.Wrist];
|
||||||
hrm = undefined;
|
hrm = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { [BleChar.HRM]: o };
|
return {
|
||||||
|
[BleChar.HRM]: o,
|
||||||
|
[BleChar.SensorLocation]: os,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
|
@ -591,6 +627,25 @@ const serviceToAdvert = (serv: BleServ, initial = false): BleServAdvert => {
|
||||||
|
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case BleServ.Acc: {
|
||||||
|
const o: BleServAdvert = {};
|
||||||
|
|
||||||
|
if (acc || initial) {
|
||||||
|
o[BleChar.Acc] = {
|
||||||
|
maxLen: encodeAcc.maxLen,
|
||||||
|
readable: true,
|
||||||
|
notify: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (acc) {
|
||||||
|
o[BleChar.Acc]!.value = encodeAcc(acc);
|
||||||
|
acc = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return o;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -702,16 +757,39 @@ enableSensors();
|
||||||
// must have fixed services from the start:
|
// must have fixed services from the start:
|
||||||
const ad = getBleAdvert(serv => serviceToAdvert(serv, true), /*all*/true);
|
const ad = getBleAdvert(serv => serviceToAdvert(serv, true), /*all*/true);
|
||||||
|
|
||||||
const adServices = Object
|
|
||||||
.keys(ad)
|
|
||||||
.map((k: string) => k.replace("0x", ""));
|
|
||||||
|
|
||||||
NRF.setServices(
|
NRF.setServices(
|
||||||
ad,
|
ad,
|
||||||
{
|
{
|
||||||
advertise: adServices,
|
|
||||||
uart: false,
|
uart: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type BleAdvert = { [key: string]: number[] };
|
||||||
|
const bangle2 = Bangle as {
|
||||||
|
bleAdvert?: BleAdvert | BleAdvert[];
|
||||||
|
};
|
||||||
|
const cycle = Array.isArray(bangle2.bleAdvert) ? bangle2.bleAdvert : [];
|
||||||
|
|
||||||
|
for(const id in ad){
|
||||||
|
const serv = ad[id as BleServ];
|
||||||
|
let value;
|
||||||
|
|
||||||
|
// pick the first characteristic to advertise
|
||||||
|
for(const ch in serv){
|
||||||
|
value = serv[ch as BleChar]!.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
cycle.push({ [id]: value || [] });
|
||||||
|
}
|
||||||
|
|
||||||
|
bangle2.bleAdvert = cycle;
|
||||||
|
|
||||||
|
NRF.setAdvertising(
|
||||||
|
cycle,
|
||||||
|
{
|
||||||
|
interval: 100,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"id": "btadv",
|
"id": "btadv",
|
||||||
"name": "btadv",
|
"name": "btadv",
|
||||||
"shortName": "btadv",
|
"shortName": "btadv",
|
||||||
"version": "0.01",
|
"version": "0.02",
|
||||||
"description": "Advertise & export live heart rate, accel, pressure, GPS & mag data over bluetooth",
|
"description": "Advertise & export live heart rate, accel, pressure, GPS & mag data over bluetooth",
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
"tags": "health,tool,sensors,bluetooth",
|
"tags": "health,tool,sensors,bluetooth",
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
|
0.02: Handle the case where other apps have set bleAdvert to an array
|
||||||
|
|
|
@ -23,7 +23,7 @@ function onTemperature(p) {
|
||||||
var temp100 = Math.round(avrTemp*100);
|
var temp100 = Math.round(avrTemp*100);
|
||||||
var pressure100 = Math.round(avrPressure*100);
|
var pressure100 = Math.round(avrPressure*100);
|
||||||
|
|
||||||
Bangle.bleAdvert[0xFCD2] = [ 0x40, /* BTHome Device Information
|
var advert = [ 0x40, /* BTHome Device Information
|
||||||
bit 0: "Encryption flag"
|
bit 0: "Encryption flag"
|
||||||
bit 1-4: "Reserved for future use"
|
bit 1-4: "Reserved for future use"
|
||||||
bit 5-7: "BTHome Version" */
|
bit 5-7: "BTHome Version" */
|
||||||
|
@ -37,6 +37,21 @@ function onTemperature(p) {
|
||||||
0x04, // Pressure, 16 bit
|
0x04, // Pressure, 16 bit
|
||||||
pressure100&255,(pressure100>>8)&255,pressure100>>16
|
pressure100&255,(pressure100>>8)&255,pressure100>>16
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if(Array.isArray(Bangle.bleAdvert)){
|
||||||
|
var found = false;
|
||||||
|
for(var ad in Bangle.bleAdvert){
|
||||||
|
if(ad[0xFCD2]){
|
||||||
|
ad[0xFCD2] = advert;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!found)
|
||||||
|
Bangle.bleAdvert.push({ 0xFCD2: advert });
|
||||||
|
}else{
|
||||||
|
Bangle.bleAdvert[0xFCD2] = advert;
|
||||||
|
}
|
||||||
NRF.setAdvertising(Bangle.bleAdvert);
|
NRF.setAdvertising(Bangle.bleAdvert);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{ "id": "bthometemp",
|
{ "id": "bthometemp",
|
||||||
"name": "BTHome Temperature and Pressure",
|
"name": "BTHome Temperature and Pressure",
|
||||||
"shortName":"BTHome T",
|
"shortName":"BTHome T",
|
||||||
"version":"0.01",
|
"version":"0.02",
|
||||||
"description": "Displays temperature and pressure, and advertises them over bluetooth using BTHome.io standard",
|
"description": "Displays temperature and pressure, and advertises them over bluetooth using BTHome.io standard",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"tags": "bthome,bluetooth,temperature",
|
"tags": "bthome,bluetooth,temperature",
|
||||||
|
|
|
@ -14,3 +14,4 @@
|
||||||
0.13: Switch to swipe left/right for month and up/down for year selection
|
0.13: Switch to swipe left/right for month and up/down for year selection
|
||||||
Display events for current month on touch
|
Display events for current month on touch
|
||||||
0.14: Add support for holidays
|
0.14: Add support for holidays
|
||||||
|
0.15: Edit holidays on device in settings
|
||||||
|
|
|
@ -76,11 +76,32 @@ getDowLbls = function(locale) {
|
||||||
};
|
};
|
||||||
|
|
||||||
sameDay = function(d1, d2) {
|
sameDay = function(d1, d2) {
|
||||||
|
"jit";
|
||||||
return d1.getFullYear() === d2.getFullYear() &&
|
return d1.getFullYear() === d2.getFullYear() &&
|
||||||
d1.getMonth() === d2.getMonth() &&
|
d1.getMonth() === d2.getMonth() &&
|
||||||
d1.getDate() === d2.getDate();
|
d1.getDate() === d2.getDate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
drawEvent = function(ev, curDay, x1, y1, x2, y2) {
|
||||||
|
"ram";
|
||||||
|
switch(ev.type) {
|
||||||
|
case "e": // alarm/event
|
||||||
|
const hour = 0|ev.date.getHours() + 0|ev.date.getMinutes()/60.0;
|
||||||
|
const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59
|
||||||
|
const height = (y2-2) - (y1+2); // height of a cell
|
||||||
|
const sliceHeight = height/eventsPerDay;
|
||||||
|
const ystart = (y1+2) + slice*sliceHeight;
|
||||||
|
g.setColor(bgEvent).fillRect(x1+1, ystart, x2-2, ystart+sliceHeight);
|
||||||
|
break;
|
||||||
|
case "h": // holiday
|
||||||
|
g.setColor(bgColorWeekend).fillRect(x1+1, y1+1, x2-1, y2-1);
|
||||||
|
break;
|
||||||
|
case "o": // other
|
||||||
|
g.setColor(bgOtherEvent).fillRect(x1+1, y1+1, x2-1, y2-1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
drawCalendar = function(date) {
|
drawCalendar = function(date) {
|
||||||
g.setBgColor(bgColor);
|
g.setBgColor(bgColor);
|
||||||
g.clearRect(0, 0, maxX, maxY);
|
g.clearRect(0, 0, maxX, maxY);
|
||||||
|
@ -119,7 +140,6 @@ drawCalendar = function(date) {
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
|
||||||
g.setFont("6x8", fontSize);
|
|
||||||
let dowLbls = getDowLbls(require('locale').name);
|
let dowLbls = getDowLbls(require('locale').name);
|
||||||
dowLbls.forEach((lbl, i) => {
|
dowLbls.forEach((lbl, i) => {
|
||||||
g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2);
|
g.drawString(lbl, i * colW + colW / 2, headerH + rowH / 2);
|
||||||
|
@ -173,6 +193,7 @@ drawCalendar = function(date) {
|
||||||
const eventsThisMonth = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth);
|
const eventsThisMonth = events.filter(ev => ev.date > weekBeforeMonth && ev.date < week2AfterMonth);
|
||||||
eventsThisMonth.sort((a,b) => a.date - b.date);
|
eventsThisMonth.sort((a,b) => a.date - b.date);
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
g.setFont("8x12", fontSize);
|
||||||
for (y = 0; y < rowN - 1; y++) {
|
for (y = 0; y < rowN - 1; y++) {
|
||||||
for (x = 0; x < colN; x++) {
|
for (x = 0; x < colN; x++) {
|
||||||
i++;
|
i++;
|
||||||
|
@ -189,22 +210,7 @@ drawCalendar = function(date) {
|
||||||
// Display events for this day
|
// Display events for this day
|
||||||
eventsThisMonth.forEach((ev, idx) => {
|
eventsThisMonth.forEach((ev, idx) => {
|
||||||
if (sameDay(ev.date, curDay)) {
|
if (sameDay(ev.date, curDay)) {
|
||||||
switch(ev.type) {
|
drawEvent(ev, curDay, x1, y1, x2, y2);
|
||||||
case "e": // alarm/event
|
|
||||||
const hour = ev.date.getHours() + ev.date.getMinutes()/60.0;
|
|
||||||
const slice = hour/24*(eventsPerDay-1); // slice 0 for 0:00 up to eventsPerDay for 23:59
|
|
||||||
const height = (y2-2) - (y1+2); // height of a cell
|
|
||||||
const sliceHeight = height/eventsPerDay;
|
|
||||||
const ystart = (y1+2) + slice*sliceHeight;
|
|
||||||
g.setColor(bgEvent).fillRect(x1+1, ystart, x2-2, ystart+sliceHeight);
|
|
||||||
break;
|
|
||||||
case "h": // holiday
|
|
||||||
g.setColor(bgColorWeekend).fillRect(x1+1, y1+1, x2-1, y2-1);
|
|
||||||
break;
|
|
||||||
case "o": // other
|
|
||||||
g.setColor(bgOtherEvent).fillRect(x1+1, y1+1, x2-1, y2-1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
eventsThisMonth.splice(idx, 1); // this event is no longer needed
|
eventsThisMonth.splice(idx, 1); // this event is no longer needed
|
||||||
}
|
}
|
||||||
|
@ -222,17 +228,15 @@ drawCalendar = function(date) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
require("Font8x12").add(Graphics);
|
|
||||||
g.setFont("8x12", fontSize);
|
|
||||||
g.setColor(day < 50 ? fgOtherMonth : fgSameMonth);
|
g.setColor(day < 50 ? fgOtherMonth : fgSameMonth);
|
||||||
g.drawString(
|
g.drawString(
|
||||||
(day > 50 ? day - 50 : day).toString(),
|
(day > 50 ? day - 50 : day).toString(),
|
||||||
x * colW + colW / 2,
|
x * colW + colW / 2,
|
||||||
headerH + rowH + y * rowH + rowH / 2
|
headerH + rowH + y * rowH + rowH / 2
|
||||||
);
|
);
|
||||||
}
|
} // end for (x = 0; x < colN; x++)
|
||||||
}
|
} // end for (y = 0; y < rowN - 1; y++)
|
||||||
};
|
}; // end function drawCalendar
|
||||||
|
|
||||||
showMenu = function() {
|
showMenu = function() {
|
||||||
const menu = {
|
const menu = {
|
||||||
|
@ -319,6 +323,7 @@ setUI = function() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
require("Font8x12").add(Graphics);
|
||||||
drawCalendar(date);
|
drawCalendar(date);
|
||||||
setUI();
|
setUI();
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
|
|
|
@ -28,11 +28,16 @@ function readFile(input) {
|
||||||
for(let i=0; i<input.files.length; i++) {
|
for(let i=0; i<input.files.length; i++) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.addEventListener("load", () => {
|
reader.addEventListener("load", () => {
|
||||||
const jCalData = ICAL.parse(reader.result);
|
const icalText = reader.result.substring(reader.result.indexOf("BEGIN:VCALENDAR")); // remove html before data
|
||||||
|
const jCalData = ICAL.parse(icalText);
|
||||||
const comp = new ICAL.Component(jCalData);
|
const comp = new ICAL.Component(jCalData);
|
||||||
|
const vtz = comp.getFirstSubcomponent('vtimezone');
|
||||||
|
const tz = new ICAL.Timezone(vtz);
|
||||||
|
|
||||||
// Fetch the VEVENT part
|
// Fetch the VEVENT part
|
||||||
comp.getAllSubcomponents('vevent').forEach(vevent => {
|
comp.getAllSubcomponents('vevent').forEach(vevent => {
|
||||||
event = new ICAL.Event(vevent);
|
const event = new ICAL.Event(vevent);
|
||||||
|
event.startDate.zone = tz;
|
||||||
holidays = holidays.filter(holiday => !sameDay(new Date(holiday.date), event.startDate.toJSDate())); // remove if already exists
|
holidays = holidays.filter(holiday => !sameDay(new Date(holiday.date), event.startDate.toJSDate())); // remove if already exists
|
||||||
|
|
||||||
const holiday = eventToHoliday(event);
|
const holiday = eventToHoliday(event);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "calendar",
|
"id": "calendar",
|
||||||
"name": "Calendar",
|
"name": "Calendar",
|
||||||
"version": "0.14",
|
"version": "0.15",
|
||||||
"description": "Simple calendar",
|
"description": "Simple calendar",
|
||||||
"icon": "calendar.png",
|
"icon": "calendar.png",
|
||||||
"screenshots": [{"url":"screenshot_calendar.png"}],
|
"screenshots": [{"url":"screenshot_calendar.png"}],
|
||||||
|
|
|
@ -1,27 +1,154 @@
|
||||||
(function (back) {
|
(function (back) {
|
||||||
const FILE = "calendar.json";
|
var FILE = "calendar.json";
|
||||||
const settings = require('Storage').readJSON(FILE, true) || {};
|
const HOLIDAY_FILE = "calendar.days.json";
|
||||||
|
var settings = require('Storage').readJSON(FILE, true) || {};
|
||||||
if (settings.ndColors === undefined)
|
if (settings.ndColors === undefined)
|
||||||
if (process.env.HWVERSION == 2) {
|
if (process.env.HWVERSION == 2) {
|
||||||
settings.ndColors = true;
|
settings.ndColors = true;
|
||||||
} else {
|
} else {
|
||||||
settings.ndColors = false;
|
settings.ndColors = false;
|
||||||
}
|
}
|
||||||
|
const holidays = require("Storage").readJSON(HOLIDAY_FILE,1).sort((a,b) => new Date(a.date) - new Date(b.date)) || [];
|
||||||
|
|
||||||
function writeSettings() {
|
function writeSettings() {
|
||||||
require('Storage').writeJSON(FILE, settings);
|
require('Storage').writeJSON(FILE, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
E.showMenu({
|
function writeHolidays() {
|
||||||
"": { "title": "Calendar" },
|
holidays.sort((a,b) => new Date(a.date) - new Date(b.date));
|
||||||
"< Back": () => back(),
|
require('Storage').writeJSON(HOLIDAY_FILE, holidays);
|
||||||
'B2 Colors': {
|
}
|
||||||
value: settings.ndColors,
|
|
||||||
onchange: v => {
|
|
||||||
settings.ndColors = v;
|
|
||||||
writeSettings();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
|
function formatDate(d) {
|
||||||
|
return d.getFullYear() + "-" + (d.getMonth() + 1).toString().padStart(2, '0') + "-" + d.getDate().toString().padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
const editdate = (i) => {
|
||||||
|
const holiday = holidays[i];
|
||||||
|
const date = new Date(holiday.date);
|
||||||
|
const dateStr = require("locale").date(date, 1);
|
||||||
|
const menu = {
|
||||||
|
"": { "title" : holiday.name},
|
||||||
|
"< Back": () => {
|
||||||
|
writeHolidays();
|
||||||
|
editdates();
|
||||||
|
},
|
||||||
|
/*LANG*/"Day": {
|
||||||
|
value: date ? date.getDate() : null,
|
||||||
|
min: 1,
|
||||||
|
max: 31,
|
||||||
|
wrap: true,
|
||||||
|
onchange: v => {
|
||||||
|
date.setDate(v);
|
||||||
|
holiday.date = formatDate(date);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/*LANG*/"Month": {
|
||||||
|
value: date ? date.getMonth() + 1 : null,
|
||||||
|
format: v => require("date_utils").month(v),
|
||||||
|
onchange: v => {
|
||||||
|
date.setMonth((v+11)%12);
|
||||||
|
holiday.date = formatDate(date);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/*LANG*/"Year": {
|
||||||
|
value: date ? date.getFullYear() : null,
|
||||||
|
min: 1900,
|
||||||
|
max: 2100,
|
||||||
|
onchange: v => {
|
||||||
|
date.setFullYear(v);
|
||||||
|
holiday.date = formatDate(date);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/*LANG*/"Name": () => {
|
||||||
|
require("textinput").input({text:holiday.name}).then(result => {
|
||||||
|
holiday.name = result;
|
||||||
|
editdate(i);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
/*LANG*/"Type": {
|
||||||
|
value: function() {
|
||||||
|
switch(holiday.type) {
|
||||||
|
case 'h': return 0;
|
||||||
|
case 'o': return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}(),
|
||||||
|
min: 0, max: 1,
|
||||||
|
format: v => [/*LANG*/"Holiday", /*LANG*/"Other"][v],
|
||||||
|
onchange: v => {
|
||||||
|
holiday.type = function() {
|
||||||
|
switch(v) {
|
||||||
|
case 0: return 'h';
|
||||||
|
case 1: return 'o';
|
||||||
|
}
|
||||||
|
}();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/*LANG*/"Repeat": {
|
||||||
|
value: !!holiday.repeat,
|
||||||
|
format: v => v ? /*LANG*/"Yearly" : /*LANG*/"Never",
|
||||||
|
onchange: v => {
|
||||||
|
holiday.repeat = v ? 'y' : undefined;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/*LANG*/"Delete": () => E.showPrompt(/*LANG*/"Delete" + " " + menu[""].title + "?").then(function(v) {
|
||||||
|
if (v) {
|
||||||
|
holidays.splice(i, 1);
|
||||||
|
writeHolidays();
|
||||||
|
editdates();
|
||||||
|
} else {
|
||||||
|
editday(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
require("textinput");
|
||||||
|
} catch(e) {
|
||||||
|
// textinput not installed
|
||||||
|
delete menu[/*LANG*/"Name"];
|
||||||
|
}
|
||||||
|
|
||||||
|
E.showMenu(menu);
|
||||||
|
};
|
||||||
|
|
||||||
|
const editdates = () => {
|
||||||
|
const menu = holidays.map((holiday,i) => {
|
||||||
|
const date = new Date(holiday.date);
|
||||||
|
const dateStr = require("locale").date(date, 1);
|
||||||
|
return {
|
||||||
|
title: dateStr + ' ' + holiday.name,
|
||||||
|
onchange: v => setTimeout(() => editdate(i), 10),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
menu[''] = { 'title': 'Holidays' };
|
||||||
|
menu['< Back'] = ()=>settingsmenu();
|
||||||
|
E.showMenu(menu);
|
||||||
|
};
|
||||||
|
|
||||||
|
const settingsmenu = () => {
|
||||||
|
E.showMenu({
|
||||||
|
"": { "title": "Calendar" },
|
||||||
|
"< Back": () => back(),
|
||||||
|
'B2 Colors': {
|
||||||
|
value: settings.ndColors,
|
||||||
|
onchange: v => {
|
||||||
|
settings.ndColors = v;
|
||||||
|
writeSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/*LANG*/"Edit Holidays": () => editdates(),
|
||||||
|
/*LANG*/"Add Holiday": () => {
|
||||||
|
holidays.push({
|
||||||
|
"date":formatDate(new Date()),
|
||||||
|
"name":/*LANG*/"New",
|
||||||
|
"type":'h',
|
||||||
|
});
|
||||||
|
editdate(holidays.length-1);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
settingsmenu();
|
||||||
|
})
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* JS source adapted from https://github.com/lindell/JsBarcode
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2016 Johan Lindell (johan@lindell.me)
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to
|
||||||
|
* deal in the Software without restriction, including without limitation the
|
||||||
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
* sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
* IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Barcode{
|
||||||
|
constructor(data, options){
|
||||||
|
this.data = data;
|
||||||
|
this.text = options.text || data;
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Barcode;
|
|
@ -0,0 +1 @@
|
||||||
|
0.01: Simple app to display loyalty cards
|
|
@ -0,0 +1,32 @@
|
||||||
|
# Cards
|
||||||
|
|
||||||
|
Simple app to display loyalty cards synced from Catima through GadgetBridge.
|
||||||
|
The app can display the cards' info (balance, expiration, note, etc.) and tapping on the appropriate field will display the code, if the type is supported.
|
||||||
|
|
||||||
|
To come back to the visualization of the card's details from the code view, simply press the button.
|
||||||
|
|
||||||
|
Beware that the small screen of the Banglejs 2 cannot render properly complex barcodes (in fact the resolution is very limited to render most barcodes).
|
||||||
|
|
||||||
|
### Supported codes types
|
||||||
|
|
||||||
|
* `CODE_39`
|
||||||
|
* `CODABAR`
|
||||||
|
* `QR_CODE`
|
||||||
|
|
||||||
|
### Disclaimer
|
||||||
|
|
||||||
|
This app is a proof of concept, many codes are too complex to be rendered by the bangle's screen or hardware (at least with the current logic), keep that in mind.
|
||||||
|
|
||||||
|
### How to sync
|
||||||
|
|
||||||
|
We can synchronize cards with GadgetBridge and Catima, refer to those projects for further information.
|
||||||
|
The feature is currently available on nightly builds only.
|
||||||
|
It should be released from version 0.77 (not yet out at the time of writing).
|
||||||
|
|
||||||
|
GadgetBridge syncronizes all cards at once, if you have too many cards you may want to explicitly select which ones to syncronize, keep in mind the limitations of the Banglejs.
|
||||||
|
|
||||||
|
### Credits
|
||||||
|
|
||||||
|
Barcode generation adapted from [lindell/JsBarcode](https://github.com/lindell/JsBarcode)
|
||||||
|
|
||||||
|
QR code generation adapted from [ricmoo/QRCode](https://github.com/ricmoo/QRCode)
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwxH+AH4A/AH4A/AH4AYoIAjF/4v/F/4v/F/4v/FAdNAAsoADgv/F/4v/F/4vqu4AjF/4v/F/4v6poAjF/4AfFAYAGF/4v/F/4v/F/4v/F94A/AH4A/AH4A/ABo"))
|
|
@ -0,0 +1,210 @@
|
||||||
|
/* CARDS is a list of:
|
||||||
|
{id:int,
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
type,
|
||||||
|
expiration,
|
||||||
|
color,
|
||||||
|
balance,
|
||||||
|
note,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
|
||||||
|
//may make it configurable in the future
|
||||||
|
const WHITE=-1
|
||||||
|
const BLACK=0
|
||||||
|
|
||||||
|
var FILE = "android.cards.json";
|
||||||
|
|
||||||
|
var Locale = require("locale");
|
||||||
|
|
||||||
|
var fontSmall = "6x8";
|
||||||
|
var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2";
|
||||||
|
var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2";
|
||||||
|
var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4";
|
||||||
|
|
||||||
|
var CARDS = require("Storage").readJSON("android.cards.json",true)||[];
|
||||||
|
var settings = require("Storage").readJSON("cards.settings.json",true)||{};
|
||||||
|
|
||||||
|
function getDate(timestamp) {
|
||||||
|
return new Date(timestamp*1000);
|
||||||
|
}
|
||||||
|
function formatDay(date) {
|
||||||
|
let formattedDate = Locale.dow(date,1) + " " + Locale.date(date).replace(/\d\d\d\d/,"");
|
||||||
|
if (!settings.useToday) {
|
||||||
|
return formattedDate;
|
||||||
|
}
|
||||||
|
const today = new Date(Date.now());
|
||||||
|
if (date.getDay() == today.getDay() && date.getMonth() == today.getMonth())
|
||||||
|
return /*LANG*/"Today ";
|
||||||
|
else {
|
||||||
|
const tomorrow = new Date(Date.now() + 86400 * 1000);
|
||||||
|
if (date.getDay() == tomorrow.getDay() && date.getMonth() == tomorrow.getMonth()) {
|
||||||
|
return /*LANG*/"Tomorrow ";
|
||||||
|
}
|
||||||
|
return formattedDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getColor(intColor) {
|
||||||
|
return "#"+(0x1000000+Number(intColor)).toString(16).padStart(6,"0");
|
||||||
|
}
|
||||||
|
function isLight(color) {
|
||||||
|
var r = +("0x"+color.slice(1,3));
|
||||||
|
var g = +("0x"+color.slice(3,5));
|
||||||
|
var b = +("0x"+color.slice(5,7));
|
||||||
|
var threshold = 0x88 * 3;
|
||||||
|
return (r+g+b) > threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printSquareCode(binary, size) {
|
||||||
|
var padding = 5;
|
||||||
|
var ratio = (g.getWidth()-(2*padding))/size;
|
||||||
|
for (var y = 0; y < size; y++) {
|
||||||
|
for (var x = 0; x < size; x++) {
|
||||||
|
if (binary[x + y * size]) {
|
||||||
|
g.setColor(BLACK).fillRect({x:x*ratio+padding, y:y*ratio+padding, w:ratio, h:ratio});
|
||||||
|
} else {
|
||||||
|
g.setColor(WHITE).fillRect({x:x*ratio+padding, y:y*ratio+padding, w:ratio, h:ratio});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function printLinearCode(binary) {
|
||||||
|
var yFrom = 15;
|
||||||
|
var yTo = 28;
|
||||||
|
var width = g.getWidth()/binary.length;
|
||||||
|
for(var b = 0; b < binary.length; b++){
|
||||||
|
var x = b * width;
|
||||||
|
if(binary[b] === "1"){
|
||||||
|
g.setColor(BLACK).fillRect({x:x, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)});
|
||||||
|
}
|
||||||
|
else if(binary[b]){
|
||||||
|
g.setColor(WHITE).fillRect({x:x, y:yFrom, w:width, h:g.getHeight() - (yTo+yFrom)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCode(card) {
|
||||||
|
E.showScroller();
|
||||||
|
// keeping it on rising edge would come back twice..
|
||||||
|
setWatch(()=>showCard(card), BTN, {edge:"falling"});
|
||||||
|
// theme independent
|
||||||
|
g.setColor(WHITE).fillRect(0, 0, g.getWidth(), g.getHeight());
|
||||||
|
switch (card.type) {
|
||||||
|
case "QR_CODE": {
|
||||||
|
const getBinaryQR = require("cards.qrcode.js");
|
||||||
|
let code = getBinaryQR(card.value);
|
||||||
|
printSquareCode(code.data, code.size);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "CODE_39": {
|
||||||
|
g.setFont("Vector:20");
|
||||||
|
g.setFontAlign(0,1).setColor(BLACK);
|
||||||
|
g.drawString(card.value, g.getWidth()/2, g.getHeight());
|
||||||
|
const CODE39 = require("cards.code39.js");
|
||||||
|
let code = new CODE39(card.value, {});
|
||||||
|
printLinearCode(code.encode().data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "CODABAR": {
|
||||||
|
g.setFont("Vector:20");
|
||||||
|
g.setFontAlign(0,1).setColor(BLACK);
|
||||||
|
g.drawString(card.value, g.getWidth()/2, g.getHeight());
|
||||||
|
const codabar = require("cards.codabar.js");
|
||||||
|
let code = new codabar(card.value, {});
|
||||||
|
printLinearCode(code.encode().data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
g.clear(true);
|
||||||
|
g.setFont("Vector:30");
|
||||||
|
g.setFontAlign(0,0);
|
||||||
|
g.drawString(card.value, g.getWidth()/2, g.getHeight()/2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCard(card) {
|
||||||
|
var lines = [];
|
||||||
|
var bodyFont = fontBig;
|
||||||
|
if(!card) return;
|
||||||
|
g.setFont(bodyFont);
|
||||||
|
//var lines = [];
|
||||||
|
if (card.name) lines = g.wrapString(card.name, g.getWidth()-10);
|
||||||
|
var titleCnt = lines.length;
|
||||||
|
var start = getDate(card.expiration);
|
||||||
|
var includeDay = true;
|
||||||
|
lines = lines.concat("", /*LANG*/"View code");
|
||||||
|
var valueLine = lines.length - 1;
|
||||||
|
if (card.expiration)
|
||||||
|
lines = lines.concat("",/*LANG*/"Expires"+": ", g.wrapString(formatDay(getDate(card.expiration)), g.getWidth()-10));
|
||||||
|
if(card.balance)
|
||||||
|
lines = lines.concat("",/*LANG*/"Balance"+": ", g.wrapString(card.balance, g.getWidth()-10));
|
||||||
|
if(card.note && card.note.trim())
|
||||||
|
lines = lines.concat("",g.wrapString(card.note, g.getWidth()-10));
|
||||||
|
lines = lines.concat("",/*LANG*/"< Back");
|
||||||
|
var titleBgColor = card.color ? getColor(card.color) : g.theme.bg2;
|
||||||
|
var titleColor = g.theme.fg2;
|
||||||
|
if (card.color)
|
||||||
|
titleColor = isLight(titleBgColor) ? BLACK : WHITE;
|
||||||
|
E.showScroller({
|
||||||
|
h : g.getFontHeight(), // height of each menu item in pixels
|
||||||
|
c : lines.length, // number of menu items
|
||||||
|
// a function to draw a menu item
|
||||||
|
draw : function(idx, r) {
|
||||||
|
// FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12
|
||||||
|
g.setBgColor(idx<titleCnt ? titleBgColor : g.theme.bg).
|
||||||
|
setColor(idx<titleCnt ? titleColor : g.theme.fg).
|
||||||
|
clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
|
||||||
|
g.setFont(bodyFont).drawString(lines[idx], r.x, r.y);
|
||||||
|
}, select : function(idx) {
|
||||||
|
if (idx>=lines.length-2)
|
||||||
|
showList();
|
||||||
|
else if (idx==valueLine)
|
||||||
|
showCode(card);
|
||||||
|
},
|
||||||
|
back : () => showList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://github.com/metafloor/bwip-js
|
||||||
|
// https://github.com/lindell/JsBarcode
|
||||||
|
|
||||||
|
function showList() {
|
||||||
|
if(CARDS.length == 0) {
|
||||||
|
E.showMessage(/*LANG*/"No cards");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
E.showScroller({
|
||||||
|
h : 52,
|
||||||
|
c : Math.max(CARDS.length,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11)
|
||||||
|
draw : function(idx, r) {"ram"
|
||||||
|
var card = CARDS[idx];
|
||||||
|
g.setColor(g.theme.fg);
|
||||||
|
g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
|
||||||
|
if (!card) return;
|
||||||
|
var isPast = false;
|
||||||
|
var x = r.x+2, name = card.name;
|
||||||
|
var body = card.expiration ? formatDay(getDate(card.expiration)) : "";
|
||||||
|
if (card.balance) body += "\n" + card.balance;
|
||||||
|
if (name) g.setFontAlign(-1,-1).setFont(fontBig)
|
||||||
|
.setColor(isPast ? "#888" : g.theme.fg).drawString(name, x+4,r.y+2);
|
||||||
|
if (body) {
|
||||||
|
g.setFontAlign(-1,-1).setFont(fontMedium).setColor(isPast ? "#888" : g.theme.fg);
|
||||||
|
g.drawString(body, x+10,r.y+20);
|
||||||
|
}
|
||||||
|
g.setColor("#888").fillRect(r.x,r.y+r.h-1,r.x+r.w-1,r.y+r.h-1); // dividing line between items
|
||||||
|
if(card.color) {
|
||||||
|
g.setColor(getColor(card.color));
|
||||||
|
g.fillRect(r.x,r.y+4,r.x+3, r.y+r.h-4);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
select : idx => showCard(CARDS[idx]),
|
||||||
|
back : () => load()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
showList();
|
After Width: | Height: | Size: 218 B |
|
@ -0,0 +1,88 @@
|
||||||
|
// Encoding specification:
|
||||||
|
// http://www.barcodeisland.com/codabar.phtml
|
||||||
|
/*
|
||||||
|
* JS source adapted from https://github.com/lindell/JsBarcode
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2016 Johan Lindell (johan@lindell.me)
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to
|
||||||
|
* deal in the Software without restriction, including without limitation the
|
||||||
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
* sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
* IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Barcode = require("cards.Barcode.js");
|
||||||
|
|
||||||
|
class codabar extends Barcode{
|
||||||
|
constructor(data, options){
|
||||||
|
if (/^[0-9\-\$\:\.\+\/]+$/.test(data)) {
|
||||||
|
data = "A" + data + "A";
|
||||||
|
}
|
||||||
|
|
||||||
|
super(data.toUpperCase(), options);
|
||||||
|
|
||||||
|
this.text = this.options.text || this.text.replace(/[A-D]/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
valid(){
|
||||||
|
return /^[A-D][0-9\-\$\:\.\+\/]+[A-D]$/.test(this.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
encode(){
|
||||||
|
var result = [];
|
||||||
|
var encodings = this.getEncodings();
|
||||||
|
for(var i = 0; i < this.data.length; i++){
|
||||||
|
result.push(encodings[this.data.charAt(i)]);
|
||||||
|
// for all characters except the last, append a narrow-space ("0")
|
||||||
|
if (i !== this.data.length - 1) {
|
||||||
|
result.push("0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
text: this.text,
|
||||||
|
data: result.join('')
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getEncodings(){
|
||||||
|
return {
|
||||||
|
"0": "101010011",
|
||||||
|
"1": "101011001",
|
||||||
|
"2": "101001011",
|
||||||
|
"3": "110010101",
|
||||||
|
"4": "101101001",
|
||||||
|
"5": "110101001",
|
||||||
|
"6": "100101011",
|
||||||
|
"7": "100101101",
|
||||||
|
"8": "100110101",
|
||||||
|
"9": "110100101",
|
||||||
|
"-": "101001101",
|
||||||
|
"$": "101100101",
|
||||||
|
":": "1101011011",
|
||||||
|
"/": "1101101011",
|
||||||
|
".": "1101101101",
|
||||||
|
"+": "1011011011",
|
||||||
|
"A": "1011001001",
|
||||||
|
"B": "1001001011",
|
||||||
|
"C": "1010010011",
|
||||||
|
"D": "1010011001"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = codabar
|
|
@ -0,0 +1,130 @@
|
||||||
|
// Encoding documentation:
|
||||||
|
// https://en.wikipedia.org/wiki/Code_39#Encoding
|
||||||
|
/*
|
||||||
|
* JS source adapted from https://github.com/lindell/JsBarcode
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2016 Johan Lindell (johan@lindell.me)
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to
|
||||||
|
* deal in the Software without restriction, including without limitation the
|
||||||
|
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
||||||
|
* sell copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
* IN THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const Barcode = require("cards.Barcode.js");
|
||||||
|
|
||||||
|
class CODE39 extends Barcode {
|
||||||
|
constructor(data, options){
|
||||||
|
data = data.toUpperCase();
|
||||||
|
|
||||||
|
// Calculate mod43 checksum if enabled
|
||||||
|
if(options.mod43){
|
||||||
|
data += getCharacter(mod43checksum(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
super(data, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
encode(){
|
||||||
|
// First character is always a *
|
||||||
|
var result = getEncoding("*");
|
||||||
|
|
||||||
|
// Take every character and add the binary representation to the result
|
||||||
|
for(let i = 0; i < this.data.length; i++){
|
||||||
|
result += getEncoding(this.data[i]) + "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last character is always a *
|
||||||
|
result += getEncoding("*");
|
||||||
|
|
||||||
|
return {
|
||||||
|
data: result,
|
||||||
|
text: this.text
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
valid(){
|
||||||
|
return /^[0-9A-Z\-\.\ \$\/\+\%]+$/.test(this.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// All characters. The position in the array is the (checksum) value
|
||||||
|
var characters = [
|
||||||
|
"0", "1", "2", "3",
|
||||||
|
"4", "5", "6", "7",
|
||||||
|
"8", "9", "A", "B",
|
||||||
|
"C", "D", "E", "F",
|
||||||
|
"G", "H", "I", "J",
|
||||||
|
"K", "L", "M", "N",
|
||||||
|
"O", "P", "Q", "R",
|
||||||
|
"S", "T", "U", "V",
|
||||||
|
"W", "X", "Y", "Z",
|
||||||
|
"-", ".", " ", "$",
|
||||||
|
"/", "+", "%", "*"
|
||||||
|
];
|
||||||
|
|
||||||
|
// The decimal representation of the characters, is converted to the
|
||||||
|
// corresponding binary with the getEncoding function
|
||||||
|
var encodings = [
|
||||||
|
20957, 29783, 23639, 30485,
|
||||||
|
20951, 29813, 23669, 20855,
|
||||||
|
29789, 23645, 29975, 23831,
|
||||||
|
30533, 22295, 30149, 24005,
|
||||||
|
21623, 29981, 23837, 22301,
|
||||||
|
30023, 23879, 30545, 22343,
|
||||||
|
30161, 24017, 21959, 30065,
|
||||||
|
23921, 22385, 29015, 18263,
|
||||||
|
29141, 17879, 29045, 18293,
|
||||||
|
17783, 29021, 18269, 17477,
|
||||||
|
17489, 17681, 20753, 35770
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get the binary representation of a character by converting the encodings
|
||||||
|
// from decimal to binary
|
||||||
|
function getEncoding(character){
|
||||||
|
return getBinary(characterValue(character));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBinary(characterValue){
|
||||||
|
return encodings[characterValue].toString(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCharacter(characterValue){
|
||||||
|
return characters[characterValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
function characterValue(character){
|
||||||
|
return characters.indexOf(character);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mod43checksum(data){
|
||||||
|
var checksum = 0;
|
||||||
|
for(let i = 0; i < data.length; i++){
|
||||||
|
checksum += characterValue(data[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
checksum = checksum % 43;
|
||||||
|
return checksum;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = CODE39;
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"id": "cards",
|
||||||
|
"name": "Cards",
|
||||||
|
"version": "0.01",
|
||||||
|
"description": "Display loyalty cards",
|
||||||
|
"icon": "app.png",
|
||||||
|
"screenshots": [{"url":"screenshot_cards_overview.png"}, {"url":"screenshot_cards_card1.png"}, {"url":"screenshot_cards_card2.png"}, {"url":"screenshot_cards_barcode.png"}, {"url":"screenshot_cards_qrcode.png"}],
|
||||||
|
"tags": "cards",
|
||||||
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"cards.app.js","url":"app.js"},
|
||||||
|
{"name":"cards.settings.js","url":"settings.js"},
|
||||||
|
{"name":"cards.Barcode.js","url":"Barcode.js"},
|
||||||
|
{"name":"cards.qrcode.js","url":"qrcode.js"},
|
||||||
|
{"name":"cards.codabar.js","url":"codabar.js"},
|
||||||
|
{"name":"cards.code39.js","url":"code39.js"},
|
||||||
|
{"name":"cards.img","url":"app-icon.js","evaluate":true}
|
||||||
|
],
|
||||||
|
"data": [{"name":"cards.settings.json"}]
|
||||||
|
}
|
|
@ -0,0 +1,705 @@
|
||||||
|
/*
|
||||||
|
* C source adapted from https://github.com/ricmoo/QRCode
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* This library is written and maintained by Richard Moore.
|
||||||
|
* Major parts were derived from Project Nayuki's library.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2017 Richard Moore (https://github.com/ricmoo/QRCode)
|
||||||
|
* Copyright (c) 2017 Project Nayuki (https://www.nayuki.io/page/qr-code-generator-library)
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var c = E.compiledC(`
|
||||||
|
// int get_qr(int, int)
|
||||||
|
|
||||||
|
typedef signed char __int8_t;
|
||||||
|
typedef unsigned char __uint8_t;
|
||||||
|
typedef signed short int __int16_t;
|
||||||
|
typedef unsigned short int __uint16_t;
|
||||||
|
typedef signed int __int32_t;
|
||||||
|
typedef unsigned int __uint32_t;
|
||||||
|
|
||||||
|
typedef __int8_t int8_t;
|
||||||
|
typedef __int16_t int16_t;
|
||||||
|
typedef __int32_t int32_t;
|
||||||
|
typedef __uint8_t uint8_t;
|
||||||
|
typedef __uint16_t uint16_t;
|
||||||
|
typedef __uint32_t uint32_t;
|
||||||
|
|
||||||
|
typedef struct QRCode {
|
||||||
|
uint8_t version;
|
||||||
|
uint8_t size;
|
||||||
|
uint8_t ecc;
|
||||||
|
uint8_t mode;
|
||||||
|
uint8_t mask;
|
||||||
|
uint8_t *modules;
|
||||||
|
} QRCode;
|
||||||
|
uint16_t qrcode_getBufferSize(uint8_t version);
|
||||||
|
|
||||||
|
int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data);
|
||||||
|
int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length);
|
||||||
|
|
||||||
|
bool qrcode_getModule(QRCode *qrcode, uint8_t x, uint8_t y);
|
||||||
|
|
||||||
|
static const uint16_t NUM_ERROR_CORRECTION_CODEWORDS[4][40] = {
|
||||||
|
{ 10, 16, 26, 36, 48, 64, 72, 88, 110, 130, 150, 176, 198, 216, 240, 280, 308, 338, 364, 416, 442, 476, 504, 560, 588, 644, 700, 728, 784, 812, 868, 924, 980, 1036, 1064, 1120, 1204, 1260, 1316, 1372},
|
||||||
|
{ 7, 10, 15, 20, 26, 36, 40, 48, 60, 72, 80, 96, 104, 120, 132, 144, 168, 180, 196, 224, 224, 252, 270, 300, 312, 336, 360, 390, 420, 450, 480, 510, 540, 570, 570, 600, 630, 660, 720, 750},
|
||||||
|
{ 17, 28, 44, 64, 88, 112, 130, 156, 192, 224, 264, 308, 352, 384, 432, 480, 532, 588, 650, 700, 750, 816, 900, 960, 1050, 1110, 1200, 1260, 1350, 1440, 1530, 1620, 1710, 1800, 1890, 1980, 2100, 2220, 2310, 2430},
|
||||||
|
{ 13, 22, 36, 52, 72, 96, 108, 132, 160, 192, 224, 260, 288, 320, 360, 408, 448, 504, 546, 600, 644, 690, 750, 810, 870, 952, 1020, 1050, 1140, 1200, 1290, 1350, 1440, 1530, 1590, 1680, 1770, 1860, 1950, 2040},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint8_t NUM_ERROR_CORRECTION_BLOCKS[4][40] = {
|
||||||
|
{ 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49},
|
||||||
|
{ 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25},
|
||||||
|
{ 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81},
|
||||||
|
{ 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68},
|
||||||
|
};
|
||||||
|
|
||||||
|
static const uint16_t NUM_RAW_DATA_MODULES[40] = {
|
||||||
|
208, 359, 567, 807, 1079, 1383, 1568, 1936, 2336, 2768, 3232, 3728, 4256, 4651, 5243, 5867, 6523,
|
||||||
|
7211, 7931, 8683, 9252, 10068, 10916, 11796, 12708, 13652, 14628, 15371, 16411, 17483, 18587,
|
||||||
|
19723, 20891, 22091, 23008, 24272, 25568, 26896, 28256, 29648
|
||||||
|
};
|
||||||
|
static int max(int a, int b) {
|
||||||
|
if (a > b) { return a; }
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int abs(int value) {
|
||||||
|
if (value < 0) { return -value; }
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *memset(void *s, int c, int n) {
|
||||||
|
char *arr = (char *)s;
|
||||||
|
for (int i = 0; i<n; i++) {
|
||||||
|
arr[i] = c;
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void *memcpy(void *dest, const void *src, int n) {
|
||||||
|
char *arr1 = (char *)dest;
|
||||||
|
const char *arr2 = (char *)src;
|
||||||
|
for (int i = 0; i<n; i++) {
|
||||||
|
arr1[i] = arr2[i];
|
||||||
|
}
|
||||||
|
return arr1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int strlen(const char *s) {
|
||||||
|
int i = 0;
|
||||||
|
while (s[i]) ++i;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int8_t getAlphanumeric(char c) {
|
||||||
|
if (c >= '0' && c <= '9') { return (c - '0'); }
|
||||||
|
if (c >= 'A' && c <= 'Z') { return (c - 'A' + 10); }
|
||||||
|
switch (c) {
|
||||||
|
case ' ': return 36;
|
||||||
|
case '$': return 37;
|
||||||
|
case '%': return 38;
|
||||||
|
case '*': return 39;
|
||||||
|
case '+': return 40;
|
||||||
|
case '-': return 41;
|
||||||
|
case '.': return 42;
|
||||||
|
case '/': return 43;
|
||||||
|
case ':': return 44;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool isAlphanumeric(const char *text, uint16_t length) {
|
||||||
|
while (length != 0) {
|
||||||
|
if (getAlphanumeric(text[--length]) == -1) { return false; }
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static bool isNumeric(const char *text, uint16_t length) {
|
||||||
|
while (length != 0) {
|
||||||
|
char c = text[--length];
|
||||||
|
if (c < '0' || c > '9') { return false; }
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
static char getModeBits(uint8_t version, uint8_t mode) {
|
||||||
|
unsigned int modeInfo = 0x7bbb80a;
|
||||||
|
if (version > 9) { modeInfo >>= 9; }
|
||||||
|
|
||||||
|
if (version > 26) { modeInfo >>= 9; }
|
||||||
|
char result = 8 + ((modeInfo >> (3 * mode)) & 0x07);
|
||||||
|
if (result == 15) { result = 16; }
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef struct BitBucket {
|
||||||
|
uint32_t bitOffsetOrWidth;
|
||||||
|
uint16_t capacityBytes;
|
||||||
|
uint8_t *data;
|
||||||
|
} BitBucket;
|
||||||
|
static uint16_t bb_getGridSizeBytes(uint8_t size) {
|
||||||
|
return (((size * size) + 7) / 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint16_t bb_getBufferSizeBytes(uint32_t bits) {
|
||||||
|
return ((bits + 7) / 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void bb_initBuffer(BitBucket *bitBuffer, uint8_t *data, int32_t capacityBytes) {
|
||||||
|
bitBuffer->bitOffsetOrWidth = 0;
|
||||||
|
bitBuffer->capacityBytes = capacityBytes;
|
||||||
|
bitBuffer->data = data;
|
||||||
|
|
||||||
|
memset(data, 0, bitBuffer->capacityBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void bb_initGrid(BitBucket *bitGrid, uint8_t *data, uint8_t size) {
|
||||||
|
bitGrid->bitOffsetOrWidth = size;
|
||||||
|
bitGrid->capacityBytes = bb_getGridSizeBytes(size);
|
||||||
|
bitGrid->data = data;
|
||||||
|
|
||||||
|
memset(data, 0, bitGrid->capacityBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void bb_appendBits(BitBucket *bitBuffer, uint32_t val, uint8_t length) {
|
||||||
|
uint32_t offset = bitBuffer->bitOffsetOrWidth;
|
||||||
|
for (int8_t i = length - 1; i >= 0; i--, offset++) {
|
||||||
|
bitBuffer->data[offset >> 3] |= ((val >> i) & 1) << (7 - (offset & 7));
|
||||||
|
}
|
||||||
|
bitBuffer->bitOffsetOrWidth = offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void bb_setBit(BitBucket *bitGrid, uint8_t x, uint8_t y, bool on) {
|
||||||
|
uint32_t offset = y * bitGrid->bitOffsetOrWidth + x;
|
||||||
|
uint8_t mask = 1 << (7 - (offset & 0x07));
|
||||||
|
if (on) {
|
||||||
|
bitGrid->data[offset >> 3] |= mask;
|
||||||
|
} else {
|
||||||
|
bitGrid->data[offset >> 3] &= ~mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void bb_invertBit(BitBucket *bitGrid, uint8_t x, uint8_t y, bool invert) {
|
||||||
|
uint32_t offset = y * bitGrid->bitOffsetOrWidth + x;
|
||||||
|
uint8_t mask = 1 << (7 - (offset & 0x07));
|
||||||
|
bool on = ((bitGrid->data[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0);
|
||||||
|
if (on ^ invert) {
|
||||||
|
bitGrid->data[offset >> 3] |= mask;
|
||||||
|
} else {
|
||||||
|
bitGrid->data[offset >> 3] &= ~mask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool bb_getBit(BitBucket *bitGrid, uint8_t x, uint8_t y) {
|
||||||
|
uint32_t offset = y * bitGrid->bitOffsetOrWidth + x;
|
||||||
|
return (bitGrid->data[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void applyMask(BitBucket *modules, BitBucket *isFunction, uint8_t mask) {
|
||||||
|
uint8_t size = modules->bitOffsetOrWidth;
|
||||||
|
for (uint8_t y = 0; y < size; y++) {
|
||||||
|
for (uint8_t x = 0; x < size; x++) {
|
||||||
|
if (bb_getBit(isFunction, x, y)) { continue; }
|
||||||
|
|
||||||
|
bool invert = 0;
|
||||||
|
switch (mask) {
|
||||||
|
case 0: invert = (x + y) % 2 == 0; break;
|
||||||
|
case 1: invert = y % 2 == 0; break;
|
||||||
|
case 2: invert = x % 3 == 0; break;
|
||||||
|
case 3: invert = (x + y) % 3 == 0; break;
|
||||||
|
case 4: invert = (x / 3 + y / 2) % 2 == 0; break;
|
||||||
|
case 5: invert = x * y % 2 + x * y % 3 == 0; break;
|
||||||
|
case 6: invert = (x * y % 2 + x * y % 3) % 2 == 0; break;
|
||||||
|
case 7: invert = ((x + y) % 2 + x * y % 3) % 2 == 0; break;
|
||||||
|
}
|
||||||
|
bb_invertBit(modules, x, y, invert);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setFunctionModule(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y, bool on) {
|
||||||
|
bb_setBit(modules, x, y, on);
|
||||||
|
bb_setBit(isFunction, x, y, true);
|
||||||
|
}
|
||||||
|
static void drawFinderPattern(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y) {
|
||||||
|
uint8_t size = modules->bitOffsetOrWidth;
|
||||||
|
|
||||||
|
for (int8_t i = -4; i <= 4; i++) {
|
||||||
|
for (int8_t j = -4; j <= 4; j++) {
|
||||||
|
uint8_t dist = max(abs(i), abs(j));
|
||||||
|
int16_t xx = x + j, yy = y + i;
|
||||||
|
if (0 <= xx && xx < size && 0 <= yy && yy < size) {
|
||||||
|
setFunctionModule(modules, isFunction, xx, yy, dist != 2 && dist != 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static void drawAlignmentPattern(BitBucket *modules, BitBucket *isFunction, uint8_t x, uint8_t y) {
|
||||||
|
for (int8_t i = -2; i <= 2; i++) {
|
||||||
|
for (int8_t j = -2; j <= 2; j++) {
|
||||||
|
setFunctionModule(modules, isFunction, x + j, y + i, max(abs(i), abs(j)) != 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drawFormatBits(BitBucket *modules, BitBucket *isFunction, uint8_t ecc, uint8_t mask) {
|
||||||
|
|
||||||
|
uint8_t size = modules->bitOffsetOrWidth;
|
||||||
|
uint32_t data = ecc << 3 | mask;
|
||||||
|
uint32_t rem = data;
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
rem = (rem << 1) ^ ((rem >> 9) * 0x537);
|
||||||
|
}
|
||||||
|
|
||||||
|
data = data << 10 | rem;
|
||||||
|
data ^= 0x5412;
|
||||||
|
for (uint8_t i = 0; i <= 5; i++) {
|
||||||
|
setFunctionModule(modules, isFunction, 8, i, ((data >> i) & 1) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFunctionModule(modules, isFunction, 8, 7, ((data >> 6) & 1) != 0);
|
||||||
|
setFunctionModule(modules, isFunction, 8, 8, ((data >> 7) & 1) != 0);
|
||||||
|
setFunctionModule(modules, isFunction, 7, 8, ((data >> 8) & 1) != 0);
|
||||||
|
|
||||||
|
for (int8_t i = 9; i < 15; i++) {
|
||||||
|
setFunctionModule(modules, isFunction, 14 - i, 8, ((data >> i) & 1) != 0);
|
||||||
|
}
|
||||||
|
for (int8_t i = 0; i <= 7; i++) {
|
||||||
|
setFunctionModule(modules, isFunction, size - 1 - i, 8, ((data >> i) & 1) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int8_t i = 8; i < 15; i++) {
|
||||||
|
setFunctionModule(modules, isFunction, 8, size - 15 + i, ((data >> i) & 1) != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
setFunctionModule(modules, isFunction, 8, size - 8, true);
|
||||||
|
}
|
||||||
|
static void drawVersion(BitBucket *modules, BitBucket *isFunction, uint8_t version) {
|
||||||
|
|
||||||
|
int8_t size = modules->bitOffsetOrWidth;
|
||||||
|
if (version < 7) { return; }
|
||||||
|
uint32_t rem = version;
|
||||||
|
for (uint8_t i = 0; i < 12; i++) {
|
||||||
|
rem = (rem << 1) ^ ((rem >> 11) * 0x1F25);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t data = version << 12 | rem;
|
||||||
|
for (uint8_t i = 0; i < 18; i++) {
|
||||||
|
bool bit = ((data >> i) & 1) != 0;
|
||||||
|
uint8_t a = size - 11 + i % 3, b = i / 3;
|
||||||
|
setFunctionModule(modules, isFunction, a, b, bit);
|
||||||
|
setFunctionModule(modules, isFunction, b, a, bit);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void drawFunctionPatterns(BitBucket *modules, BitBucket *isFunction, uint8_t version, uint8_t ecc) {
|
||||||
|
|
||||||
|
uint8_t size = modules->bitOffsetOrWidth;
|
||||||
|
for (uint8_t i = 0; i < size; i++) {
|
||||||
|
setFunctionModule(modules, isFunction, 6, i, i % 2 == 0);
|
||||||
|
setFunctionModule(modules, isFunction, i, 6, i % 2 == 0);
|
||||||
|
}
|
||||||
|
drawFinderPattern(modules, isFunction, 3, 3);
|
||||||
|
drawFinderPattern(modules, isFunction, size - 4, 3);
|
||||||
|
drawFinderPattern(modules, isFunction, 3, size - 4);
|
||||||
|
|
||||||
|
if (version > 1) {
|
||||||
|
|
||||||
|
uint8_t alignCount = version / 7 + 2;
|
||||||
|
uint8_t step;
|
||||||
|
if (version != 32) {
|
||||||
|
step = (version * 4 + alignCount * 2 + 1) / (2 * alignCount - 2) * 2;
|
||||||
|
} else {
|
||||||
|
step = 26;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t alignPositionIndex = alignCount - 1;
|
||||||
|
uint8_t alignPosition[alignCount];
|
||||||
|
|
||||||
|
alignPosition[0] = 6;
|
||||||
|
|
||||||
|
uint8_t l_size = version * 4 + 17;
|
||||||
|
for (uint8_t i = 0, pos = l_size - 7; i < alignCount - 1; i++, pos -= step) {
|
||||||
|
alignPosition[alignPositionIndex--] = pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < alignCount; i++) {
|
||||||
|
for (uint8_t j = 0; j < alignCount; j++) {
|
||||||
|
if ((i == 0 && j == 0) || (i == 0 && j == alignCount - 1) || (i == alignCount - 1 && j == 0)) {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
drawAlignmentPattern(modules, isFunction, alignPosition[i], alignPosition[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drawFormatBits(modules, isFunction, ecc, 0);
|
||||||
|
drawVersion(modules, isFunction, version);
|
||||||
|
}
|
||||||
|
static void drawCodewords(BitBucket *modules, BitBucket *isFunction, BitBucket *codewords) {
|
||||||
|
|
||||||
|
uint32_t bitLength = codewords->bitOffsetOrWidth;
|
||||||
|
uint8_t *data = codewords->data;
|
||||||
|
|
||||||
|
uint8_t size = modules->bitOffsetOrWidth;
|
||||||
|
uint32_t i = 0;
|
||||||
|
for (int16_t right = size - 1; right >= 1; right -= 2) {
|
||||||
|
if (right == 6) { right = 5; }
|
||||||
|
|
||||||
|
for (uint8_t vert = 0; vert < size; vert++) {
|
||||||
|
for (int j = 0; j < 2; j++) {
|
||||||
|
uint8_t x = right - j;
|
||||||
|
bool upwards = ((right & 2) == 0) ^ (x < 6);
|
||||||
|
uint8_t y = upwards ? size - 1 - vert : vert;
|
||||||
|
if (!bb_getBit(isFunction, x, y) && i < bitLength) {
|
||||||
|
bb_setBit(modules, x, y, ((data[i >> 3] >> (7 - (i & 7))) & 1) != 0);
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static uint32_t getPenaltyScore(BitBucket *modules) {
|
||||||
|
uint32_t result = 0;
|
||||||
|
|
||||||
|
uint8_t size = modules->bitOffsetOrWidth;
|
||||||
|
for (uint8_t y = 0; y < size; y++) {
|
||||||
|
|
||||||
|
bool colorX = bb_getBit(modules, 0, y);
|
||||||
|
for (uint8_t x = 1, runX = 1; x < size; x++) {
|
||||||
|
bool cx = bb_getBit(modules, x, y);
|
||||||
|
if (cx != colorX) {
|
||||||
|
colorX = cx;
|
||||||
|
runX = 1;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
runX++;
|
||||||
|
if (runX == 5) {
|
||||||
|
result += 3;
|
||||||
|
} else if (runX > 5) {
|
||||||
|
result++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (uint8_t x = 0; x < size; x++) {
|
||||||
|
bool colorY = bb_getBit(modules, x, 0);
|
||||||
|
for (uint8_t y = 1, runY = 1; y < size; y++) {
|
||||||
|
bool cy = bb_getBit(modules, x, y);
|
||||||
|
if (cy != colorY) {
|
||||||
|
colorY = cy;
|
||||||
|
runY = 1;
|
||||||
|
} else {
|
||||||
|
runY++;
|
||||||
|
if (runY == 5) {
|
||||||
|
result += 3;
|
||||||
|
} else if (runY > 5) {
|
||||||
|
result++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t black = 0;
|
||||||
|
for (uint8_t y = 0; y < size; y++) {
|
||||||
|
uint16_t bitsRow = 0, bitsCol = 0;
|
||||||
|
for (uint8_t x = 0; x < size; x++) {
|
||||||
|
bool color = bb_getBit(modules, x, y);
|
||||||
|
if (x > 0 && y > 0) {
|
||||||
|
bool colorUL = bb_getBit(modules, x - 1, y - 1);
|
||||||
|
bool colorUR = bb_getBit(modules, x, y - 1);
|
||||||
|
bool colorL = bb_getBit(modules, x - 1, y);
|
||||||
|
if (color == colorUL && color == colorUR && color == colorL) {
|
||||||
|
result += 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bitsRow = ((bitsRow << 1) & 0x7FF) | color;
|
||||||
|
bitsCol = ((bitsCol << 1) & 0x7FF) | bb_getBit(modules, y, x);
|
||||||
|
if (x >= 10) {
|
||||||
|
if (bitsRow == 0x05D || bitsRow == 0x5D0) {
|
||||||
|
result += 40;
|
||||||
|
}
|
||||||
|
if (bitsCol == 0x05D || bitsCol == 0x5D0) {
|
||||||
|
result += 40;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (color) { black++; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
uint16_t total = size * size;
|
||||||
|
for (uint16_t k = 0; black * 20 < (9 - k) * total || black * 20 > (11 + k) * total; k++) {
|
||||||
|
result += 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint8_t rs_multiply(uint8_t x, uint8_t y) {
|
||||||
|
uint16_t z = 0;
|
||||||
|
for (int8_t i = 7; i >= 0; i--) {
|
||||||
|
z = (z << 1) ^ ((z >> 7) * 0x11D);
|
||||||
|
z ^= ((y >> i) & 1) * x;
|
||||||
|
}
|
||||||
|
return z;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void rs_init(uint8_t degree, uint8_t *coeff) {
|
||||||
|
memset(coeff, 0, degree);
|
||||||
|
coeff[degree - 1] = 1;
|
||||||
|
uint16_t root = 1;
|
||||||
|
for (uint8_t i = 0; i < degree; i++) {
|
||||||
|
|
||||||
|
for (uint8_t j = 0; j < degree; j++) {
|
||||||
|
coeff[j] = rs_multiply(coeff[j], root);
|
||||||
|
if (j + 1 < degree) {
|
||||||
|
coeff[j] ^= coeff[j + 1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
root = (root << 1) ^ ((root >> 7) * 0x11D);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void rs_getRemainder(uint8_t degree, uint8_t *coeff, const uint8_t *data, uint8_t length, uint8_t *result, uint8_t stride) {
|
||||||
|
for (uint8_t i = 0; i < length; i++) {
|
||||||
|
uint8_t factor = data[i] ^ result[0];
|
||||||
|
for (uint8_t j = 1; j < degree; j++) {
|
||||||
|
result[(j - 1) * stride] = result[j * stride];
|
||||||
|
}
|
||||||
|
result[(degree - 1) * stride] = 0;
|
||||||
|
|
||||||
|
for (uint8_t j = 0; j < degree; j++) {
|
||||||
|
result[j * stride] ^= rs_multiply(coeff[j], factor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static int8_t encodeDataCodewords(BitBucket *dataCodewords, const uint8_t *text, uint16_t length, uint8_t version) {
|
||||||
|
int8_t mode = 2;
|
||||||
|
|
||||||
|
if (isNumeric((char*)text, length)) {
|
||||||
|
mode = 0;
|
||||||
|
bb_appendBits(dataCodewords, 1 << 0, 4);
|
||||||
|
bb_appendBits(dataCodewords, length, getModeBits(version, 0));
|
||||||
|
|
||||||
|
uint16_t accumData = 0;
|
||||||
|
uint8_t accumCount = 0;
|
||||||
|
for (uint16_t i = 0; i < length; i++) {
|
||||||
|
accumData = accumData * 10 + ((char)(text[i]) - '0');
|
||||||
|
accumCount++;
|
||||||
|
if (accumCount == 3) {
|
||||||
|
bb_appendBits(dataCodewords, accumData, 10);
|
||||||
|
accumData = 0;
|
||||||
|
accumCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (accumCount > 0) {
|
||||||
|
bb_appendBits(dataCodewords, accumData, accumCount * 3 + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (isAlphanumeric((char*)text, length)) {
|
||||||
|
mode = 1;
|
||||||
|
bb_appendBits(dataCodewords, 1 << 1, 4);
|
||||||
|
bb_appendBits(dataCodewords, length, getModeBits(version, 1));
|
||||||
|
|
||||||
|
uint16_t accumData = 0;
|
||||||
|
uint8_t accumCount = 0;
|
||||||
|
for (uint16_t i = 0; i < length; i++) {
|
||||||
|
accumData = accumData * 45 + getAlphanumeric((char)(text[i]));
|
||||||
|
accumCount++;
|
||||||
|
if (accumCount == 2) {
|
||||||
|
bb_appendBits(dataCodewords, accumData, 11);
|
||||||
|
accumData = 0;
|
||||||
|
accumCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (accumCount > 0) {
|
||||||
|
bb_appendBits(dataCodewords, accumData, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
bb_appendBits(dataCodewords, 1 << 2, 4);
|
||||||
|
bb_appendBits(dataCodewords, length, getModeBits(version, 2));
|
||||||
|
for (uint16_t i = 0; i < length; i++) {
|
||||||
|
bb_appendBits(dataCodewords, (char)(text[i]), 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mode;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void performErrorCorrection(uint8_t version, uint8_t ecc, BitBucket *data) {
|
||||||
|
uint8_t numBlocks = NUM_ERROR_CORRECTION_BLOCKS[ecc][version - 1];
|
||||||
|
uint16_t totalEcc = NUM_ERROR_CORRECTION_CODEWORDS[ecc][version - 1];
|
||||||
|
uint16_t moduleCount = NUM_RAW_DATA_MODULES[version - 1];
|
||||||
|
uint8_t blockEccLen = totalEcc / numBlocks;
|
||||||
|
uint8_t numShortBlocks = numBlocks - moduleCount / 8 % numBlocks;
|
||||||
|
uint8_t shortBlockLen = moduleCount / 8 / numBlocks;
|
||||||
|
|
||||||
|
uint8_t shortDataBlockLen = shortBlockLen - blockEccLen;
|
||||||
|
|
||||||
|
uint8_t result[data->capacityBytes];
|
||||||
|
memset(result, 0, sizeof(result));
|
||||||
|
|
||||||
|
uint8_t coeff[blockEccLen];
|
||||||
|
rs_init(blockEccLen, coeff);
|
||||||
|
|
||||||
|
uint16_t offset = 0;
|
||||||
|
uint8_t *dataBytes = data->data;
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < shortDataBlockLen; i++) {
|
||||||
|
uint16_t index = i;
|
||||||
|
uint8_t stride = shortDataBlockLen;
|
||||||
|
for (uint8_t blockNum = 0; blockNum < numBlocks; blockNum++) {
|
||||||
|
result[offset++] = dataBytes[index];
|
||||||
|
if (blockNum == numShortBlocks) { stride++; }
|
||||||
|
|
||||||
|
index += stride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
uint16_t index = shortDataBlockLen * (numShortBlocks + 1);
|
||||||
|
uint8_t stride = shortDataBlockLen;
|
||||||
|
for (uint8_t blockNum = 0; blockNum < numBlocks - numShortBlocks; blockNum++) {
|
||||||
|
result[offset++] = dataBytes[index];
|
||||||
|
|
||||||
|
if (blockNum == 0) { stride++; }
|
||||||
|
index += stride;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t blockSize = shortDataBlockLen;
|
||||||
|
for (uint8_t blockNum = 0; blockNum < numBlocks; blockNum++) {
|
||||||
|
if (blockNum == numShortBlocks) { blockSize++; }
|
||||||
|
|
||||||
|
rs_getRemainder(blockEccLen, coeff, dataBytes, blockSize, &result[offset + blockNum], numBlocks);
|
||||||
|
dataBytes += blockSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(data->data, result, data->capacityBytes);
|
||||||
|
data->bitOffsetOrWidth = moduleCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const uint8_t ECC_FORMAT_BITS = (0x02 << 6) | (0x03 << 4) | (0x00 << 2) | (0x01 << 0);
|
||||||
|
|
||||||
|
uint16_t qrcode_getBufferSize(uint8_t version) {
|
||||||
|
return bb_getGridSizeBytes(4 * version + 17);
|
||||||
|
}
|
||||||
|
int8_t qrcode_initBytes(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, uint8_t *data, uint16_t length) {
|
||||||
|
uint8_t size = version * 4 + 17;
|
||||||
|
qrcode->version = version;
|
||||||
|
qrcode->size = size;
|
||||||
|
qrcode->ecc = ecc;
|
||||||
|
qrcode->modules = modules;
|
||||||
|
|
||||||
|
uint8_t eccFormatBits = (ECC_FORMAT_BITS >> (2 * ecc)) & 0x03;
|
||||||
|
uint16_t moduleCount = NUM_RAW_DATA_MODULES[version - 1];
|
||||||
|
uint16_t dataCapacity = moduleCount / 8 - NUM_ERROR_CORRECTION_CODEWORDS[eccFormatBits][version - 1];
|
||||||
|
struct BitBucket codewords;
|
||||||
|
uint8_t codewordBytes[bb_getBufferSizeBytes(moduleCount)];
|
||||||
|
bb_initBuffer(&codewords, codewordBytes, (int32_t)sizeof(codewordBytes));
|
||||||
|
int8_t mode = encodeDataCodewords(&codewords, data, length, version);
|
||||||
|
|
||||||
|
if (mode < 0) { return -1; }
|
||||||
|
qrcode->mode = mode;
|
||||||
|
uint32_t padding = (dataCapacity * 8) - codewords.bitOffsetOrWidth;
|
||||||
|
if (padding > 4) { padding = 4; }
|
||||||
|
bb_appendBits(&codewords, 0, padding);
|
||||||
|
bb_appendBits(&codewords, 0, (8 - codewords.bitOffsetOrWidth % 8) % 8);
|
||||||
|
for (uint8_t padByte = 0xEC; codewords.bitOffsetOrWidth < (dataCapacity * 8); padByte ^= 0xEC ^ 0x11) {
|
||||||
|
bb_appendBits(&codewords, padByte, 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
BitBucket modulesGrid;
|
||||||
|
bb_initGrid(&modulesGrid, modules, size);
|
||||||
|
|
||||||
|
BitBucket isFunctionGrid;
|
||||||
|
uint8_t isFunctionGridBytes[bb_getGridSizeBytes(size)];
|
||||||
|
bb_initGrid(&isFunctionGrid, isFunctionGridBytes, size);
|
||||||
|
drawFunctionPatterns(&modulesGrid, &isFunctionGrid, version, eccFormatBits);
|
||||||
|
performErrorCorrection(version, eccFormatBits, &codewords);
|
||||||
|
drawCodewords(&modulesGrid, &isFunctionGrid, &codewords);
|
||||||
|
uint8_t mask = 0;
|
||||||
|
int32_t minPenalty = (2147483647);
|
||||||
|
for (uint8_t i = 0; i < 8; i++) {
|
||||||
|
drawFormatBits(&modulesGrid, &isFunctionGrid, eccFormatBits, i);
|
||||||
|
applyMask(&modulesGrid, &isFunctionGrid, i);
|
||||||
|
int penalty = getPenaltyScore(&modulesGrid);
|
||||||
|
if (penalty < minPenalty) {
|
||||||
|
mask = i;
|
||||||
|
minPenalty = penalty;
|
||||||
|
}
|
||||||
|
applyMask(&modulesGrid, &isFunctionGrid, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
qrcode->mask = mask;
|
||||||
|
drawFormatBits(&modulesGrid, &isFunctionGrid, eccFormatBits, mask);
|
||||||
|
applyMask(&modulesGrid, &isFunctionGrid, mask);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int8_t qrcode_initText(QRCode *qrcode, uint8_t *modules, uint8_t version, uint8_t ecc, const char *data) {
|
||||||
|
return qrcode_initBytes(qrcode, modules, version, ecc, (uint8_t*)data, strlen(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool qrcode_getModule(QRCode *qrcode, uint8_t x, uint8_t y) {
|
||||||
|
if (x >= qrcode->size || y >= qrcode->size) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t offset = y * qrcode->size + x;
|
||||||
|
return (qrcode->modules[offset >> 3] & (1 << (7 - (offset & 0x07)))) != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get_qr (char *string, uint8_t *qrcodeBitmap) {
|
||||||
|
// The structure to manage the QR code
|
||||||
|
QRCode qrcode;
|
||||||
|
|
||||||
|
// Allocate a chunk of memory to store the QR code
|
||||||
|
uint8_t qrcodeBytes[qrcode_getBufferSize(3)];
|
||||||
|
|
||||||
|
qrcode_initText(&qrcode, qrcodeBytes, 3, 0, string);
|
||||||
|
for (uint8_t y = 0; y < qrcode.size; y++) {
|
||||||
|
for (uint8_t x = 0; x < qrcode.size; x++) {
|
||||||
|
qrcodeBitmap[x + y * qrcode.size] = qrcode_getModule(&qrcode, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return qrcode.size;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
function getBinaryQR (value) {
|
||||||
|
var qrcodeBitmap = new Uint8Array(850);
|
||||||
|
var flatValue = Uint8Array(E.toArrayBuffer(E.toFlatString(value ,0)));
|
||||||
|
var valueAddr = E.getAddressOf(flatValue, true);
|
||||||
|
var qrAddr = E.getAddressOf(qrcodeBitmap, true);
|
||||||
|
if (valueAddr == 0 || qrAddr == 0) {
|
||||||
|
console.log ("Failed to get flat arrays..");
|
||||||
|
//return;
|
||||||
|
}
|
||||||
|
var qrsize = c.get_qr(valueAddr, qrAddr);
|
||||||
|
return { data: qrcodeBitmap, size: qrsize };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = getBinaryQR;
|
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.9 KiB |
|
@ -0,0 +1,19 @@
|
||||||
|
(function(back) {
|
||||||
|
var settings = require("Storage").readJSON("cards.settings.json",1)||{};
|
||||||
|
function updateSettings() {
|
||||||
|
require("Storage").writeJSON("cards.settings.json", settings);
|
||||||
|
}
|
||||||
|
var mainmenu = {
|
||||||
|
"" : { "title" : "Cards" },
|
||||||
|
"< Back" : back,
|
||||||
|
/*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?/*LANG*/"Yes":/*LANG*/"No" },
|
||||||
|
/*LANG*/"Use 'Today',..." : {
|
||||||
|
value : !!settings.useToday,
|
||||||
|
onchange: v => {
|
||||||
|
settings.useToday = v;
|
||||||
|
updateSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
E.showMenu(mainmenu);
|
||||||
|
})
|
|
@ -1 +1,2 @@
|
||||||
0.01: New App!
|
0.01: New App!
|
||||||
|
0.02: Handle missing settings (e.g. first-install)
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -5,3 +5,4 @@
|
||||||
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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Clock Switcher
|
||||||
|
|
||||||
|
This switches the default clock.
|
||||||
|
The idea is that you can use this app in combination with e.g. the
|
||||||
|
[Pattern Launcher](?q=ptlaunch) as a quick toggle, instead of navigating through
|
||||||
|
the settings menu.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Load the app to switch to your next installed clock.
|
||||||
|
|
||||||
|
## Creator
|
||||||
|
|
||||||
|
Richard de Boer (rigrig)
|
|
@ -0,0 +1,23 @@
|
||||||
|
const storage = require('Storage');
|
||||||
|
const clocks = storage.list(/\.info$/)
|
||||||
|
.map(app => {
|
||||||
|
const a=storage.readJSON(app, 1);
|
||||||
|
return (a && a.type == "clock") ? a : undefined;
|
||||||
|
})
|
||||||
|
.filter(app => app) // filter out any undefined apps
|
||||||
|
.sort((a, b) => a.sortorder - b.sortorder)
|
||||||
|
.map(app => app.src);
|
||||||
|
if (clocks.length<1) {
|
||||||
|
E.showAlert(/*LANG*/"No clocks found!", "Clock Switcher")
|
||||||
|
.then(load);
|
||||||
|
} else if (clocks.length<2) {
|
||||||
|
E.showAlert(/*LANG*/"Nothing to do:\nOnly one clock installed!", "Clock Switcher")
|
||||||
|
.then(load);
|
||||||
|
} else {
|
||||||
|
let settings = storage.readJSON('setting.json',true)||{clock:null};
|
||||||
|
const old = clocks.indexOf(settings.clock),
|
||||||
|
next = (old+1)%clocks.length;
|
||||||
|
settings.clock = clocks[next];
|
||||||
|
storage.writeJSON('setting.json', settings);
|
||||||
|
setTimeout(load, 100); // storage.writeJSON needs some time to complete
|
||||||
|
}
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwxH+AClhCyoAYsIwuF4IwtF4Qxqw2GF4mG1YsqAAeF1eyAAIteFhAvHGLeGwouLR4IuEGDJcGwooBAweH6/X6wwGGKtbKownB640C1gGCAAQwZLgotDF4WG6wuFMZAuVw2yEgqLCABIuD1eGF6eGExYwLw4bCF1BuCDgWFdaGFRgwAJlb0HJogvPdQoAKq0AlYJG1YwDRr+sgEAL4wABwxgNF4ZeSqwLIMAYvNwpebAAOFSBgMCw7sQLxSQORwZLKLw4OLSBlbBgWyLznX2RfPLqBeM6/WcQYvZldbrYvN64jDF7rRNF7qPDGBqPLd6YxDGBTvQPpowQ1YvLGAeHF54wDlYMIwwvPwovQGAIuJ6+FdxSQF1YwRABKONF4mGF7aONAANbMDpeDRxRgFsOyFy+yP4gvLMAiRX6yNDwouMGDYuELxyRGwySS2QuUMAr0SdQguSGA+G1gtMLgguUGAQxFwuH1aWE2QsBwoQEFyzEHAB+FFzAwCMQoALFrRiRwwtefI5mCQwIslAH4A/AFw"))
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,14 @@
|
||||||
|
{ "id": "clockswitch",
|
||||||
|
"name": "Clock Switcher",
|
||||||
|
"shortName":"Switch Clock",
|
||||||
|
"version":"0.01",
|
||||||
|
"description": "Switch to the next installed clock",
|
||||||
|
"icon": "icon.png",
|
||||||
|
"tags": "tool",
|
||||||
|
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||||
|
"readme": "README.md",
|
||||||
|
"storage": [
|
||||||
|
{"name":"clockswitch.app.js","url":"app.js"},
|
||||||
|
{"name":"clockswitch.img","url":"icon.js","evaluate":true}
|
||||||
|
]
|
||||||
|
}
|
|
@ -10,3 +10,4 @@
|
||||||
0.28: More config options for cleaner look, enabled fast loading
|
0.28: More config options for cleaner look, enabled fast loading
|
||||||
0.29: Fixed a bug that would leave old font files in storage.
|
0.29: Fixed a bug that would leave old font files in storage.
|
||||||
0.30: Added options to show widgets and date on twist and tap. New fonts.
|
0.30: Added options to show widgets and date on twist and tap. New fonts.
|
||||||
|
0.31: Bugfix, no more freeze.
|
||||||
|
|
|
@ -34,9 +34,12 @@
|
||||||
extrasTimeout = undefined;
|
extrasTimeout = undefined;
|
||||||
hideExtras();
|
hideExtras();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
extrasShown = false;
|
||||||
};
|
};
|
||||||
let drawExtras = function() { //draw date, day of the week and widgets
|
let drawExtras = function() { //draw date, day of the week and widgets
|
||||||
let date = new Date();
|
let date = new Date();
|
||||||
|
g.reset();
|
||||||
|
g.clearRect(0, 138, g.getWidth() - 1, 176);
|
||||||
g.setFont("Teletext10x18Ascii").setFontAlign(0, 1);
|
g.setFont("Teletext10x18Ascii").setFontAlign(0, 1);
|
||||||
if (settings.weekday) g.drawString(require("locale").dow(date).toUpperCase(), g.getWidth() / 2, g.getHeight() - 18);
|
if (settings.weekday) g.drawString(require("locale").dow(date).toUpperCase(), g.getWidth() / 2, g.getHeight() - 18);
|
||||||
if (settings.date) g.drawString(require('locale').date(date, 1), g.getWidth() / 2, g.getHeight());
|
if (settings.date) g.drawString(require('locale').date(date, 1), g.getWidth() / 2, g.getHeight());
|
||||||
|
@ -45,21 +48,23 @@
|
||||||
};
|
};
|
||||||
let hideExtras = function() {
|
let hideExtras = function() {
|
||||||
if (extrasTimeout) clearTimeout(extrasTimeout);
|
if (extrasTimeout) clearTimeout(extrasTimeout);
|
||||||
|
extrasTimeout = undefined; //NEW
|
||||||
|
g.reset();
|
||||||
g.clearRect(0, 138, g.getWidth() - 1, 176);
|
g.clearRect(0, 138, g.getWidth() - 1, 176);
|
||||||
require("widget_utils").hide();
|
require("widget_utils").hide();
|
||||||
extrasShown = false;
|
extrasShown = false; ///NEW
|
||||||
};
|
};
|
||||||
let draw = function() {
|
let draw = function() {
|
||||||
|
if (drawTimeout) clearTimeout(drawTimeout); //NEW
|
||||||
|
drawTimeout = setTimeout(function() {
|
||||||
|
drawTimeout = undefined;
|
||||||
|
draw();
|
||||||
|
}, 60000 - (Date.now() % 60000));
|
||||||
let date = new Date();
|
let date = new Date();
|
||||||
g.reset();
|
g.reset();
|
||||||
if (extrasShown) drawExtras();
|
if (extrasShown) drawExtras();
|
||||||
else hideExtras();
|
else hideExtras();
|
||||||
require('contourclock').drawClock(settings.fontIndex);
|
require('contourclock').drawClock(settings.fontIndex);
|
||||||
if (drawTimeout) clearTimeout(drawTimeout);
|
|
||||||
drawTimeout = setTimeout(function() {
|
|
||||||
drawTimeout = undefined;
|
|
||||||
draw();
|
|
||||||
}, 60000 - (Date.now() % 60000));
|
|
||||||
};
|
};
|
||||||
if (settings.hideWhenLocked) {
|
if (settings.hideWhenLocked) {
|
||||||
onLock = locked => {
|
onLock = locked => {
|
||||||
|
@ -83,6 +88,8 @@
|
||||||
Bangle.removeListener('twist', showExtras);
|
Bangle.removeListener('twist', showExtras);
|
||||||
if (drawTimeout) clearTimeout(drawTimeout);
|
if (drawTimeout) clearTimeout(drawTimeout);
|
||||||
if (extrasTimeout) clearTimeout(extrasTimeout);
|
if (extrasTimeout) clearTimeout(extrasTimeout);
|
||||||
|
drawTimeout = undefined;
|
||||||
|
extrasTimeout = undefined;
|
||||||
if (settings.hideWhenLocked) require("widget_utils").show();
|
if (settings.hideWhenLocked) require("widget_utils").show();
|
||||||
g.reset();
|
g.reset();
|
||||||
g.clear();
|
g.clear();
|
||||||
|
@ -91,7 +98,7 @@
|
||||||
g.clear();
|
g.clear();
|
||||||
if (settings.widgets) {
|
if (settings.widgets) {
|
||||||
Bangle.loadWidgets();
|
Bangle.loadWidgets();
|
||||||
Bangle.drawWidgets();
|
setTimeout(Bangle.drawWidgets,0); //NEW
|
||||||
}
|
}
|
||||||
draw();
|
draw();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{ "id": "contourclock",
|
{ "id": "contourclock",
|
||||||
"name": "Contour Clock",
|
"name": "Contour Clock",
|
||||||
"shortName" : "Contour Clock",
|
"shortName" : "Contour Clock",
|
||||||
"version":"0.30",
|
"version":"0.31",
|
||||||
"icon": "app.png",
|
"icon": "app.png",
|
||||||
"readme": "README.md",
|
"readme": "README.md",
|
||||||
"description": "A Minimalist clockface with large Digits.",
|
"description": "A Minimalist clockface with large Digits.",
|
||||||
|
|
|
@ -5,3 +5,6 @@
|
||||||
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.
|
||||||
|
0.09: Fix colors settings, where color was stored as string instead of the expected int.
|
||||||
|
|
|
@ -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.
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{ "id": "dragboard",
|
{ "id": "dragboard",
|
||||||
"name": "Dragboard",
|
"name": "Dragboard",
|
||||||
"version":"0.07",
|
"version":"0.09",
|
||||||
"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",
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
value: settings[key] == color,
|
value: settings[key] == color,
|
||||||
onchange: () => {
|
onchange: () => {
|
||||||
if (color >= 0) {
|
if (color >= 0) {
|
||||||
settings[key] = color;
|
settings[key] = parseInt(color);
|
||||||
} else {
|
} else {
|
||||||
delete settings[key];
|
delete settings[key];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
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.
|
||||||
|
0.03: Fix "Uncaught Error: Unhandled promise rejection: ReferenceError: "dragHandlerDB" is not defined"
|
||||||
|
|
|
@ -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: dragHandlerUB
|
||||||
"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;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{ "id": "draguboard",
|
{ "id": "draguboard",
|
||||||
"name": "DragUboard",
|
"name": "DragUboard",
|
||||||
"version":"0.01",
|
"version":"0.03",
|
||||||
"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",
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
value: settings[key] == color,
|
value: settings[key] == color,
|
||||||
onchange: () => {
|
onchange: () => {
|
||||||
if (color >= 0) {
|
if (color >= 0) {
|
||||||
settings[key] = color;
|
settings[key] = parseInt(color);
|
||||||
} else {
|
} else {
|
||||||
delete settings[key];
|
delete settings[key];
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
0.01: Initial release.
|
||||||
|
0.02: Fix reset of progress bars on midnight. Fix display of 100k+ steps.
|
||||||
|
0.03: Added option to display weather.
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Edge Clock
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
Tinxx presents you a clock with as many straight edges as possible to allow for a crisp look and perfect readability.
|
||||||
|
It comes with a custom font to display weekday, date, time, and steps. Also displays battery percentage while charging.
|
||||||
|
There are three progress bars that indicate day of the week, time of the day, and daily step goal.
|
||||||
|
The watch face is monochrome and allows for applying your favorite color scheme.
|
||||||
|
|
||||||
|
The appearance is highly configurable. In the settings menu you can:
|
||||||
|
- De-/activate a buzz when the charger is connected while the watch face is active.
|
||||||
|
- Decide if month or day should be displayed first.
|
||||||
|
- Switch between 24h and 12h clock.
|
||||||
|
- Hide or display seconds.*
|
||||||
|
- Show AM/PM in place of the seconds.
|
||||||
|
- Show weather temperature and icon in place of the seconds.
|
||||||
|
- Set the daily step goal.
|
||||||
|
- En- or disable the individual progress bars.
|
||||||
|
- Set if your week should start with Monday or Sunday (for week progress bar).
|
||||||
|
|
||||||
|
*) Hiding seconds should further reduce power consumption as the draw interval is prolonged as well.
|
||||||
|
|
||||||
|
The clock implements Fast Loading for faster switching to and fro.
|
||||||
|
|
||||||
|
## Contributors
|
||||||
|
- [tinxx](https://github.com/tinxx)
|
||||||
|
- [peerdavid](https://github.com/peerdavid)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
require("heatshrink").decompress(atob("mEwgMgBAoFEuADCgP8sAFD/wLE/wXDCIIjFAAv/ABQRF5fegEPgfe5UbgEJgVS5ebBYMyr36BYdC7YXEGq4AFj8f/ED8f+ApHjAoMHjkA8HjxwFIgAFCC4IFJjk4AoodEAogXBAoI1BDoYFGL5Z3XmHv33whkfuAFE/Fgw0whuD/Fjz0wh/fuALCh/Y/Fv30wgOf7AFE"))
|
|
@ -0,0 +1,342 @@
|
||||||
|
{
|
||||||
|
/* Configuration
|
||||||
|
------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
const settings = Object.assign({
|
||||||
|
buzzOnCharge: true,
|
||||||
|
monthFirst: true,
|
||||||
|
twentyFourH: true,
|
||||||
|
showAmPm: false,
|
||||||
|
showSeconds: true,
|
||||||
|
showWeather: false,
|
||||||
|
stepGoal: 10000,
|
||||||
|
stepBar: true,
|
||||||
|
weekBar: true,
|
||||||
|
mondayFirst: true,
|
||||||
|
dayBar: true,
|
||||||
|
}, require('Storage').readJSON('edgeclk.settings.json', true) || {});
|
||||||
|
|
||||||
|
/* Runtime Variables
|
||||||
|
------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
let startTimeout;
|
||||||
|
let drawInterval;
|
||||||
|
|
||||||
|
let lcdPower = true;
|
||||||
|
let charging = Bangle.isCharging();
|
||||||
|
|
||||||
|
const font = atob('AA////wDwDwDwD////AAAAAAAAwAwA////AAAAAAAA8/8/wzwzwzwz/z/zAAAA4H4HwDxjxjxj////AAAA/w/wAwAwD/D/AwAwAAAA/j/jxjxjxjxjx/x/AAAA////xjxjxjxjx/x/AAAAwAwAwAwAwA////AAAAAA////xjxjxjxj////AAAA/j/jxjxjxjxj////AAAAAAAAAAMMMMAAAAAAAAAAAAAAABMOMMAAAAAAAAAABgBgDwDwGYGYMMMMAAAAAAGYGYGYGYGYGYAAAAAAMMMMGYGYDwDwBgBgAAAA4A4Ax7x7xgxg/g/gAAAA//gBv9shshv9gF/7AAAA////wwwwwwww////AAAA////xjxjxjxj////AAAA////wDwDwDwD4H4HAAAA////wDwDwD4Hf+P8AAAA////xjxjxjxjwDwDAAAA////xgxgxgxgwAwAAAAA////wDwDwzwz4/4/AAAA////BgBgBgBg////AAAAAAwDwD////wDwDAAAAAAAAwPwPwDwD////AAAAAA////DwH4OccO4HwDAAAA////ADADADADADADAAAA////YAGAGAYA////AAAA////MADAAwAM////AAAA////wDwDwDwD////AAAA////xgxgxgxg/g/gAAAA/+/+wGwOwOwO////AAAA////xgxgxwx8/v/jAAAA/j/jxjxjxjxjx/x/AAAAwAwAwA////wAwAwAAAAA////ADADADAD////AAAA/w/8AOAHAHAO/8/wAAAA////AGAYAYAG////AAAAwD4PecH4H4ec4PwDAAAAwA4AeBH/H/eA4AwAAAAAwPwfw7xzzj3D+D8DAAAAAAAAAAAA////wDAAAAAAAAAABgBgBgBgAAAAAAAAAAwD////AAAAAAAAAAAAAwDwPA8A8APADwAwAAAAAAAAAAAAAAAAAAAAAA');
|
||||||
|
|
||||||
|
const iconSize = [19, 26];
|
||||||
|
const plugIcon = atob('ExoBBxwA44AccAOOAHHAf/8P/+H//D//h//w//4P/4H/8B/8Af8ABwAA4AAcAAOAAHAADgABwAA4AAcAAOAAHAA=');
|
||||||
|
const stepIcon1 = atob('ExoBAfAAPgAHwAD4AB8AAAAB/wD/8D//Bn9wz+cZ/HM/hmfwAP4AAAAD+AD/gBxwB48A4OA8HgcBwcAcOAOGADA=');
|
||||||
|
const stepIcon2 = atob('ExoBAfAAPgMHwfD4dx8ccAcH/8B/8Af8AH8AD+AB/AA/gAfwAP4AAAAD+AD/gBxwB48A4OA8HgcBwcAcOAOGADA=');
|
||||||
|
|
||||||
|
|
||||||
|
/* Draw Functions
|
||||||
|
------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
const drawAll = function () {
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
drawDate(date);
|
||||||
|
if (settings.showSeconds) drawSecs(date);
|
||||||
|
drawTime(date);
|
||||||
|
drawLower();
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawLower = function (stepsOnlyCount) {
|
||||||
|
if (charging) {
|
||||||
|
drawCharge();
|
||||||
|
} else {
|
||||||
|
drawSteps(stepsOnlyCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
drawWeather();
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawWeather = function () {
|
||||||
|
if (!settings.showWeather){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
|
||||||
|
g.setFontAlign(1, 1); // right bottom
|
||||||
|
|
||||||
|
try{
|
||||||
|
const weather = require('weather');
|
||||||
|
const w = weather.get();
|
||||||
|
let temp = parseInt(w.temp-273.15);
|
||||||
|
temp = temp < 0 ? '\\' + String(temp*-1) : String(temp);
|
||||||
|
|
||||||
|
g.drawString(temp, g.getWidth()-40, g.getHeight() - 1, true);
|
||||||
|
|
||||||
|
// clear icon area in case weather condition changed
|
||||||
|
g.clearRect(g.getWidth()-40, g.getHeight()-30, g.getWidth(), g.getHeight());
|
||||||
|
weather.drawIcon(w, g.getWidth()-20, g.getHeight()-15, 14);
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
g.drawString("???", g.getWidth()-3, g.getHeight() - 1, true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawDate = function (date) {
|
||||||
|
const top = 30;
|
||||||
|
g.reset();
|
||||||
|
|
||||||
|
// weekday
|
||||||
|
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
|
||||||
|
g.setFontAlign(-1, -1); // left top
|
||||||
|
g.drawString(date.toString().slice(0,3).toUpperCase(), 0, top + 12, true);
|
||||||
|
|
||||||
|
// date
|
||||||
|
g.setFontAlign(1, -1); // right top
|
||||||
|
// Note: to save space first and last two lines of ASCII are left out.
|
||||||
|
// That is why '-' is assigned to '\' and ' ' (space) to '_'.
|
||||||
|
if (settings.monthFirst) {
|
||||||
|
g.drawString((date.getMonth()+1).toString().padStart(2, '_')
|
||||||
|
+ '\\'
|
||||||
|
+ date.getDate().toString().padStart(2, 0),
|
||||||
|
g.getWidth(), top + 12, true);
|
||||||
|
} else {
|
||||||
|
g.drawString('_'
|
||||||
|
+ date.getDate().toString().padStart(2, 0)
|
||||||
|
+ '\\'
|
||||||
|
+ (date.getMonth()+1).toString(),
|
||||||
|
g.getWidth(), top + 12, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// line/progress bar
|
||||||
|
if (settings.weekBar) {
|
||||||
|
let weekday = date.getDay();
|
||||||
|
if (settings.mondayFirst) {
|
||||||
|
if (weekday === 0) { weekday = 7; }
|
||||||
|
} else {
|
||||||
|
weekday += 1;
|
||||||
|
}
|
||||||
|
drawBar(top, weekday/7);
|
||||||
|
} else {
|
||||||
|
drawLine(top);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawTime = function (date) {
|
||||||
|
const top = 72;
|
||||||
|
g.reset();
|
||||||
|
|
||||||
|
const h = date.getHours();
|
||||||
|
g.setFontCustom(font, 48, 10, 1024 + 12); // triple size (2<<9)
|
||||||
|
g.setFontAlign(-1, -1); // left top
|
||||||
|
g.drawString((settings.twentyFourH ? h : (h % 12 || 12)).toString().padStart(2, 0),
|
||||||
|
0, top+12, true);
|
||||||
|
g.setFontAlign(0, -1); // center top
|
||||||
|
g.drawString(':', g.getWidth()/2, top+12, false);
|
||||||
|
const m = date.getMinutes();
|
||||||
|
g.setFontAlign(1, -1); // right top
|
||||||
|
g.drawString(m.toString().padStart(2, 0),
|
||||||
|
g.getWidth(), top+12, true);
|
||||||
|
|
||||||
|
if (settings.showAmPm) {
|
||||||
|
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
|
||||||
|
g.setFontAlign(1, 1); // right bottom
|
||||||
|
g.drawString(h < 12 ? 'AM' : 'PM', g.getWidth(), g.getHeight() - 1, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (settings.dayBar) {
|
||||||
|
drawBar(top, (h*60+m)/1440);
|
||||||
|
} else {
|
||||||
|
drawLine(top);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawSecs = function (date) {
|
||||||
|
g.reset();
|
||||||
|
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
|
||||||
|
g.setFontAlign(1, 1); // right bottom
|
||||||
|
g.drawString(date.getSeconds().toString().padStart(2, 0), g.getWidth(), g.getHeight() - 1, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawSteps = function (onlyCount) {
|
||||||
|
g.reset();
|
||||||
|
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
|
||||||
|
g.setFontAlign(-1, 1); // left bottom
|
||||||
|
|
||||||
|
const steps = Bangle.getHealthStatus('day').steps;
|
||||||
|
const toKSteps = settings.showWeather ? 1000 : 100000;
|
||||||
|
g.drawString((steps < toKSteps ? steps.toString() : ((steps / 1000).toFixed(0) + 'K')).padEnd(5, '_'),
|
||||||
|
iconSize[0] + 6, g.getHeight() - 1, true);
|
||||||
|
|
||||||
|
if (onlyCount === true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const progress = steps / settings.stepGoal;
|
||||||
|
if (settings.stepBar) {
|
||||||
|
drawBar(g.getHeight() - 38, progress);
|
||||||
|
} else {
|
||||||
|
drawLine(g.getHeight() - 38);
|
||||||
|
}
|
||||||
|
|
||||||
|
// icon
|
||||||
|
if (progress < 1) {
|
||||||
|
g.drawImage(stepIcon1, 0, g.getHeight() - iconSize[1]);
|
||||||
|
} else {
|
||||||
|
g.drawImage(stepIcon2, 0, g.getHeight() - iconSize[1]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawCharge = function () {
|
||||||
|
g.reset();
|
||||||
|
g.setFontCustom(font, 48, 10, 512 + 12); // double size (1<<9)
|
||||||
|
g.setFontAlign(-1, 1); // left bottom
|
||||||
|
|
||||||
|
const charge = E.getBattery();
|
||||||
|
g.drawString(charge.toString().padEnd(5, '_'), iconSize[0] + 6, g.getHeight() - 1, true);
|
||||||
|
|
||||||
|
drawBar(g.getHeight() - 38, charge / 100);
|
||||||
|
g.drawImage(plugIcon, 0, g.getHeight() - 26);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawBar = function (top, progress) {
|
||||||
|
// draw frame
|
||||||
|
g.drawRect(0, top, g.getWidth() - 1, top + 5);
|
||||||
|
g.drawRect(1, top + 1, g.getWidth() - 2, top + 4);
|
||||||
|
// clear bar area
|
||||||
|
g.clearRect(2, top + 2, g.getWidth() - 3, top + 3);
|
||||||
|
// draw bar
|
||||||
|
const barLen = progress >= 1 ? g.getWidth() : (g.getWidth() - 4) * progress;
|
||||||
|
if (barLen < 1) return;
|
||||||
|
g.drawLine(2, top + 2, barLen + 2, top + 2);
|
||||||
|
g.drawLine(2, top + 3, barLen + 2, top + 3);
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawLine = function (top) {
|
||||||
|
const width = g.getWidth();
|
||||||
|
g.drawLine(0, top + 2, width, top + 2);
|
||||||
|
g.drawLine(0, top + 3, width, top + 3);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* Event Handlers
|
||||||
|
------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
const onSecondInterval = function () {
|
||||||
|
const date = new Date();
|
||||||
|
drawSecs(date);
|
||||||
|
if (date.getSeconds() === 0) {
|
||||||
|
onMinuteInterval();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMinuteInterval = function () {
|
||||||
|
const date = new Date();
|
||||||
|
drawTime(date);
|
||||||
|
drawLower(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMinuteIntervalStarter = function () {
|
||||||
|
drawInterval = setInterval(onMinuteInterval, 60000);
|
||||||
|
startTimeout = null;
|
||||||
|
onMinuteInterval();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLcdPower = function (on) {
|
||||||
|
lcdPower = on;
|
||||||
|
if (on) {
|
||||||
|
drawAll();
|
||||||
|
startTimers();
|
||||||
|
} else {
|
||||||
|
stopTimers();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMidnight = function () {
|
||||||
|
if (!lcdPower) return;
|
||||||
|
drawDate(new Date());
|
||||||
|
// Lower part (steps/charge) will be updated every minute.
|
||||||
|
// However, to save power while on battery only step count will get updated.
|
||||||
|
// This will update icon and progress bar as well:
|
||||||
|
if (!charging) drawSteps();
|
||||||
|
drawWeather();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onHealth = function () {
|
||||||
|
if (!lcdPower || charging) return;
|
||||||
|
// This will update progress bar and icon:
|
||||||
|
drawSteps();
|
||||||
|
drawWeather();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLock = function (locked) {
|
||||||
|
if (locked) return;
|
||||||
|
drawLower();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCharging = function (isCharging) {
|
||||||
|
charging = isCharging;
|
||||||
|
if (isCharging && settings.buzzOnCharge) Bangle.buzz();
|
||||||
|
if (!lcdPower) return;
|
||||||
|
drawLower();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* Lifecycle Functions
|
||||||
|
------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
const registerEvents = function () {
|
||||||
|
// This is for original Bangle.js; version two has always-on display:
|
||||||
|
Bangle.on('lcdPower', onLcdPower);
|
||||||
|
|
||||||
|
// Midnight event is triggered when health data is reset and a new day begins:
|
||||||
|
Bangle.on('midnight', onMidnight);
|
||||||
|
|
||||||
|
// Health data is published via 10 mins interval:
|
||||||
|
Bangle.on('health', onHealth);
|
||||||
|
|
||||||
|
// Lock event signals screen (un)lock:
|
||||||
|
Bangle.on('lock', onLock);
|
||||||
|
|
||||||
|
// Charging event signals when charging status changes:
|
||||||
|
Bangle.on('charging', onCharging);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deregisterEvents = function () {
|
||||||
|
Bangle.removeListener('lcdPower', onLcdPower);
|
||||||
|
Bangle.removeListener('midnight', onMidnight);
|
||||||
|
Bangle.removeListener('health', onHealth);
|
||||||
|
Bangle.removeListener('lock', onLock);
|
||||||
|
Bangle.removeListener('charging', onCharging);
|
||||||
|
};
|
||||||
|
|
||||||
|
const startTimers = function () {
|
||||||
|
if (drawInterval) return;
|
||||||
|
if (settings.showSeconds) {
|
||||||
|
drawInterval = setInterval( onSecondInterval, 1000);
|
||||||
|
} else {
|
||||||
|
startTimeout = setTimeout(onMinuteIntervalStarter, (60 - new Date().getSeconds()) * 1000);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopTimers = function () {
|
||||||
|
if (startTimeout) clearTimeout(startTimeout);
|
||||||
|
if (!drawInterval) return;
|
||||||
|
clearInterval(drawInterval);
|
||||||
|
drawInterval = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* Startup Process
|
||||||
|
------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
g.clear();
|
||||||
|
drawAll();
|
||||||
|
startTimers();
|
||||||
|
registerEvents();
|
||||||
|
|
||||||
|
Bangle.setUI({mode: 'clock', remove: function() {
|
||||||
|
stopTimers();
|
||||||
|
deregisterEvents();
|
||||||
|
}});
|
||||||
|
Bangle.loadWidgets();
|
||||||
|
Bangle.drawWidgets();
|
||||||
|
}
|
After Width: | Height: | Size: 4.5 KiB |
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"id": "edgeclk",
|
||||||
|
"name": "Edge Clock",
|
||||||
|
"shortName": "Edge Clock",
|
||||||
|
"version": "0.03",
|
||||||
|
"description": "Crisp clock with perfect readability.",
|
||||||
|
"readme": "README.md",
|
||||||
|
"icon": "app.png",
|
||||||
|
"screenshots": [{"url":"screenshot.png"}, {"url":"screenshot2.png"}, {"url":"screenshot3.png"}, {"url":"screenshot4.png"}],
|
||||||
|
"type": "clock",
|
||||||
|
"tags": "clock",
|
||||||
|
"supports": ["BANGLEJS2"],
|
||||||
|
"allow_emulator": true,
|
||||||
|
"storage": [
|
||||||
|
{"name":"edgeclk.app.js", "url": "app.js"},
|
||||||
|
{"name":"edgeclk.settings.js", "url": "settings.js"},
|
||||||
|
{"name":"edgeclk.img", "url": "app-icon.js", "evaluate": true}
|
||||||
|
],
|
||||||
|
"data": [{"name":"edgeclk.settings.json"}]
|
||||||
|
}
|
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 5.1 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,125 @@
|
||||||
|
(function(back) {
|
||||||
|
const SETTINGS_FILE = 'edgeclk.settings.json';
|
||||||
|
const storage = require('Storage');
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
buzzOnCharge: true,
|
||||||
|
monthFirst: true,
|
||||||
|
twentyFourH: true,
|
||||||
|
showAmPm: false,
|
||||||
|
showSeconds: true,
|
||||||
|
showWeather: false,
|
||||||
|
stepGoal: 10000,
|
||||||
|
stepBar: true,
|
||||||
|
weekBar: true,
|
||||||
|
mondayFirst: true,
|
||||||
|
dayBar: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const saved_settings = storage.readJSON(SETTINGS_FILE, true);
|
||||||
|
if (saved_settings) {
|
||||||
|
for (const key in saved_settings) {
|
||||||
|
if (!settings.hasOwnProperty(key)) continue;
|
||||||
|
settings[key] = saved_settings[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let save = function() {
|
||||||
|
storage.write(SETTINGS_FILE, settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
E.showMenu({
|
||||||
|
'': { 'title': 'Edge Clock' },
|
||||||
|
'< Back': back,
|
||||||
|
'Charge Buzz': {
|
||||||
|
value: settings.buzzOnCharge,
|
||||||
|
onchange: () => {
|
||||||
|
settings.buzzOnCharge = !settings.buzzOnCharge;
|
||||||
|
save();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Month First': {
|
||||||
|
value: settings.monthFirst,
|
||||||
|
onchange: () => {
|
||||||
|
settings.monthFirst = !settings.monthFirst;
|
||||||
|
save();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'24h Clock': {
|
||||||
|
value: settings.twentyFourH,
|
||||||
|
onchange: () => {
|
||||||
|
settings.twentyFourH = !settings.twentyFourH;
|
||||||
|
save();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Show AM/PM': {
|
||||||
|
value: settings.showAmPm,
|
||||||
|
onchange: () => {
|
||||||
|
settings.showAmPm = !settings.showAmPm;
|
||||||
|
// TODO can this be visually changed?
|
||||||
|
if (settings.showAmPm && settings.showSeconds) settings.showSeconds = false;
|
||||||
|
if (settings.showAmPm && settings.showWeather) settings.showWeather = false;
|
||||||
|
save();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Show Seconds': {
|
||||||
|
value: settings.showSeconds,
|
||||||
|
onchange: () => {
|
||||||
|
settings.showSeconds = !settings.showSeconds;
|
||||||
|
// TODO can this be visually changed?
|
||||||
|
if (settings.showSeconds && settings.showAmPm) settings.showAmPm = false;
|
||||||
|
if (settings.showSeconds && settings.showWeather) settings.showWeather = false;
|
||||||
|
save();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Show Weather': {
|
||||||
|
value: settings.showWeather,
|
||||||
|
onchange: () => {
|
||||||
|
settings.showWeather = !settings.showWeather;
|
||||||
|
// TODO can this be visually changed?
|
||||||
|
if (settings.showWeather && settings.showAmPm) settings.showAmPm = false;
|
||||||
|
if (settings.showWeather && settings.showSeconds) settings.showSeconds = false;
|
||||||
|
save();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Step Goal': {
|
||||||
|
value: settings.stepGoal,
|
||||||
|
min: 250,
|
||||||
|
max: 50000,
|
||||||
|
step: 250,
|
||||||
|
onchange: v => {
|
||||||
|
settings.stepGoal = v;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Step Progress': {
|
||||||
|
value: settings.stepBar,
|
||||||
|
onchange: () => {
|
||||||
|
settings.stepBar = !settings.stepBar;
|
||||||
|
save();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'Week Progress': {
|
||||||
|
value: settings.weekBar,
|
||||||
|
onchange: () => {
|
||||||
|
settings.weekBar = !settings.weekBar;
|
||||||
|
save();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Week Start': {
|
||||||
|
value: settings.mondayFirst,
|
||||||
|
format: () => settings.mondayFirst ? 'Monday' : 'Sunday',
|
||||||
|
onchange: () => {
|
||||||
|
settings.mondayFirst = !settings.mondayFirst;
|
||||||
|
save();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'Day Progress': {
|
||||||
|
value: settings.dayBar,
|
||||||
|
onchange: () => {
|
||||||
|
settings.dayBar = !settings.dayBar;
|
||||||
|
save();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
})
|
|
@ -128,38 +128,36 @@ LED.set();NRF.sleep();`);
|
||||||
posteditor.on("change", editorChanged);
|
posteditor.on("change", editorChanged);
|
||||||
|
|
||||||
document.getElementById("upload").addEventListener("click", function() {
|
document.getElementById("upload").addEventListener("click", function() {
|
||||||
if (!hasWarnings()) {
|
var precode = preeditor.getValue();
|
||||||
var precode = preeditor.getValue();
|
var jscode = jseditor.getValue();
|
||||||
var jscode = jseditor.getValue();
|
var postcode = posteditor.getValue();
|
||||||
var postcode = posteditor.getValue();
|
var namePrefix = document.getElementById("nameprefix").value;
|
||||||
var namePrefix = document.getElementById("nameprefix").value;
|
localStorage.setItem(LS_PRECODE, precode);
|
||||||
localStorage.setItem(LS_PRECODE, precode);
|
localStorage.setItem(LS_JSCODE, jscode);
|
||||||
localStorage.setItem(LS_JSCODE, jscode);
|
localStorage.setItem(LS_POSTCODE, postcode);
|
||||||
localStorage.setItem(LS_POSTCODE, postcode);
|
localStorage.setItem(LS_NAMEPREFIX, namePrefix);
|
||||||
localStorage.setItem(LS_NAMEPREFIX, namePrefix);
|
|
||||||
|
|
||||||
// force version - as long as we're above 1v96 we get the ability to upload to different storage files
|
// force version - as long as we're above 1v96 we get the ability to upload to different storage files
|
||||||
var ENV = Espruino.Core.Env.getData();
|
var ENV = Espruino.Core.Env.getData();
|
||||||
ENV.VERSION_MAJOR = 2;
|
ENV.VERSION_MAJOR = 2;
|
||||||
ENV.VERSION_MINOR = 0;
|
ENV.VERSION_MINOR = 0;
|
||||||
// Now compile
|
// Now compile
|
||||||
Espruino.transform(jscode, {
|
Espruino.transform(jscode, {
|
||||||
SET_TIME_ON_WRITE : false, // time would just be out of date
|
SET_TIME_ON_WRITE : false, // time would just be out of date
|
||||||
SAVE_ON_SEND : 1, // save to flash
|
SAVE_ON_SEND : 1, // save to flash
|
||||||
LOAD_STORAGE_FILE : 0, // do not load from storage after saving
|
LOAD_STORAGE_FILE : 0, // do not load from storage after saving
|
||||||
// PRETOKENISE : true,
|
// PRETOKENISE : true,
|
||||||
// MINIFICATION_LEVEL : "ESPRIMA", // maybe?
|
// MINIFICATION_LEVEL : "ESPRIMA", // maybe?
|
||||||
}).then(content => {
|
}).then(content => {
|
||||||
sendCustomizedApp({
|
sendCustomizedApp({
|
||||||
storage: [{ name: "espruinoprog.json", content: JSON.stringify({
|
storage: [{ name: "espruinoprog.json", content: JSON.stringify({
|
||||||
namePrefix : namePrefix,
|
namePrefix : namePrefix,
|
||||||
pre : Espruino.Core.CodeWriter.reformatCode(precode),
|
pre : Espruino.Core.CodeWriter.reformatCode(precode),
|
||||||
code : Espruino.Core.CodeWriter.reformatCode(content),
|
code : Espruino.Core.CodeWriter.reformatCode(content),
|
||||||
post : Espruino.Core.CodeWriter.reformatCode(postcode)
|
post : Espruino.Core.CodeWriter.reformatCode(postcode)
|
||||||
})}]
|
})}]
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
document.getElementById("setdefault").addEventListener("click", function(e) {
|
document.getElementById("setdefault").addEventListener("click", function(e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|