1
0
Fork 0

Merge branch master into lcars-flash-warning

master
Adam Schmalhofer 2022-05-31 15:35:06 +02:00
commit 1812f963cc
205 changed files with 4679 additions and 1205 deletions

View File

@ -256,6 +256,7 @@ and which gives information about the app for the Launcher.
// 'clock' - a clock - required for clocks to automatically start
// 'widget' - a widget
// 'bootloader' - an app that at startup (app.boot.js) but doesn't have a launcher entry for 'app.js'
// 'settings' - apps that appear in Settings->Apps (with appname.settings.js) but that have no 'app.js'
// 'RAM' - code that runs and doesn't upload anything to storage
// 'launch' - replacement 'Launcher'
// 'textinput' - provides a 'textinput' library that allows text to be input on the Bangle

352
android.html Normal file
View File

@ -0,0 +1,352 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=0.8,maximum-scale=0.8, minimum-scale=0.8, shrink-to-fit=no">
<link rel="stylesheet" href="css/spectre.min.css">
<link rel="stylesheet" href="css/spectre-exp.min.css">
<link rel="stylesheet" href="css/spectre-icons.min.css">
<link rel="stylesheet" href="css/pwa.css">
<link rel="stylesheet" href="css/main.css">
<link rel="apple-touch-icon" sizes="180x180" href="img/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="img/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="img/favicon-16x16.png">
<link rel="manifest" href="site.webmanifest">
<link rel="mask-icon" href="img/safari-pinned-tab.svg" color="#5755d9">
<meta name="apple-mobile-web-app-title" content="BangleApps">
<meta name="application-name" content="BangleApps">
<meta name="msapplication-TileColor" content="#5755d9">
<meta name="theme-color" content="#5755d9">
<title>Bangle.js App Loader</title>
</head>
<body>
<!--<button id="test">Test</button>
<div id="status"></div>-->
<header class="navbar-primary navbar">
<section class="navbar-section" >
<a href="https://banglejs.com" target="_blank" class="navbar-brand mr-2" ><img src="img/banglejs-logo-sml.png" alt="Bangle.js">
<div>App Loader</div></a>
<!-- <a href="#" class="btn btn-link">...</a> -->
</section>
<section class="navbar-section">
<button class="btn" id="connectmydevice">Connect</button>
</section>
<!--<section class="navbar-section">
<div class="input-group input-inline">
<input class="form-input" type="text" placeholder="search">
<button class="btn btn-primary input-group-btn">Search</button>
</div>
</section>-->
</header>
<div class="container" style="padding-top:4px">
<p id="requireHTTPS" class="hidden">
<b>STOP!</b> This page <b>must</b> be served over HTTPS. Please <a>reload this page via HTTPS</a>.
</p>
</div>
<ul class="tab tab-block" id="tab-navigate">
<li class="tab-item active" id="tab-librarycontainer">
<a href="javascript:showTab('librarycontainer')">Library</a>
</li>
<li class="tab-item" id="tab-myappscontainer">
<a href="javascript:showTab('myappscontainer')">My Apps</a>
</li>
<li class="tab-item" id="tab-morecontainer">
<a href="javascript:showTab('morecontainer')">More...</a>
</li>
</ul>
<div class="container" id="toastcontainer">
</div>
<div class="container apploader-tab" id="librarycontainer">
<div class="dropdown-container">
<div class="dropdown devicetype-nav">
<a href="#" class="btn btn-link dropdown-toggle" tabindex="0">
<span>All apps</span><i class="icon icon-caret"></i>
</a>
<!-- menu component -->
<ul class="menu">
<li class="menu-item"><a>All apps</a></li>
<li class="menu-item"><a dt="BANGLEJS">Bangle.js 1</a></li>
<li class="menu-item"><a dt="BANGLEJS2">Bangle.js 2</a></li>
</ul>
</div>
<div class="filter-nav">
<label class="chip active" filterid="">Default</label>
<label class="chip" filterid="clock">Clocks</label>
<label class="chip" filterid="game">Games</label>
<label class="chip" filterid="tool">Tools</label>
<label class="chip" filterid="widget">Widgets</label>
<label class="chip" filterid="bluetooth">Bluetooth</label>
<label class="chip" filterid="outdoors">Outdoors</label>
<label class="chip" filterid="favourites">Favourites</label>
</div>
<div class="sort-nav hidden">
<span>Sort by:</span>
<label class="chip active" sortid="">None</label>
<label class="chip" sortid="created">New</label>
<label class="chip" sortid="modified">Updated</label>
</div>
</div>
<div class="panel" style="clear:both">
<div class="panel-header">
<div class="input-group" id="searchform">
<input class="form-input" type="text" placeholder="Keywords...">
<button class="btn btn-primary input-group-btn">Search</button>
</div>
</div>
<div class="panel-body columns"><!-- apps go here --></div>
</div>
</div>
<div class="container apploader-tab" id="myappscontainer" style="display:none">
<div class="panel">
<div class="panel-header" style="text-align:right">
<button class="btn refresh">Refresh...</button>
<button class="btn btn-primary updateapps hidden">Update X apps</button>
</div>
<div class="panel-body columns"><!-- apps go here --></div>
</div>
</div>
<div class="container apploader-tab" id="morecontainer" style="display:none">
<div class="hero bg-gray">
<div class="hero-body">
<a href="https://banglejs.com" target="_blank"><img src="img/banglejs-logo-mid.png" alt="Bangle.js"></a>
<h2>App Loader</h2>
<p>A tool for uploading and removing apps from <a href="https://banglejs.com" target="_blank">Bangle.js Smart Watches</a></p>
</div>
</div>
<div class="container" style="padding-top: 8px;">
<p><b>Can't connect?</b> Check out the <a href="https://www.espruino.com/Troubleshooting+Bangle.js" target="_blank">Bangle.js Troubleshooting page</a>
<p id="apploaderlinks"></p>
<p>Check out <a href="https://github.com/espruino/BangleApps" target="_blank">the Source on GitHub</a>, or
find out <a href="https://www.espruino.com/Bangle.js+App+Loader" target="_blank">how to add your own app</a></p>
<p>Using <a href="https://espruino.com/" target="_blank">Espruino</a>, Icons from <a href="https://icons8.com/" target="_blank">icons8.com</a></p>
<h3>Utilities</h3>
<p><button class="btn" id="settime">Set Bangle.js Time</button>
<button class="btn" id="removeall" data-tooltip="Delete everything from your Bangle, leaving it blank">Remove all Apps</button>
<button class="btn" id="reinstallall" data-tooltip="Remove and re-install every app, leaving all other data intact">Reinstall apps</button>
<button class="btn" id="installdefault">Install default apps</button>
<button class="btn" id="installfavourite" data-tooltip="Delete everything, install apps you've marked as favourites">Install favourite apps</button></p>
<p><button class="btn tooltip tooltip-right" id="downloadallapps" data-tooltip="Download all Bangle.js files to a ZIP file">Backup</button>
<button class="btn tooltip tooltip-right" id="uploadallapps" data-tooltip="Restore Bangle.js from a ZIP file">Restore</button></p>
<h3>Settings</h3>
<div class="form-group">
<label class="form-switch">
<input type="checkbox" id="settings-pretokenise">
<i class="form-icon"></i> Pretokenise apps before upload (smaller, faster apps)
</label>
<label class="form-switch">
<input type="checkbox" id="settings-settime">
<i class="form-icon"></i> Always update time when we connect
</label>
<div class="form-group">
<select class="form-select form-inline" id="settings-lang" style="width: 10em">
<option value="">None (English)</option>
</select>&nbsp;&nbsp;<span>Translations (<a href="https://github.com/espruino/BangleApps/issues/1311" target="_blank">BETA - more info</a>). Any apps that are uploaded to Bangle.js after changing this will have any text automatically translated.</span>
</div>
<button class="btn" id="defaultsettings">Default settings</button>
</div>
<div id="more-deviceinfo" style="display:none">
<h3>Device info</h3>
<div id="more-deviceinfo-content"></div>
</div>
</div>
</div>
<footer class="floating hidden">
<!-- Install button, hidden by default -->
<div id="installContainer" class="hidden">
<button id="butInstall" type="button">
Install
</button>
</div>
</footer>
<script src="https://www.puck-js.com/puck.js"></script>
<script src="core/lib/marked.min.js"></script>
<script src="core/lib/espruinotools.js"></script>
<script src="core/lib/heatshrink.js"></script>
<script src="core/js/utils.js"></script>
<script src="loader.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js"></script> <!-- for backup.js -->
<script src="backup.js"></script>
<script src="core/js/ui.js"></script>
<script src="core/js/comms.js"></script>
<script src="core/js/appinfo.js"></script>
<script src="core/js/index.js"></script>
<script src="core/js/pwa.js" defer></script>
<script>
/*Android = {
bangleTx : function(data) {
console.log("TX : "+JSON.stringify(data));
}
};*/
/*document.getElementById("test").addEventListener("click", function() {
console.log("Pressed");
Android.bangleTx("LED1.toggle();\n");
});*/
if (typeof Android!=="undefined") {
console.log("Running in Android, overwrite Puck library");
var isBusy = false;
var queue = [];
var connection = {
cb : function(data) {},
write : function(data, writecb) {
Android.bangleTx(data);
Puck.writeProgress(data.length, data.length);
if (writecb) setTimeout(writecb,10);
},
close : function() {},
received : "",
hadData : false
}
function bangleRx(data) {
// document.getElementById("status").innerText = "RX:"+data;
connection.received += data;
connection.hadData = true;
if (connection.cb) connection.cb(data);
}
function log(level, s) {
if (Puck.log) Puck.log(level, s);
}
function handleQueue() {
if (!queue.length) return;
var q = queue.shift();
log(3,"Executing "+JSON.stringify(q)+" from queue");
if (q.type == "write") Puck.write(q.data, q.callback, q.callbackNewline);
else log(1,"Unknown queue item "+JSON.stringify(q));
}
/* convenience function... Write data, call the callback with data:
callbackNewline = false => if no new data received for ~0.2 sec
callbackNewline = true => after a newline */
function write(data, callback, callbackNewline) {
let result;
/// If there wasn't a callback function, then promisify
if (typeof callback !== 'function') {
callbackNewline = callback;
result = new Promise((resolve, reject) => callback = (value, err) => {
if (err) reject(err);
else resolve(value);
});
}
if (isBusy) {
log(3, "Busy - adding Puck.write to queue");
queue.push({type:"write", data:data, callback:callback, callbackNewline:callbackNewline});
return result;
}
var cbTimeout;
function onWritten() {
if (callbackNewline) {
connection.cb = function(d) {
var newLineIdx = connection.received.indexOf("\n");
if (newLineIdx>=0) {
var l = connection.received.substr(0,newLineIdx);
connection.received = connection.received.substr(newLineIdx+1);
connection.cb = undefined;
if (cbTimeout) clearTimeout(cbTimeout);
cbTimeout = undefined;
if (callback)
callback(l);
isBusy = false;
handleQueue();
}
};
}
// wait for any received data if we have a callback...
var maxTime = 300; // 30 sec - Max time we wait in total, even if getting data
var dataWaitTime = callbackNewline ? 100/*10 sec if waiting for newline*/ : 3/*300ms*/;
var maxDataTime = dataWaitTime; // max time we wait after having received data
cbTimeout = setTimeout(function timeout() {
cbTimeout = undefined;
if (maxTime) maxTime--;
if (maxDataTime) maxDataTime--;
if (connection.hadData) maxDataTime=dataWaitTime;
if (maxDataTime && maxTime) {
cbTimeout = setTimeout(timeout, 100);
} else {
connection.cb = undefined;
if (callback)
callback(connection.received);
isBusy = false;
handleQueue();
connection.received = "";
}
connection.hadData = false;
}, 100);
}
if (!connection.txInProgress) connection.received = "";
isBusy = true;
connection.write(data, onWritten);
return result
}
// ----------------------------------------------------------
Puck = {
/// Are we writing debug information? 0 is no, 1 is some, 2 is more, 3 is all.
debug : Puck.debug,
/// Should we use flow control? Default is true
flowControl : true,
/// Used internally to write log information - you can replace this with your own function
log : function(level, s) { if (level <= this.debug) console.log("<BLE> "+s)},
/// Called with the current send progress or undefined when done - you can replace this with your own function
writeProgress : Puck.writeProgress,
connect : function(callback) {
setTimeout(callback, 10);
},
write : write,
eval : function(expr, cb) {
const response = write('\x10Bluetooth.println(JSON.stringify(' + expr + '))\n', true)
.then(function (d) {
try {
return JSON.parse(d);
} catch (e) {
log(1, "Unable to decode " + JSON.stringify(d) + ", got " + e.toString());
return Promise.reject(d);
}
});
if (cb) {
return void response.then(cb, (err) => cb(null, err));
} else {
return response;
}
},
isConnected : function() { return true; },
getConnection : function() { return connection; },
close : function() {
if (connection)
connection.close();
},
};
// no need for header
document.getElementsByTagName("header")[0].style="display:none";
// force connection attempt automatically
setTimeout(function() {
getInstalledApps(true).catch(err => {
showToast("Device connection failed, "+err,"error");
if ("object"==typeof err) console.log(err.stack);
});
}, 500);
}
</script>
</body>
</html>

View File

@ -26,3 +26,6 @@
Add "Enable All", "Disable All" and "Remove All" actions
0.25: Fix redrawing selected Alarm/Timer entry inside edit submenu
0.26: Add support for Monday as first day of the week (#1780)
0.27: New UI!
0.28: Fix bug with alarms not firing when configured to fire only once
0.29: Fix wrong 'dow' handling in new timer if first day of week is Monday

View File

@ -1,7 +1,31 @@
Alarms & Timers
===============
# Alarms & Timers
This app allows you to add/modify any alarms and timers.
It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched)
to handle the alarm scheduling in an efficient way that can work alongside other apps.
It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched) to handle the alarm scheduling in an efficient way that can work alongside other apps.
## Menu overview
- `New...`
- `New Alarm` &rarr; Configure a new alarm
- `Repeat` &rarr; 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` &rarr; Configure a new timer
- `Advanced`
- `Scheduler settings` &rarr; 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` &rarr; Enable _all_ disabled alarms & timers
- `Disable All` &rarr; Disable _all_ enabled alarms & timers
- `Delete All` &rarr; Delete _all_ alarms & timers
## Creator
- [Gordon Williams](https://github.com/gfwilliams)
## Main Contributors
- [Alessandro Cocco](https://github.com/alessandrococco) - New UI, full rewrite, new features
- [Sabin Iacob](https://github.com/m0n5t3r) - Auto snooze support
- [storm64](https://github.com/storm64) - Fix redrawing in submenus
## Attributions
All icons used in this app are from [icons8](https://icons8.com).

View File

@ -1,271 +1,358 @@
Bangle.loadWidgets();
Bangle.drawWidgets();
// 0 = Sunday (default), 1 = Monday
const firstDayOfWeek = (require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0;
const WORKDAYS = 62
const WEEKEND = firstDayOfWeek ? 192 : 65;
const EVERY_DAY = firstDayOfWeek ? 254 : 127;
const iconAlarmOn = "\0" + atob("GBiBAAAAAAAAAAYAYA4AcBx+ODn/nAP/wAf/4A/n8A/n8B/n+B/n+B/n+B/n+B/h+B/4+A/+8A//8Af/4AP/wAH/gAB+AAAAAAAAAA==");
const iconAlarmOff = "\0" + (g.theme.dark
? atob("GBjBAP////8AAAAAAAAGAGAOAHAcfjg5/5wD/8AH/+AP5/AP5/Af5/gf5/gf5wAf5gAf4Hgf+f4P+bYP8wMH84cD84cB8wMAebYAAf4AAHg=")
: atob("GBjBAP//AAAAAAAAAAAGAGAOAHAcfjg5/5wD/8AH/+AP5/AP5/Af5/gf5/gf5wAf5gAf4Hgf+f4P+bYP8wMH84cD84cB8wMAebYAAf4AAHg="));
const iconTimerOn = "\0" + (g.theme.dark
? atob("GBjBAP////8AAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5wAAwwABgYABgYABgYAH/+AH/+AAAAAAAAAAAAA=")
: atob("GBjBAP//AAAAAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5wAAwwABgYABgYABgYAH/+AH/+AAAAAAAAAAAAA="));
const iconTimerOff = "\0" + (g.theme.dark
? atob("GBjBAP////8AAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5HgAwf4BgbYBgwMBg4cH84cH8wMAAbYAAf4AAHg=")
: atob("GBjBAP//AAAAAAAAAAAAAAAH/+AH/+ABgYABgYABgYAA/wAA/wAAfgAAPAAAPAAAfgAA5HgAwf4BgbYBgwMBg4cH84cH8wMAAbYAAf4AAHg="));
// An array of alarm objects (see sched/README.md)
var alarms = require("sched").getAlarms();
// 0 = Sunday
// 1 = Monday
var firstDayOfWeek = (require("Storage").readJSON("setting.json", true) || {}).firstDayOfWeek || 0;
function getCurrentTime() {
var time = new Date();
return (
time.getHours() * 3600000 +
time.getMinutes() * 60000 +
time.getSeconds() * 1000
);
}
function saveAndReload() {
// Before saving revert the dow to the standard format
alarms.forEach(a => a.dow = handleFirstDayOfWeek(a.dow, firstDayOfWeek));
require("sched").setAlarms(alarms);
require("sched").reload();
}
function showMainMenu() {
// Timer img "\0"+atob("DhKBAP////MDDAwwMGGBzgPwB4AeAPwHOBhgwMMzDez////w")
// Alarm img "\0"+atob("FBSBAABgA4YcMPDGP8Zn/mx/48//PP/zD/8A//AP/wD/8A//AP/wH/+D//w//8AAAADwAAYA")
const menu = {
'': { 'title': /*LANG*/'Alarms&Timers' },
/*LANG*/'< Back': () => { load(); },
/*LANG*/'New Alarm': () => editAlarm(-1),
/*LANG*/'New Timer': () => editTimer(-1)
};
alarms.forEach((alarm, idx) => {
alarm.dow = handleFirstDayOfWeek(alarm.dow, firstDayOfWeek);
var type, txt; // a leading space is currently required (JS error in Espruino 2v12)
if (alarm.timer) {
type = /*LANG*/"Timer";
txt = " " + require("sched").formatTime(alarm.timer);
} else {
type = /*LANG*/"Alarm";
txt = " " + require("sched").formatTime(alarm.t);
}
if (alarm.rp) txt += "\0" + atob("FBaBAAABgAAcAAHn//////wAHsABzAAYwAAMAADAAAAAAwAAMAADGAAzgAN4AD//////54AAOAABgAA=");
// rename duplicate alarms
if (menu[type + txt]) {
var n = 2;
while (menu[type + " " + n + txt]) n++;
txt = type + " " + n + txt;
} else txt = type + txt;
// add to menu
menu[txt] = {
value: "\0" + atob(alarm.on ? "EhKBAH//v/////////////5//x//j//H+eP+Mf/A//h//z//////////3//g" : "EhKBAH//v//8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA8AA///3//g"),
onchange: function () {
setTimeout(alarm.timer ? editTimer : editAlarm, 10, idx, alarm);
}
};
});
if (alarms.some(e => !e.on)) {
menu[/*LANG*/"Enable All"] = () => enableAll(true);
}
if (alarms.some(e => e.on)) {
menu[/*LANG*/"Disable All"] = () => enableAll(false);
}
if (alarms.length > 0) {
menu[/*LANG*/"Delete All"] = () => deleteAll();
}
if (WIDGETS["alarm"]) WIDGETS["alarm"].reload();
return E.showMenu(menu);
}
function editDOW(dow, onchange) {
const menu = {
'': { 'title': /*LANG*/'Days of Week' },
/*LANG*/'< Back': () => onchange(dow)
};
require("date_utils").dows(firstDayOfWeek).forEach((day, i) => {
menu[day] = {
value: !!(dow & (1 << (i + firstDayOfWeek))),
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: v => v ? (dow |= 1 << (i + firstDayOfWeek)) : (dow &= ~(1 << (i + firstDayOfWeek)))
};
});
E.showMenu(menu);
}
function editAlarm(alarmIndex, alarm) {
var newAlarm = alarmIndex < 0;
var a = require("sched").newDefaultAlarm();
a.dow = handleFirstDayOfWeek(a.dow, firstDayOfWeek);
if (!newAlarm) Object.assign(a, alarms[alarmIndex]);
if (alarm) Object.assign(a, alarm);
var t = require("sched").decodeTime(a.t);
const menu = {
'': { 'title': /*LANG*/'Alarm' },
/*LANG*/'< Back': () => {
saveAlarm(newAlarm, alarmIndex, a, t);
showMainMenu();
},
/*LANG*/'Hours': {
value: t.hrs, min: 0, max: 23, wrap: true,
onchange: v => t.hrs = v
},
/*LANG*/'Minutes': {
value: t.mins, min: 0, max: 59, wrap: true,
onchange: v => t.mins = v
},
/*LANG*/'Enabled': {
value: a.on,
format: v => v ? /*LANG*/"On" : /*LANG*/"Off",
onchange: v => a.on = v
},
/*LANG*/'Repeat': {
value: a.rp,
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: v => a.rp = v
},
/*LANG*/'Days': {
value: decodeDOW(a.dow),
onchange: () => setTimeout(editDOW, 100, a.dow, d => {
a.dow = d;
a.t = require("sched").encodeTime(t);
editAlarm(alarmIndex, a);
})
},
/*LANG*/'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate = v),
/*LANG*/'Auto Snooze': {
value: a.as,
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
onchange: v => a.as = v
}
};
menu[/*LANG*/"Cancel"] = () => showMainMenu();
if (!newAlarm) {
menu[/*LANG*/"Delete"] = function () {
alarms.splice(alarmIndex, 1);
saveAndReload();
showMainMenu();
};
}
return E.showMenu(menu);
}
function saveAlarm(newAlarm, alarmIndex, a, t) {
a.t = require("sched").encodeTime(t);
a.last = (a.t < getCurrentTime()) ? (new Date()).getDate() : 0;
if (newAlarm) {
alarms.push(a);
} else {
alarms[alarmIndex] = a;
}
saveAndReload();
}
function editTimer(alarmIndex, alarm) {
var newAlarm = alarmIndex < 0;
var a = require("sched").newDefaultTimer();
if (!newAlarm) Object.assign(a, alarms[alarmIndex]);
if (alarm) Object.assign(a, alarm);
var t = require("sched").decodeTime(a.timer);
const menu = {
'': { 'title': /*LANG*/'Timer' },
/*LANG*/'< Back': () => {
saveTimer(newAlarm, alarmIndex, a, t);
showMainMenu();
},
/*LANG*/'Hours': {
value: t.hrs, min: 0, max: 23, wrap: true,
onchange: v => t.hrs = v
},
/*LANG*/'Minutes': {
value: t.mins, min: 0, max: 59, wrap: true,
onchange: v => t.mins = v
},
/*LANG*/'Enabled': {
value: a.on,
format: v => v ? /*LANG*/"On" : /*LANG*/"Off",
onchange: v => a.on = v
},
/*LANG*/'Vibrate': require("buzz_menu").pattern(a.vibrate, v => a.vibrate = v),
};
menu[/*LANG*/"Cancel"] = () => showMainMenu();
if (!newAlarm) {
menu[/*LANG*/"Delete"] = function () {
alarms.splice(alarmIndex, 1);
saveAndReload();
showMainMenu();
};
}
return E.showMenu(menu);
}
function saveTimer(newAlarm, alarmIndex, a, t) {
a.timer = require("sched").encodeTime(t);
a.t = getCurrentTime() + a.timer;
a.last = 0;
if (newAlarm) {
alarms.push(a);
} else {
alarms[alarmIndex] = a;
}
saveAndReload();
}
function handleFirstDayOfWeek(dow, firstDayOfWeek) {
function handleFirstDayOfWeek(dow) {
if (firstDayOfWeek == 1) {
if ((dow & 1) == 1) {
// By default 1 = Sunday.
// Here the week starts on Monday and Sunday is ON so move Sunday to 128.
// In the scheduler API Sunday is 1.
// Here the week starts on Monday and Sunday is ON so
// when I read the dow I need to move Sunday to 128...
dow += 127;
} else if ((dow & 128) == 128) {
// ... and then when I write the dow I need to move Sunday back to 1.
dow -= 127;
}
}
return dow;
}
function decodeDOW(dow) {
return require("date_utils")
.dows(firstDayOfWeek, 2)
.map((day, index) => dow & (1 << (index + firstDayOfWeek)) ? day : "_")
.join("");
// Check the first day of week and update the dow field accordingly (alarms only!)
alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow));
function showMainMenu() {
const menu = {
"": { "title": /*LANG*/"Alarms & Timers" },
"< Back": () => load(),
/*LANG*/"New...": () => showNewMenu()
};
alarms.forEach((e, index) => {
var label = e.timer
? require("time_utils").formatDuration(e.timer)
: require("time_utils").formatTime(e.t) + (e.rp ? ` ${decodeDOW(e)}` : "");
menu[label] = {
value: e.on ? (e.timer ? iconTimerOn : iconAlarmOn) : (e.timer ? iconTimerOff : iconAlarmOff),
onchange: () => setTimeout(e.timer ? showEditTimerMenu : showEditAlarmMenu, 10, e, index)
};
});
menu[/*LANG*/"Advanced"] = () => showAdvancedMenu();
E.showMenu(menu);
}
function showNewMenu() {
E.showMenu({
"": { "title": /*LANG*/"New..." },
"< Back": () => showMainMenu(),
/*LANG*/"Alarm": () => showEditAlarmMenu(undefined, undefined),
/*LANG*/"Timer": () => showEditTimerMenu(undefined, undefined)
});
}
function showEditAlarmMenu(selectedAlarm, alarmIndex) {
var isNew = alarmIndex === undefined;
var alarm = require("sched").newDefaultAlarm();
alarm.dow = handleFirstDayOfWeek(alarm.dow);
if (selectedAlarm) {
Object.assign(alarm, selectedAlarm);
}
var time = require("time_utils").decodeTime(alarm.t);
const menu = {
"": { "title": isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm" },
"< Back": () => {
saveAlarm(alarm, alarmIndex, time);
showMainMenu();
},
/*LANG*/"Hour": {
value: time.h,
format: v => ("0" + v).substr(-2),
min: 0,
max: 23,
wrap: true,
onchange: v => time.h = v
},
/*LANG*/"Minute": {
value: time.m,
format: v => ("0" + v).substr(-2),
min: 0,
max: 59,
wrap: true,
onchange: v => time.m = v
},
/*LANG*/"Enabled": {
value: alarm.on,
onchange: v => alarm.on = v
},
/*LANG*/"Repeat": {
value: decodeDOW(alarm),
onchange: () => setTimeout(showEditRepeatMenu, 100, alarm.rp, alarm.dow, (repeat, dow) => {
alarm.rp = repeat;
alarm.dow = dow;
alarm.t = require("time_utils").encodeTime(time);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex);
})
},
/*LANG*/"Vibrate": require("buzz_menu").pattern(alarm.vibrate, v => alarm.vibrate = v),
/*LANG*/"Auto Snooze": {
value: alarm.as,
onchange: v => alarm.as = v
},
/*LANG*/"Cancel": () => showMainMenu()
};
if (!isNew) {
menu[/*LANG*/"Delete"] = () => {
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Alarm" }).then((confirm) => {
if (confirm) {
alarms.splice(alarmIndex, 1);
saveAndReload();
showMainMenu();
} else {
alarm.t = require("time_utils").encodeTime(time);
setTimeout(showEditAlarmMenu, 10, alarm, alarmIndex);
}
});
};
}
E.showMenu(menu);
}
function saveAlarm(alarm, alarmIndex, time) {
alarm.t = require("time_utils").encodeTime(time);
alarm.last = alarm.t < require("time_utils").getCurrentTimeMillis() ? new Date().getDate() : 0;
if (alarmIndex === undefined) {
alarms.push(alarm);
} else {
alarms[alarmIndex] = alarm;
}
saveAndReload();
}
function saveAndReload() {
// Before saving revert the dow to the standard format (alarms only!)
alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow));
require("sched").setAlarms(alarms);
require("sched").reload();
// Fix after save
alarms.filter(e => e.timer === undefined).forEach(a => a.dow = handleFirstDayOfWeek(a.dow));
}
function decodeDOW(alarm) {
return alarm.rp
? require("date_utils")
.dows(firstDayOfWeek, 2)
.map((day, index) => alarm.dow & (1 << (index + firstDayOfWeek)) ? day : "_")
.join("")
.toLowerCase()
: "Once"
}
function showEditRepeatMenu(repeat, dow, dowChangeCallback) {
var originalRepeat = repeat;
var originalDow = dow;
var isCustom = repeat && dow != WORKDAYS && dow != WEEKEND && dow != EVERY_DAY;
const menu = {
"": { "title": /*LANG*/"Repeat Alarm" },
"< Back": () => dowChangeCallback(repeat, dow),
/*LANG*/"Once": {
// The alarm will fire once. Internally it will be saved
// as "fire every days" BUT the repeat flag is false so
// we avoid messing up with the scheduler.
value: !repeat,
onchange: () => dowChangeCallback(false, EVERY_DAY)
},
/*LANG*/"Workdays": {
value: repeat && dow == WORKDAYS,
onchange: () => dowChangeCallback(true, WORKDAYS)
},
/*LANG*/"Weekends": {
value: repeat && dow == WEEKEND,
onchange: () => dowChangeCallback(true, WEEKEND)
},
/*LANG*/"Every Day": {
value: repeat && dow == EVERY_DAY,
onchange: () => dowChangeCallback(true, EVERY_DAY)
},
/*LANG*/"Custom": {
value: isCustom ? decodeDOW({ rp: true, dow: dow }) : false,
onchange: () => setTimeout(showCustomDaysMenu, 10, isCustom ? dow : EVERY_DAY, dowChangeCallback, originalRepeat, originalDow)
}
};
E.showMenu(menu);
}
function showCustomDaysMenu(dow, dowChangeCallback, originalRepeat, originalDow) {
const menu = {
"": { "title": /*LANG*/"Custom Days" },
"< Back": () => {
// If the user unchecks all the days then we assume repeat = once
// and we force the dow to every day.
var repeat = dow > 0;
dowChangeCallback(repeat, repeat ? dow : EVERY_DAY)
}
};
require("date_utils").dows(firstDayOfWeek).forEach((day, i) => {
menu[day] = {
value: !!(dow & (1 << (i + firstDayOfWeek))),
onchange: v => v ? (dow |= 1 << (i + firstDayOfWeek)) : (dow &= ~(1 << (i + firstDayOfWeek)))
};
});
menu[/*LANG*/"Cancel"] = () => setTimeout(showEditRepeatMenu, 10, originalRepeat, originalDow, dowChangeCallback)
E.showMenu(menu);
}
function showEditTimerMenu(selectedTimer, timerIndex) {
var isNew = timerIndex === undefined;
var timer = require("sched").newDefaultTimer();
if (selectedTimer) {
Object.assign(timer, selectedTimer);
}
var time = require("time_utils").decodeTime(timer.timer);
const menu = {
"": { "title": isNew ? /*LANG*/"New Timer" : /*LANG*/"Edit Timer" },
"< Back": () => {
saveTimer(timer, timerIndex, time);
showMainMenu();
},
/*LANG*/"Hours": {
value: time.h,
min: 0,
max: 23,
wrap: true,
onchange: v => time.h = v
},
/*LANG*/"Minutes": {
value: time.m,
min: 0,
max: 59,
wrap: true,
onchange: v => time.m = v
},
/*LANG*/"Enabled": {
value: timer.on,
onchange: v => timer.on = v
},
/*LANG*/"Vibrate": require("buzz_menu").pattern(timer.vibrate, v => timer.vibrate = v),
};
if (!isNew) {
menu[/*LANG*/"Delete"] = () => {
E.showPrompt(/*LANG*/"Are you sure?", { title: /*LANG*/"Delete Timer" }).then((confirm) => {
if (confirm) {
alarms.splice(timerIndex, 1);
saveAndReload();
showMainMenu();
} else {
timer.timer = require("time_utils").encodeTime(time);
setTimeout(showEditTimerMenu, 10, timer, timerIndex)
}
});
};
}
E.showMenu(menu);
}
function saveTimer(timer, timerIndex, time) {
timer.timer = require("time_utils").encodeTime(time);
timer.t = require("time_utils").getCurrentTimeMillis() + timer.timer;
timer.last = 0;
if (timerIndex === undefined) {
alarms.push(timer);
} else {
alarms[timerIndex] = timer;
}
saveAndReload();
}
function showAdvancedMenu() {
E.showMenu({
"": { "title": /*LANG*/"Advanced" },
"< Back": () => showMainMenu(),
/*LANG*/"Scheduler Settings": () => eval(require("Storage").read("sched.settings.js"))(() => showAdvancedMenu()),
/*LANG*/"Enable All": () => enableAll(true),
/*LANG*/"Disable All": () => enableAll(false),
/*LANG*/"Delete All": () => deleteAll()
});
}
function enableAll(on) {
E.showPrompt(/*LANG*/"Are you sure?", {
title: on ? /*LANG*/"Enable All" : /*LANG*/"Disable All"
}).then((confirm) => {
if (confirm) {
alarms.forEach(alarm => alarm.on = on);
saveAndReload();
}
showMainMenu();
});
if (alarms.filter(e => e.on == !on).length == 0) {
E.showAlert(
on ? /*LANG*/"Nothing to Enable" : /*LANG*/"Nothing to Disable",
on ? /*LANG*/"Enable All" : /*LANG*/"Disable All"
).then(() => showAdvancedMenu());
} else {
E.showPrompt(/*LANG*/"Are you sure?", { title: on ? /*LANG*/"Enable All" : /*LANG*/"Disable All" }).then((confirm) => {
if (confirm) {
alarms.forEach(alarm => alarm.on = on);
saveAndReload();
showMainMenu();
} else {
showAdvancedMenu();
}
});
}
}
function deleteAll() {
E.showPrompt(/*LANG*/"Are you sure?", {
title: /*LANG*/"Delete All"
}).then((confirm) => {
if (confirm) {
alarms = [];
saveAndReload();
}
showMainMenu();
});
if (alarms.length == 0) {
E.showAlert(/*LANG*/"Nothing to delete", /*LANG*/"Delete All").then(() => showAdvancedMenu());
} else {
E.showPrompt(/*LANG*/"Are you sure?", {
title: /*LANG*/"Delete All"
}).then((confirm) => {
if (confirm) {
alarms = [];
saveAndReload();
showMainMenu();
} else {
showAdvancedMenu();
}
});
}
}
showMainMenu();

View File

@ -2,16 +2,29 @@
"id": "alarm",
"name": "Alarms & Timers",
"shortName": "Alarms",
"version": "0.26",
"version": "0.29",
"description": "Set alarms and timers on your Bangle",
"icon": "app.png",
"tags": "tool,alarm,widget",
"supports": ["BANGLEJS","BANGLEJS2"],
"supports": [ "BANGLEJS", "BANGLEJS2" ],
"readme": "README.md",
"dependencies": {"scheduler":"type"},
"dependencies": { "scheduler":"type" },
"storage": [
{"name":"alarm.app.js","url":"app.js"},
{"name":"alarm.img","url":"app-icon.js","evaluate":true},
{"name":"alarm.wid.js","url":"widget.js"}
{ "name": "alarm.app.js", "url": "app.js" },
{ "name": "alarm.img", "url": "app-icon.js", "evaluate": true },
{ "name": "alarm.wid.js", "url": "widget.js" }
],
"screenshots": [
{ "url": "screenshot-1.png" },
{ "url": "screenshot-2.png" },
{ "url": "screenshot-3.png" },
{ "url": "screenshot-4.png" },
{ "url": "screenshot-5.png" },
{ "url": "screenshot-6.png" },
{ "url": "screenshot-7.png" },
{ "url": "screenshot-8.png" },
{ "url": "screenshot-9.png" },
{ "url": "screenshot-10.png" },
{ "url": "screenshot-11.png" }
]
}

BIN
apps/alarm/screenshot-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
apps/alarm/screenshot-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
apps/alarm/screenshot-3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
apps/alarm/screenshot-4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
apps/alarm/screenshot-5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
apps/alarm/screenshot-6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
apps/alarm/screenshot-7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
apps/alarm/screenshot-8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
apps/alarm/screenshot-9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -7,3 +7,5 @@
0.06: Option to keep messages after a disconnect (default false) (fix #1186)
0.07: Include charging state in battery updates to phone
0.08: Handling of alarms
0.09: Alarm vibration, repeat, and auto-snooze now handled by sched
0.10: Fix SMS bug

View File

@ -21,7 +21,6 @@ of Gadgetbridge - making your phone make noise so you can find it.
* `Keep Msgs` - default is `Off`. When Gadgetbridge disconnects, should Bangle.js
keep any messages it has received, or should it delete them?
* `Messages` - launches the messages app, showing a list of messages
* `Alarms` - opens a submenu where you can set default settings for alarms such as vibration pattern, repeat, and auto snooze
## How it works

View File

@ -3,6 +3,7 @@
Bluetooth.println("");
Bluetooth.println(JSON.stringify(message));
}
var lastMsg;
var settings = require("Storage").readJSON("android.settings.json",1)||{};
//default alarm settings
@ -18,7 +19,17 @@
/* TODO: Call handling, fitness */
var HANDLERS = {
// {t:"notify",id:int, src,title,subject,body,sender,tel:string} add
"notify" : function() { Object.assign(event,{t:"add",positive:true, negative:true});require("messages").pushMessage(event); },
"notify" : function() {
Object.assign(event,{t:"add",positive:true, negative:true});
// Detect a weird GadgetBridge bug and fix it
// For some reason SMS messages send two GB notifications, with different sets of info
if (lastMsg && event.body == lastMsg.body && lastMsg.src == undefined && event.src == "Messages") {
// Mutate the other message
event.id = lastMsg.id;
}
lastMsg = event;
require("messages").pushMessage(event);
},
// {t:"notify~",id:int, title:string} // modified
"notify~" : function() { event.t="modify";require("messages").pushMessage(event); },
// {t:"notify-",id:int} // remove
@ -67,17 +78,13 @@
var dow = event.d[j].rep;
if (!dow) dow = 127; //if no DOW selected, set alarm to all DOW
var last = (event.d[j].h * 3600000 + event.d[j].m * 60000 < currentTime) ? (new Date()).getDate() : 0;
var a = {
id : "gb"+j,
appid : "gbalarms",
on : true,
t : event.d[j].h * 3600000 + event.d[j].m * 60000,
dow : ((dow&63)<<1) | (dow>>6), // Gadgetbridge sends DOW in a different format
last : last,
rp : settings.rp,
as : settings.as,
vibrate : settings.vibrate
};
var a = require("sched").newDefaultAlarm();
a.id = "gb"+j;
a.appid = "gbalarms";
a.on = true;
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.last = last;
alarms.push(a);
}
sched.setAlarms(alarms);

View File

@ -2,7 +2,7 @@
"id": "android",
"name": "Android Integration",
"shortName": "Android",
"version": "0.08",
"version": "0.10",
"description": "Display notifications/music/etc sent from the Gadgetbridge app on Android. This replaces the old 'Gadgetbridge' Bangle.js widget.",
"icon": "app.png",
"tags": "tool,system,messages,notifications,gadgetbridge",

View File

@ -25,27 +25,6 @@
}
},
/*LANG*/"Messages" : ()=>load("messages.app.js"),
/*LANG*/"Alarms" : () => E.showMenu({
"" : { "title" : /*LANG*/"Alarms" },
"< Back" : ()=>E.showMenu(mainmenu),
/*LANG*/"Vibrate": require("buzz_menu").pattern(settings.vibrate, v => {settings.vibrate = v; updateSettings();}),
/*LANG*/"Repeat": {
value: settings.rp,
format : v=>v?/*LANG*/"Yes":/*LANG*/"No",
onchange: v => {
settings.rp = v;
updateSettings();
}
},
/*LANG*/"Auto snooze": {
value: settings.as,
format : v=>v?/*LANG*/"Yes":/*LANG*/"No",
onchange: v => {
settings.as = v;
updateSettings();
}
},
})
};
E.showMenu(mainmenu);
})

View File

@ -7,3 +7,5 @@
0.07: Update to use Bangle.setUI instead of setWatch
0.08: Use theme colors, Layout library
0.09: Fix time/date disappearing after fullscreen notification
0.10: Use ClockFace library
0.11: Use ClockFace.is12Hour

View File

@ -3,7 +3,6 @@
* A simple digital clock showing seconds as a bar
**/
// Check settings for what type our clock should be
const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
let locale = require("locale");
{ // add some more info to locale
let date = new Date();
@ -11,13 +10,9 @@ let locale = require("locale");
date.setMonth(1, 3); // februari: months are zero-indexed
const localized = locale.date(date, true);
locale.dayFirst = /3.*2/.test(localized);
locale.hasMeridian = false;
if (typeof locale.meridian==="function") { // function does not exist if languages app is not installed
locale.hasMeridian = (locale.meridian(date)!=="");
}
locale.hasMeridian = (locale.meridian(date)!=="");
}
Bangle.loadWidgets();
function renderBar(l) {
if (!this.fraction) {
// zero-size fillRect stills draws one line of pixels, we don't want that
@ -27,35 +22,9 @@ function renderBar(l) {
g.fillRect(l.x, l.y, l.x+width-1, l.y+l.height-1);
}
const Layout = require("Layout");
const layout = new Layout({
type: "v", c: [
{
type: "h", c: [
{id: "time", label: "88:88", type: "txt", font: "6x8:5", bgCol: g.theme.bg}, // size updated below
{id: "ampm", label: " ", type: "txt", font: "6x8:2", bgCol: g.theme.bg},
],
},
{id: "bar", type: "custom", fraction: 0, fillx: 1, height: 6, col: g.theme.fg2, render: renderBar},
{height: 40},
{id: "date", type: "txt", font: "10%", valign: 1},
],
}, {lazy: true});
// adjustments based on screen size and whether we display am/pm
let thickness; // bar thickness, same as time font "pixel block" size
if (is12Hour) {
// Maximum font size = (<screen width> - <ampm: 2chars * (2*6)px>) / (5chars * 6px)
thickness = Math.floor((g.getWidth()-24)/(5*6));
} else {
layout.ampm.label = "";
thickness = Math.floor(g.getWidth()/(5*6));
}
layout.bar.height = thickness+1;
layout.time.font = "6x8:"+thickness;
layout.update();
function timeText(date) {
if (!is12Hour) {
if (!clock.is12Hour) {
return locale.time(date, true);
}
const date12 = new Date(date.getTime());
@ -68,7 +37,7 @@ function timeText(date) {
return locale.time(date12, true);
}
function ampmText(date) {
return (is12Hour && locale.hasMeridian)? locale.meridian(date) : "";
return (clock.is12Hour && locale.hasMeridian) ? locale.meridian(date) : "";
}
function dateText(date) {
const dayName = locale.dow(date, true),
@ -78,31 +47,48 @@ function dateText(date) {
return `${dayName} ${dayMonth}`;
}
draw = function draw(force) {
if (!Bangle.isLCDOn()) {return;} // no drawing, also no new update scheduled
const date = new Date();
layout.time.label = timeText(date);
layout.ampm.label = ampmText(date);
layout.date.label = dateText(date);
const SECONDS_PER_MINUTE = 60;
layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE;
if (force) {
Bangle.drawWidgets();
layout.forgetLazyState();
}
layout.render();
// schedule update at start of next second
const millis = date.getMilliseconds();
setTimeout(draw, 1000-millis);
};
// Show launcher when button pressed
Bangle.setUI("clock");
Bangle.on("lcdPower", function(on) {
if (on) {
draw(true);
}
});
g.reset().clear();
Bangle.drawWidgets();
draw();
const ClockFace = require("ClockFace"),
clock = new ClockFace({
precision:1,
init: function() {
const Layout = require("Layout");
this.layout = new Layout({
type: "v", c: [
{
type: "h", c: [
{id: "time", label: "88:88", type: "txt", font: "6x8:5", col:g.theme.fg, bgCol: g.theme.bg}, // size updated below
{id: "ampm", label: " ", type: "txt", font: "6x8:2", col:g.theme.fg, bgCol: g.theme.bg},
],
},
{id: "bar", type: "custom", fraction: 0, fillx: 1, height: 6, col: g.theme.fg2, render: renderBar},
{height: 40},
{id: "date", type: "txt", font: "10%", valign: 1},
],
}, {lazy: true});
// adjustments based on screen size and whether we display am/pm
let thickness; // bar thickness, same as time font "pixel block" size
if (this.is12Hour) {
// Maximum font size = (<screen width> - <ampm: 2chars * (2*6)px>) / (5chars * 6px)
thickness = Math.floor((Bangle.appRect.w-24)/(5*6));
} else {
this.layout.ampm.label = "";
thickness = Math.floor(Bangle.appRect.w/(5*6));
}
this.layout.bar.height = thickness+1;
this.layout.time.font = "6x8:"+thickness;
this.layout.update();
},
update: function(date, c) {
if (c.m) this.layout.time.label = timeText(date);
if (c.h) this.layout.ampm.label = ampmText(date);
if (c.d) this.layout.date.label = dateText(date);
const SECONDS_PER_MINUTE = 60;
if (c.s) this.layout.bar.fraction = date.getSeconds()/SECONDS_PER_MINUTE;
this.layout.render();
},
resume: function() {
this.layout.forgetLazyState();
},
});
clock.start();

View File

@ -1,7 +1,7 @@
{
"id": "barclock",
"name": "Bar Clock",
"version": "0.09",
"version": "0.11",
"description": "A simple digital clock showing seconds as a bar",
"icon": "clock-bar.png",
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],

View File

@ -51,3 +51,4 @@
0.45: Fix 0.44 regression (auto-add semi-colon between each boot code chunk)
0.46: Fix no clock found error on Bangle.js 2
0.47: Add polyfill for setUI with an object as an argument (fix regression for 2v12 devices after Layout module changed)
0.48: Workaround for BTHRM issues on Bangle.js 1 (write .boot files in chunks)

View File

@ -197,8 +197,18 @@ bootFiles.forEach(bootFile=>{
require('Storage').write('.boot0',"//"+bootFile+"\n",fileOffset);
fileOffset+=2+bootFile.length+1;
var bf = require('Storage').read(bootFile);
require('Storage').write('.boot0',bf,fileOffset);
fileOffset+=bf.length;
// we can't just write 'bf' in one go because at least in 2v13 and earlier
// Espruino wants to read the whole file into RAM first, and on Bangle.js 1
// it can be too big (especially BTHRM).
var bflen = bf.length;
var bfoffset = 0;
while (bflen) {
var bfchunk = Math.min(bflen, 2048);
require('Storage').write('.boot0',bf.substr(bfoffset, bfchunk),fileOffset);
fileOffset+=bfchunk;
bfoffset+=bfchunk;
bflen-=bfchunk;
}
require('Storage').write('.boot0',";\n",fileOffset);
fileOffset+=2;
});

View File

@ -1,7 +1,7 @@
{
"id": "boot",
"name": "Bootloader",
"version": "0.47",
"version": "0.48",
"description": "This is needed by Bangle.js to automatically load the clock, menu, widgets and settings",
"icon": "bootloader.png",
"type": "bootloader",

View File

@ -1,14 +1,18 @@
{ "id": "bowserWF",
{
"id": "bowserWF",
"name": "Bowser Watchface",
"shortName":"Bowser Watchface",
"version":"0.01",
"version":"0.02",
"description": "Let bowser show you the time",
"icon": "app.png",
"tags": "",
"supports" : ["BANGLEJS2"],
"type": "clock",
"tags": "clock",
"supports" : ["BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"storage": [
{"name":"bowserWF.app.js","url":"app.js"},
{"name":"bowserWF.img","url":"app-icon.js","evaluate":true}
]
],
"data": [{"name":"bowserWF.json"}]
}

View File

@ -21,3 +21,4 @@
Adds some preset modes and a custom one
Restructure the settings menu
0.08: Allow scanning for devices in settings
0.09: Misc Fixes and improvements (https://github.com/espruino/BangleApps/pull/1655)

View File

@ -2,7 +2,7 @@
When this app is installed it overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.
HRM is requested it searches on Bluetooth for a heart rate monitor, connects, and sends data back using the `Bangle.on('HRM'` event as if it came from the on board monitor.
HRM is requested it searches on Bluetooth for a heart rate monitor, connects, and sends data back using the `Bangle.on('HRM')` event as if it came from the on board monitor.
This means it's compatible with many Bangle.js apps including:
@ -16,19 +16,23 @@ as that requires live sensor data (rather than just BPM readings).
Just install the app, then install an app that uses the heart rate monitor.
Once installed it'll automatically try and connect to the first bluetooth
heart rate monitor it finds.
Once installed you will have to go into this app's settings while your heart rate monitor
is available for bluetooth pairing and scan for devices.
**To disable this and return to normal HRM, uninstall the app**
## Compatible Heart Rate Monitors
This works with any heart rate monitor providing the standard Bluetooth
Heart Rate Service (`180D`) and characteristic (`2A37`).
Heart Rate Service (`180D`) and characteristic (`2A37`). It additionally supports
the location (`2A38`) characteristic and the Battery Service (`180F`), reporting
that information in the `BTHRM` event when they are available.
So far it has been tested on:
* CooSpo Bluetooth Heart Rate Monitor
* Polar H10
* Polar OH1
* Wahoo TICKR X 2
## Internals
@ -38,7 +42,6 @@ This replaces `Bangle.setHRMPower` with its own implementation.
## TODO
* A widget to show connection state?
* Specify a specific device by address?
## Creator

View File

@ -3,7 +3,7 @@
require('Storage').readJSON("bthrm.default.json", true) || {},
require('Storage').readJSON("bthrm.json", true) || {}
);
var log = function(text, param){
if (settings.debuglog){
var logline = new Date().toISOString() + " - " + text;
@ -13,39 +13,38 @@
print(logline);
}
};
log("Settings: ", settings);
if (settings.enabled){
function clearCache(){
var clearCache = function() {
return require('Storage').erase("bthrm.cache.json");
}
};
function getCache(){
var getCache = function() {
var cache = require('Storage').readJSON("bthrm.cache.json", true) || {};
if (settings.btname && settings.btname == cache.name) return cache;
if (settings.btid && settings.btid === cache.id) return cache;
clearCache();
return {};
}
function addNotificationHandler(characteristic){
};
var addNotificationHandler = function(characteristic) {
log("Setting notification handler: " + supportedCharacteristics[characteristic.uuid].handler);
characteristic.on('characteristicvaluechanged', supportedCharacteristics[characteristic.uuid].handler);
}
function writeCache(cache){
characteristic.on('characteristicvaluechanged', (ev) => supportedCharacteristics[characteristic.uuid].handler(ev.target.value));
};
var writeCache = function(cache) {
var oldCache = getCache();
if (oldCache != cache) {
if (oldCache !== cache) {
log("Writing cache");
require('Storage').writeJSON("bthrm.cache.json", cache)
require('Storage').writeJSON("bthrm.cache.json", cache);
} else {
log("No changes, don't write cache");
}
}
};
function characteristicsToCache(characteristics){
var characteristicsToCache = function(characteristics) {
log("Cache characteristics");
var cache = getCache();
if (!cache.characteristics) cache.characteristics = {};
@ -60,9 +59,9 @@
};
}
writeCache(cache);
}
};
function characteristicsFromCache(){
var characteristicsFromCache = function() {
log("Read cached characteristics");
var cache = getCache();
if (!cache.characteristics) return [];
@ -81,38 +80,34 @@
restored.push(r);
}
return restored;
}
};
log("Start");
var lastReceivedData={
};
var serviceFilters = [{
services: [ "180d" ]
}];
supportedServices = [
"0x180d", "0x180f"
var supportedServices = [
"0x180d", // Heart Rate
"0x180f", // Battery
];
var supportedCharacteristics = {
"0x2a37": {
//Heart rate measurement
handler: function (event){
var dv = event.target.value;
handler: function (dv){
var flags = dv.getUint8(0);
var bpm = (flags & 1) ? (dv.getUint16(1) / 100 /* ? */ ) : dv.getUint8(1); // 8 or 16 bit
var sensorContact;
if (flags & 2){
sensorContact = (flags & 4) ? true : false;
sensorContact = !!(flags & 4);
}
var idx = 2 + (flags&1);
var energyExpended;
if (flags & 8){
energyExpended = dv.getUint16(idx,1);
@ -121,11 +116,11 @@
var interval;
if (flags & 16) {
interval = [];
maxIntervalBytes = (dv.byteLength - idx);
var maxIntervalBytes = (dv.byteLength - idx);
log("Found " + (maxIntervalBytes / 2) + " rr data fields");
for(var i = 0 ; i < maxIntervalBytes / 2; i++){
interval[i] = dv.getUint16(idx,1); // in milliseconds
idx += 2
idx += 2;
}
}
@ -140,45 +135,44 @@
}
if (settings.replace){
var newEvent = {
var repEvent = {
bpm: bpm,
confidence: (sensorContact || sensorContact === undefined)? 100 : 0,
src: "bthrm"
};
log("Emitting HRM: ", newEvent);
Bangle.emit("HRM", newEvent);
log("Emitting HRM: ", repEvent);
Bangle.emit("HRM", repEvent);
}
var newEvent = {
bpm: bpm
};
if (location) newEvent.location = location;
if (interval) newEvent.rr = interval;
if (energyExpended) newEvent.energy = energyExpended;
if (battery) newEvent.battery = battery;
if (sensorContact) newEvent.contact = sensorContact;
log("Emitting BTHRM: ", newEvent);
Bangle.emit("BTHRM", newEvent);
}
},
"0x2a38": {
//Body sensor location
handler: function(data){
handler: function(dv){
if (!lastReceivedData["0x180d"]) lastReceivedData["0x180d"] = {};
if (!lastReceivedData["0x180d"]["0x2a38"]) lastReceivedData["0x180d"]["0x2a38"] = data.target.value;
lastReceivedData["0x180d"]["0x2a38"] = parseInt(dv.buffer, 10);
}
},
"0x2a19": {
//Battery
handler: function (event){
handler: function (dv){
if (!lastReceivedData["0x180f"]) lastReceivedData["0x180f"] = {};
if (!lastReceivedData["0x180f"]["0x2a19"]) lastReceivedData["0x180f"]["0x2a19"] = event.target.value.getUint8(0);
lastReceivedData["0x180f"]["0x2a19"] = dv.getUint8(0);
}
}
};
var device;
@ -195,7 +189,7 @@
maxInterval: 1500
};
function waitingPromise(timeout) {
var waitingPromise = function(timeout) {
return new Promise(function(resolve){
log("Start waiting for " + timeout);
setTimeout(()=>{
@ -203,7 +197,7 @@
resolve();
}, timeout);
});
}
};
if (settings.enabled){
Bangle.isBTHRMOn = function(){
@ -215,7 +209,6 @@
};
}
if (settings.replace){
var origIsHRMOn = Bangle.isHRMOn;
@ -229,15 +222,15 @@
};
}
function clearRetryTimeout(){
var clearRetryTimeout = function() {
if (currentRetryTimeout){
log("Clearing timeout " + currentRetryTimeout);
clearTimeout(currentRetryTimeout);
currentRetryTimeout = undefined;
}
}
};
function retry(){
var retry = function() {
log("Retry");
if (!currentRetryTimeout){
@ -252,17 +245,17 @@
initBt();
}, clampedTime);
retryTime = Math.pow(retryTime, 1.1);
retryTime = Math.pow(clampedTime, 1.1);
if (retryTime > maxRetryTime){
retryTime = maxRetryTime;
}
} else {
log("Already in retry...");
}
}
};
var buzzing = false;
function onDisconnect(reason) {
var onDisconnect = function(reason) {
log("Disconnect: " + reason);
log("GATT: ", gatt);
log("Characteristics: ", characteristics);
@ -277,11 +270,23 @@
if (Bangle.isBTHRMOn()){
retry();
}
}
};
function createCharacteristicPromise(newCharacteristic){
var createCharacteristicPromise = function(newCharacteristic) {
log("Create characteristic promise: ", newCharacteristic);
var result = Promise.resolve();
// For values that can be read, go ahead and read them, even if we might be notified in the future
// Allows for getting initial state of infrequently updating characteristics, like battery
if (newCharacteristic.readValue){
result = result.then(()=>{
log("Reading data for " + JSON.stringify(newCharacteristic));
return newCharacteristic.readValue().then((data)=>{
if (supportedCharacteristics[newCharacteristic.uuid] && supportedCharacteristics[newCharacteristic.uuid].handler) {
supportedCharacteristics[newCharacteristic.uuid].handler(data);
}
});
});
}
if (newCharacteristic.properties.notify){
result = result.then(()=>{
log("Starting notifications for: ", newCharacteristic);
@ -290,31 +295,23 @@
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
startPromise = startPromise.then(()=>{
log("Wait after connect");
waitingPromise(settings.gracePeriodNotification)
return waitingPromise(settings.gracePeriodNotification);
});
}
return startPromise;
});
} else if (newCharacteristic.read){
result = result.then(()=>{
readData(newCharacteristic);
log("Reading data for " + newCharacteristic);
return newCharacteristic.read().then((data)=>{
supportedCharacteristics[newCharacteristic.uuid].handler(data);
});
});
}
return result.then(()=>log("Handled characteristic: ", newCharacteristic));
}
function attachCharacteristicPromise(promise, characteristic){
};
var attachCharacteristicPromise = function(promise, characteristic) {
return promise.then(()=>{
log("Handling characteristic:", characteristic);
return createCharacteristicPromise(characteristic);
});
}
function createCharacteristicsPromise(newCharacteristics){
};
var createCharacteristicsPromise = function(newCharacteristics) {
log("Create characteristics promise: ", newCharacteristics);
var result = Promise.resolve();
for (var c of newCharacteristics){
@ -324,13 +321,13 @@
if (c.properties.notify){
addNotificationHandler(c);
}
result = attachCharacteristicPromise(result, c);
}
return result.then(()=>log("Handled characteristics"));
}
function createServicePromise(service){
};
var createServicePromise = function(service) {
log("Create service promise: ", service);
var result = Promise.resolve();
result = result.then(()=>{
@ -338,15 +335,13 @@
return service.getCharacteristics().then((c)=>createCharacteristicsPromise(c));
});
return result.then(()=>log("Handled service" + service.uuid));
}
function attachServicePromise(promise, service){
return promise.then(()=>createServicePromise(service));
}
var reUseCounter = 0;
};
function initBt() {
var attachServicePromise = function(promise, service) {
return promise.then(()=>createServicePromise(service));
};
var initBt = function () {
log("initBt with blockInit: " + blockInit);
if (blockInit){
retry();
@ -355,63 +350,58 @@
blockInit = true;
if (reUseCounter > 10){
log("Reuse counter to high");
gatt=undefined;
reUseCounter = 0;
}
var promise;
var filters;
if (!device){
var filters = serviceFilters;
if (settings.btname){
log("Configured device name", settings.btname);
filters = [{name: settings.btname}];
if (settings.btid){
log("Configured device id", settings.btid);
filters = [{ id: settings.btid }];
} else {
return;
}
log("Requesting device with filters", filters);
promise = NRF.requestDevice({ filters: filters });
promise = NRF.requestDevice({ filters: filters, active: true });
if (settings.gracePeriodRequest){
log("Add " + settings.gracePeriodRequest + "ms grace period after request");
}
promise = promise.then((d)=>{
log("Got device: ", d);
d.on('gattserverdisconnected', onDisconnect);
device = d;
});
promise = promise.then(()=>{
log("Wait after request");
return waitingPromise(settings.gracePeriodRequest);
});
} else {
promise = Promise.resolve();
log("Reuse device: ", device);
}
promise = promise.then(()=>{
if (gatt){
log("Reuse GATT: ", gatt);
} else {
log("GATT is new: ", gatt);
characteristics = [];
var cachedName = getCache().name;
if (device.name != cachedName){
log("Device name changed from " + cachedName + " to " + device.name + ", clearing cache");
var cachedId = getCache().id;
if (device.id !== cachedId){
log("Device ID changed from " + cachedId + " to " + device.id + ", clearing cache");
clearCache();
}
var newCache = getCache();
newCache.name = device.name;
newCache.id = device.id;
writeCache(newCache);
gatt = device.gatt;
}
return Promise.resolve(gatt);
});
promise = promise.then((gatt)=>{
if (!gatt.connected){
var connectPromise = gatt.connect(connectSettings);
@ -427,16 +417,28 @@
return Promise.resolve();
}
});
/* promise = promise.then(() => {
log(JSON.stringify(gatt.getSecurityStatus()));
if (gatt.getSecurityStatus()['bonded']) {
log("Already bonded");
return Promise.resolve();
} else {
log("Start bonding");
return gatt.startBonding()
.then(() => console.log(gatt.getSecurityStatus()));
}
});*/
promise = promise.then(()=>{
if (!characteristics || characteristics.length == 0){
if (!characteristics || characteristics.length === 0){
characteristics = characteristicsFromCache();
}
});
promise = promise.then(()=>{
var characteristicsPromise = Promise.resolve();
if (characteristics.length == 0){
if (characteristics.length === 0){
characteristicsPromise = characteristicsPromise.then(()=>{
log("Getting services");
return gatt.getPrimaryServices();
@ -454,24 +456,22 @@
log("Add " + settings.gracePeriodService + "ms grace period after services");
result = result.then(()=>{
log("Wait after services");
return waitingPromise(settings.gracePeriodService)
return waitingPromise(settings.gracePeriodService);
});
}
return result;
});
} else {
for (var characteristic of characteristics){
characteristicsPromise = attachCharacteristicPromise(characteristicsPromise, characteristic, true);
}
}
return characteristicsPromise;
});
promise = promise.then(()=>{
return promise.then(()=>{
log("Connection established, waiting for notifications");
reUseCounter = 0;
characteristicsToCache(characteristics);
clearRetryTimeout();
}).catch((e) => {
@ -479,7 +479,7 @@
log("Error:", e);
onDisconnect(e);
});
}
};
Bangle.setBTHRMPower = function(isOn, app) {
// Do app power handling
@ -487,7 +487,7 @@
if (Bangle._PWR===undefined) Bangle._PWR={};
if (Bangle._PWR.BTHRM===undefined) Bangle._PWR.BTHRM=[];
if (isOn && !Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM.push(app);
if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!=app);
if (!isOn && Bangle._PWR.BTHRM.includes(app)) Bangle._PWR.BTHRM = Bangle._PWR.BTHRM.filter(a=>a!==app);
isOn = Bangle._PWR.BTHRM.length;
// so now we know if we're really on
if (isOn) {
@ -510,7 +510,7 @@
}
}
};
var origSetHRMPower = Bangle.setHRMPower;
if (settings.startWithHrm){
@ -525,11 +525,10 @@
}
};
}
var fallbackInterval;
function switchInternalHrm(){
var switchInternalHrm = function() {
if (settings.allowFallback && !fallbackInterval){
log("Fallback to HRM enabled");
origSetHRMPower(1, "bthrm_fallback");
@ -542,7 +541,7 @@
}
}, settings.fallbackTimeout);
}
}
};
if (settings.replace){
log("Replace HRM event");
@ -557,11 +556,11 @@
}
switchInternalHrm();
}
E.on("kill", ()=>{
if (gatt && gatt.connected){
log("Got killed, trying to disconnect");
var promise = gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
gatt.disconnect().then(()=>log("Disconnected on kill")).catch((e)=>log("Error during disconnnect on kill", e));
}
});
}

View File

@ -1,7 +1,16 @@
var btm = g.getHeight()-1;
var intervalInt;
var intervalBt;
var BODY_LOCS = {
0: 'Other',
1: 'Chest',
2: 'Wrist',
3: 'Finger',
4: 'Hand',
5: 'Ear Lobe',
6: 'Foot',
}
function clear(y){
g.reset();
g.clearRect(0,y,g.getWidth(),y+75);
@ -15,17 +24,17 @@ function draw(y, type, event) {
g.setFontAlign(0,0);
g.setFontVector(40).drawString(str,px,y+20);
str = "Event: " + type;
if (type == "HRM") {
if (type === "HRM") {
str += " Confidence: " + event.confidence;
g.setFontVector(12).drawString(str,px,y+40);
str = " Source: " + (event.src ? event.src : "internal");
g.setFontVector(12).drawString(str,px,y+50);
}
if (type == "BTHRM"){
if (type === "BTHRM"){
if (event.battery) str += " Bat: " + (event.battery ? event.battery : "");
g.setFontVector(12).drawString(str,px,y+40);
str= "";
if (event.location) str += "Loc: " + event.location.toFixed(0) + "ms";
if (event.location) str += "Loc: " + BODY_LOCS[event.location];
if (event.rr && event.rr.length > 0) str += " RR: " + event.rr.join(",");
g.setFontVector(12).drawString(str,px,y+50);
str= "";
@ -45,7 +54,7 @@ function onBtHrm(e) {
firstEventBt = false;
}
draw(100, "BTHRM", e);
if (e.bpm == 0){
if (e.bpm === 0){
Bangle.buzz(100,0.2);
}
if (intervalBt){

View File

@ -7,10 +7,10 @@
"allowFallback": true,
"warnDisconnect": false,
"fallbackTimeout": 10,
"custom_replace": false,
"custom_replace": true,
"custom_debuglog": false,
"custom_startWithHrm": false,
"custom_allowFallback": false,
"custom_startWithHrm": true,
"custom_allowFallback": true,
"custom_warnDisconnect": false,
"custom_fallbackTimeout": 10,
"gracePeriodNotification": 0,

View File

@ -2,11 +2,11 @@
"id": "bthrm",
"name": "Bluetooth Heart Rate Monitor",
"shortName": "BT HRM",
"version": "0.08",
"version": "0.09",
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
"icon": "app.png",
"type": "app",
"tags": "health,bluetooth",
"tags": "health,bluetooth,hrm,bthrm",
"supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md",
"storage": [

View File

@ -5,14 +5,14 @@
require('Storage').writeJSON(FILE, s);
readSettings();
}
function readSettings(){
settings = Object.assign(
require('Storage').readJSON("bthrm.default.json", true) || {},
require('Storage').readJSON(FILE, true) || {}
);
}
var FILE="bthrm.json";
var settings;
readSettings();
@ -61,12 +61,13 @@
}
};
if (settings.btname){
var name = "Clear " + settings.btname;
if (settings.btname || settings.btid){
var name = "Clear " + (settings.btname || settings.btid);
mainmenu[name] = function() {
E.showPrompt("Clear current device name?").then((r)=>{
E.showPrompt("Clear current device?").then((r)=>{
if (r) {
writeSettings("btname",undefined);
writeSettings("btid",undefined);
}
E.showMenu(buildMainMenu());
});
@ -78,9 +79,7 @@
mainmenu.Debug = function() { E.showMenu(submenu_debug); };
return mainmenu;
}
var submenu_debug = {
'' : { title: "Debug"},
'< Back': function() { E.showMenu(buildMainMenu()); },
@ -103,35 +102,39 @@
function createMenuFromScan(){
E.showMenu();
E.showMessage("Scanning");
E.showMessage("Scanning for 4 seconds");
var submenu_scan = {
'' : { title: "Scan"},
'< Back': function() { E.showMenu(buildMainMenu()); }
};
var packets=10;
var scanStart=Date.now();
NRF.setScan(function(d) {
packets--;
if (packets<=0 || Date.now() - scanStart > 5000){
NRF.setScan();
E.showMenu(submenu_scan);
} else if (d.name){
print("Found device", d);
submenu_scan[d.name] = function(){
E.showPrompt("Set "+d.name+"?").then((r)=>{
if (r) {
writeSettings("btname",d.name);
}
E.showMenu(buildMainMenu());
NRF.findDevices(function(devices) {
submenu_scan[''] = { title: `Scan (${devices.length} found)`};
if (devices.length === 0) {
E.showAlert("No devices found")
.then(() => E.showMenu(buildMainMenu()));
return;
} else {
devices.forEach((d) => {
print("Found device", d);
var shown = (d.name || d.id.substr(0, 17));
submenu_scan[shown] = function () {
E.showPrompt("Set " + shown + "?").then((r) => {
if (r) {
writeSettings("btid", d.id);
// Store the name for displaying later. Will connect by ID
if (d.name) {
writeSettings("btname", d.name);
}
}
E.showMenu(buildMainMenu());
});
};
});
};
}
}, { filters: [{services: [ "180d" ]}]});
E.showMenu(submenu_scan);
}, { timeout: 4000, active: true, filters: [{services: [ "180d" ]}]});
}
var submenu_custom = {
'' : { title: "Custom mode"},
'< Back': function() { E.showMenu(buildMainMenu()); },
@ -167,7 +170,7 @@
}
},
};
var submenu_grace = {
'' : { title: "Grace periods"},
'< Back': function() { E.showMenu(submenu_debug); },
@ -212,51 +215,6 @@
}
}
};
var submenu = {
'' : { title: "Grace periods"},
'< Back': function() { E.showMenu(buildMainMenu()); },
'Request': {
value: settings.gracePeriodRequest,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodRequest",v);
}
},
'Connect': {
value: settings.gracePeriodConnect,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodConnect",v);
}
},
'Notification': {
value: settings.gracePeriodNotification,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodNotification",v);
}
},
'Service': {
value: settings.gracePeriodService,
min: 0,
max: 3000,
step: 100,
format: v=>v+"ms",
onchange: v => {
writeSettings("gracePeriodService",v);
}
}
};
E.showMenu(buildMainMenu());
})
});

View File

@ -3,4 +3,7 @@
0.03: Adapt colors based on the theme of the user.
0.04: Steps can be hidden now such that the time is even larger.
0.05: Included icons for information.
0.06: Design and usability improvements.
0.06: Design and usability improvements.
0.07: Improved positioning.
0.08: Select the color of widgets correctly. Additional settings to hide colon.
0.09: Larger font size if colon is hidden to improve readability further.

View File

@ -8,6 +8,7 @@
- Enable / disable lock icon in the settings.
- If the "sched" app is installed tab top / bottom of the screen to set the timer.
- The design is adapted to the theme of your bangle.
- The colon (e.g. 7:35 = 735) can be hidden now in the settings.
## Thanks to
<a href="https://www.flaticon.com/free-icons/" title="Icons">Icons created by Flaticon</a>

View File

@ -18,6 +18,7 @@ const H = g.getHeight();
let settings = {
fullscreen: false,
showLock: true,
hideColon: false,
showInfo: 0,
};
@ -33,11 +34,25 @@ for (const key in saved_settings) {
// Manrope font
Graphics.prototype.setLargeFont = function(scale) {
// Actual height 49 (50 - 2)
this.setFontCustom(atob("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAP8AAAAAAAAA/wAAAAAAAAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAcAAAAAAAAAfwAAAAAAAAf/AAAAAAAAf/8AAAAAAAf//wAAAAAAP///AAAAAAP///8AAAAAP////wAAAAP////4AAAAP////8AAAAH////8AAAAH////8AAAAB////8AAAAAH///+AAAAAAf//+AAAAAAB//+AAAAAAAH/+AAAAAAAAf+AAAAAAAAB/AAAAAAAAAHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///gAAAAAD////4AAAAA/////4AAAAH/////4AAAA//////wAAAH//////gAAA///////AAAH//////+AAA///////4AAD/4AAAH/wAAP+AAAAP/AAB/wAAAAf8AAH/AAAAA/4AAf4AAAAB/gAB/gAAAAH+AAP8AAAAAf4AA/wAAAAB/gAD/AAAAAH+AAP8AAAAAf4AAf4AAAAB/gAB/gAAAAH+AAH+AAAAA/4AAf8AAAAH/AAB/4AAAA/8AAD/4AAAH/wAAP/8AAH/+AAAf//////4AAA///////AAAB//////4AAAD//////AAAAH/////4AAAAP////+AAAAAP////gAAAAAD///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP+AAAAAAAAB/wAAAAAAAAH/AAAAAAAAA/4AAAAAAAAH/gAAAAAAAAf8AAAAAAAAD/gAAAAAAAAP+AAAAAAAAB///////8AAH///////wAAf///////AAB///////8AAH///////wAAf///////AAB///////8AAH///////wAAP///////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPAAAAfwAAAH8AAAD/AAAB/wAAAf8AAAP/AAAD/wAAB/8AAAf/AAAP/wAAD/8AAB//AAAf/wAAH/8AAD//AAA//gAAf/8AAD/wAAB//wAAf+AAAP//AAB/wAAB//8AAH+AAAP//wAAf4AAB///AAD/AAAP/v8AAP8AAB/8/wAA/wAAP/j/AAD/AAB/8P8AAH+AAH/g/wAAf4AA/8D/AAB/wAH/gP8AAH/AA/+A/wAAf/AP/wD/AAA//D/+AP8AAD////wA/wAAH///+AD/AAAP///wAP8AAAf//+AA/wAAA///wAD/AAAB//+AAP8AAAB//gAA/wAAAB/4AAD/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB4AAAD+AAAAHwAAAf4AAAAfwAAB/gAAAB/gAAH+AAAAP/AAAf4AAAA/8AAB/gAAAD/4AAH+ADAAH/wAAf4AeAAP/AAB/gD+AAP8AAH+Af+AA/4AAf4D/4AB/gAB/gP/AAH+AAH+B/8AAf4AAf4P/wAB/gAB/h//AAH+AAH+P/8AAf4AAf5//wAB/gAB/v//gAP+AAH+//+AA/4AAf//f8AH/AAB//5/8B/8AAH//D////gAAf/4P///+AAB//Af///wAAH/4A///+AAAf/AB///wAAB/4AD//+AAAH/AAH//gAAAP4AAD/4AAAAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAAAAA/+AAAAAAAAP/4AAAAAAAH//gAAAAAAB//+AAAAAAAf//4AAAAAAH///gAAAAAB///+AAAAAAf///4AAAAAH//9/gAAAAD///H+AAAAA///wf4AAAAP//8B/gAAAD///AH+AAAA///wAf4AAAH//8AB/gAAAf//AAH+AAAB//gAAf4AAAH/4AAB/gAAAf+AAAH+AAAB/gAf///8AAH4AB////wAAeAAH////AABgAAf///8AAAAAB////wAAAAAH////AAAAAAf///8AAAAAB////wAAAAAH////AAAAAAAAf4AAAAAAAAB/gAAAAAAAAH+AAAAAAAAAfwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGAAAAAAAAAAeAAAAAf//AB+AAAH///+AP8AAAf///4A/4AAB////gD/wAAH////Af/gAAf///8B/+AAB////wB/8AAH///+AB/wAAf4Af4AD/gAB/gB/AAP+AAH+AP8AAf4AAf4A/wAB/gAB/gD+AAH+AAH+AP4AAf4AAf4A/gAB/gAB/gD/AAH+AAH+AP8AAf4AAf4A/wAD/gAB/gD/gAf8AAH+AH/AD/wAAf4Af/Af+AAB/gB////4AAH+AD////AAAf4AH///8AAB/gAP///gAAH+AA///8AAAAAAA///AAAAAAAB//4AAAAAAAB/+AAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///gAAAAAB////4AAAAAf////4AAAAH/////4AAAA//////wAAAH//////gAAA///////AAAH//////+AAAf//////4AAD/4D/wH/wAAP+AP8AP/AAB/wB/gAf8AAH/AH8AA/4AAf4A/wAB/gAB/gD/AAH+AAH8AP4AAf4AA/wA/gAB/gAD/AD+AAH+AAH8AP8AAf4AAf4A/wAB/gAB/gD/AAP+AAH+AP+AB/wAAf8Af8AP/AAA/4B/8B/8AAD/gH////gAAP8AP///8AAAfgAf///wAAA8AB///+AAADgAD///wAAAAAAD//+AAAAAAAH//gAAAAAAAH/4AAAAAAAAB4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAAH+AAAAAAAAAf4AAAAAAAAB/gAAAAAAAAH+AAAAAAAAAf4AAAAADgAB/gAAAAA+AAH+AAAAAf4AAf4AAAAH/gAB/gAAAD/+AAH+AAAA//4AAf4AAAf//gAB/gAAH//+AAH+AAD///wAAf4AA///8AAB/gAf//+AAAH+AH///gAAAf4D///wAAAB/g///8AAAAH+f//+AAAAAf////gAAAAB////wAAAAAH///8AAAAAAf//+AAAAAAB///gAAAAAAH//wAAAAAAAf/8AAAAAAAB/+AAAAAAAAH/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/wAAAAB/wB//wAAAAf/wf//gAAAD//z///AAAAf/////+AAAD//////8AAAf//////4AAD///////gAAP////w//AAB/+f/8Af8AAH/Af/gA/4AAf4A/8AD/gAB/gB/wAH+AAP8AH+AAf4AA/wAf4AB/gAD/AB/gAH+AAP8AH+AAf4AA/wAf4AB/gAB/gB/wAH+AAH+AP/AAf4AAf8A/+AD/gAB/8f/8Af8AAD////4H/wAAP//////+AAAf//////4AAA///////AAAD//////8AAAH//z///gAAAH/+H//4AAAAH/gH//AAAAAAAAH/wAAAAAAAABwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAfgAAAAAAAAP/wAAAAAAAD//wAAAAAAAf//wAAAAAAH///gABgAAA////AAPAAAD///+AB+AAAf///4AP4AAD////wB/wAAP/AP/AH/AAB/4Af+AP+AAH/AA/4A/4AAf4AB/gB/gAB/gAH+AH+AAP8AAP4Af4AA/wAA/gB/gAD/AAD+AH+AAP8AAP4Af4AA/4AB/gB/gAB/gAH+AH+AAH+AAfwA/4AAf8AD/AH/AAB/4Af4A/8AAD/4H/gP/wAAP//////+AAAf//////wAAA///////AAAB//////4AAAD//////AAAAH/////wAAAAH////+AAAAAH////AAAAAAAf/8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAf4AP8AAAAAB/gA/wAAAAAH+AD/AAAAAAf4AP8AAAAAB/gA/wAAAAAH+AD/AAAAAAf4AP8AAAAAB/gA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), 46, atob("ExwqHCYlJyYoIicoFg=="), 64+(scale<<8)+(1<<16));
// Actual height 48 (49 - 2)
this.setFontCustom(
E.toString(require('heatshrink').decompress(atob('AFcH+AHFh/gA4sf4AHFn+AA4t/E43+AwsB/gHFgf4PH4AMgJ9Ngf/Pot//6bF/59F///PokfA4J9DEgIABEwYkB/7DDEgIlFCoRMDEgQsEDoRLEEgpoBA4JhGOIsHZ40PdwwA/L4SjHNAgGCP4cHA4wWDA4aVCA4gGDA4SNBe4IiBA4MPHYRBBEwScCA4d/EQUBaoRKDA4UBLQYECgb+EAgMHYYcHa4MPHoLBCBgMfYgcfBgM/PIc/BgN/A4YECIIQEDHwkDHwQHDGwQHENQUHA4d/QIQnCRIJJCSgYTCA4hqCA4hqCA4hiCA4ZCEA4RFBGYbrFAHxDGSohdDcgagFAAjPCEzicDToU/A4jPCAwbQCBwgrBgIHEFYKrDWoa7DaggA/AC0PAYV+AYSBCgKpCg4DDVIUfAYZ9BToIDDPoKVBAYfARoQDDXgMPFwTIBdYSYCv4LCv7zCXgYKCXAK8CHoUPXgY9Cn/vEYMPEwX/z46Bj4mBgf+n77CDwX4v54EIIIzCOgX/4I+CAQI9BHYQCCQ4I7CRASDBHYQHCv/Aj4+BGYIeBGAI+Bj/8AIIRBQIZjCRIiWBXgYHCPQgHBBgJ6DA4IEBPQaKBGYQ+BbgiCCAGZFDIIUBaAZBCgYHCQAQTBA4SACUwS8DDYQHBQAbVCQAYwBA4SABgYEBPoQCBFgU/CQWACgRDCHwKVCIYX+aYRDCHwMPAgY+Cn4EDHwX/AgY+B8bEFj/HA4RGCn+f94MBv45Cv+fA4J6C//+j5gBGIMBFoJWBQoRMB8E//4DBHIJcBv4HBEwJUCA4ImCj5MBA4KZCPYQHBZgRBCE4LICvwaCXAYA5PgQAEMIQAEUwQADQAJlCAARlBWYIACT4JtDAAMPA4IWESgg8CAwI+EEoPhHwYlCgY+DEoP4g4+DEoPAh4+CEoReBHwUfLYU/CwgMBXARqBHYQCCGoIjBgI+CgZSCHwcHAYY+Ch4lBJ4IbCjhACPwqUBPwqFCPwhQBIQZ+DOAKVFXooHCXop9DFAi8EFAT0GPoYAygwFEgOATISLDwBWDTQc/A4L6CTQKkCVQX+BYIHBDwX+BYIHBVQX8B4KqD+/wA4aBBj/AgK8CQIIJBA4a/BBIMBAgL/BAgUDYgL/BAII7BAQXgAII7BAQXAYQQxBYARrCMwQ0BAgV/HwYECHwgEBgY+EA4MPGwI8BA4UfGwI8BgYHBPofAQYOHPoeAR4QmBHwQHCEwI+CA4RVBHwQHCaggnBDwQHEHoIAEEQIA6v5NFfgSECBwZtEf4IHFOYQHEj4HGDwYHCDwPgv/jA4UHXQS8E/ED/AHDZ4MPSYKlCv+AYwIHDDwL7EgL7DAgTzCEwIpCeYTZBg4CBeYIJBAgICBFgIJBAgICBeYIEDHII0BAgg+EgI5CMocHGwJBCA4MfGwMD/h/BwF/PoQHC451CJIMDSgIjBA4PAA4QmBA4IhBA4JVBgEMA4bUDV4QeCAAf/HoIAENIIApOoIAEW4QAEW4QAEW4QAEWQRSFNIcDfYQMDny8DO4Q7BAQQjCewh+EHwcPToQ+Dv//ewkHUoI+En68DeIS0EHwMf/46CeYYlCHwQ0BKIY+BGgJ4Dh/nGgZZCAwKPEHYLpFDoKuFGgj4JgY0EHwQ0EYhIA6MAkf+BRBLIa5BQAJSCBgP4R4iVB/YHERoIACA4QGDE4SFBAoV/A4MH/ggBWIL7C8EfVoL4DwBHBFYIHBfYIRBAgT7CDgQEBgP4BgUBEIMDDgIMBgYMBg/gBgS5Ch/ABgUPFIMf4EHA4IEBHwUPCgJGCIIM/CgLgCAQJlBFIQFB44HBEIUBQYc/EIIHDAAIuBA4oeBRoSfBLAIHC/gHBEwIXC+AHBZghHBDwQADj4WCAHEPAwpWBKYYOCLwIHELYJUBghlDA4UcQogHBvgeDD4K0DDwIHBWgQeB4CyBh68CUAMf8DeCdIYHDdIfAfYjxCAgj2BAgbHCvwJCIIYCBBIMDHIX4BgUHFwMD+AMCA4Q0BAgg5CHwxICAQY5BdgQHBEgMDIYV/DgR1CA4PwP4KvDRgIACEYIHFWggABMQQHEZwd/Dwq1DHoTFEdooA/ACrBBcAZmC8DTCAATGBaYR+DwDTCRwbYDAASLBCIIGCFgQRBAG4='))),
46,
atob("EhooGyUkJiUnISYnFQ=="),
63+(scale<<8)+(1<<16)
);
return this;
};
Graphics.prototype.setXLargeFont = function(scale) {
// Actual height 53 (55 - 3)
this.setFontCustom(
E.toString(require('heatshrink').decompress(atob('AHM/8AIG/+AA4sD/wQGh/4EWQA/AC8YA40HNA0BRY8/RY0P/6LFgf//4iFA4IiFj4HBEQkHCAQiDHIIZGv4HCFQY5BDAo5CAAIpDDAfACA3wLYv//hsFKYxcCMgoiBOooiBQwwiBS40AHIgA/ACS/DLYjYCBAjQEBAYQDBAgHDUAbyDZQi3CegoHEVQQZFagUfW4Y0DaAgECaIJSEFYMPbIYNDv5ACGAIrBCgJ1EFYILCAAQWCj4zDGgILCegcDEQRNDHIIiCHgZ2BEQShFIqUDFYidCh5ODg4NCn40DAgd/AYR5BDILZEAAIMDAAYVCh7aHdYhKDbQg4Dv7rGBAihFCAwIDCAgA/AB3/eoa7GAAk/dgbVGDJrvCDK67DDIjaGdYpbCdYonCcQjjDEVUBEQ4A/AEMcAYV/NAUHcYUDawd/cYUPRYSmBBgaLBToP8BgYiBSgIiCj4iCg//EQSuDW4IMDVwYiCBgIiBBgrRDCATeBaIYqCv70DCgT4CEQMfIgQZBBoRnDv/3EQIvBDIffEQMHFwReBRYUfOgX/+IiDKIeHEQRRECwUHKwIuB8AiDIoJEBCwZFCv/4HIZaBIgPAEQS2CUYQiCD4SABEQcfOwIZBEQaHBO4RcEAAI/BEQQgBSIQiDTIRZBEQZuBVYQiDHoKWCEQQICFQIiDBAQeCEQQA/AANwA40BLIJ5BO4JWCBAUPAYR5En7RBUIQECN4SYCQQIiEh6CCEQk/BoQiBgYeCBoTrCAgT0CCgIfCFYQiBg4IBGgIiDj6rBg4rCBYLRDFYIiBbYIfBLgQiBIQYiD4JCCLgf/bQIWDBYV/EQV/BYXz/5FBgIiD5//IowZBD4M/NAX/BIPgDIJoC//5GgKUDn//4f/8KLE/wTBAAI8BEQPwj4HBVwYmBDgIZDN4QZCGYKJCHQP/JoSgCBATrCh5dBKITVDG4gICAAbvDAH5SCL4QADK4J5CCAiTCCAp1BCAqCDCAgiGCAIiFCAQiFeoIiFg6/FCAgiECAXnEQgQB/kfEQYQC4F/EQYQCgIiDfoIQBg4iDCAUAEQZUCcgIiDDIIQBEQhuBBoIiENoYiFDwQiECAQiFwEBPQQNCAQKDDEYMDDoMfRh4iGUwqvEESBiBaQ5oEbgr0FNAo+EEIwA+oAHGgJoFRAMHe4L0CAALNBBAT0BfwScDCAXweAL0DWgUPQYQiDwF/QYQiC/zTB+C0FBAL0CEQYIBGgMPCgIxBg4rCJIKsCh5IBBwTPCj4WBgYLBZ4V/MAIiBBQQrBEQYtCBYQiCO4QLFCwgiDIQIiGIoMHEQpFBn5FFD4JoENwRoGDgSUCAoKfBw//DgIiCT4auCFwN/T4RRET4TaCEQKoCDIQiCGgK/DAAQICdYQACHoIqCBAoQFEwIhFAH4AFQIROEj4IGXwIIGNwIACbgIhEBAiRCVwoqDTogHEW4QZFXgIZB/z9Cv49CF4MPBwI0Ca4LlB8ATCJoP4AoINDfQPAg7PBg4cBBwUfD4MfFYILCCwgOCf4QLEwEPCwILCgJaBn4WBBYQxCIQQiD+EDCYI5CBYRQBIo4fBMQIuBC4N/NAv8AoIcBSgU/FYIIBZIYrCW4hOCXIQZCgYUBv7jEh4uBZAscewZ8CgEgUYT0EEoQIBA4gICFQQIEHYQA+KQzdDAArdCAArpCEScHaIQiEvwiGe4QiFUwQiEbgIiFYIL0DEQTkBEQrJEEQc/cYYiCg4HBDIQiCfoRoEHQLaDEQQHBbQYiBCAT8Dn/BCAoXBJYP/OgZKC/6OEEARLCEQZLEEQZLEEQjKFEQI6EEQZLDEQbsGEQLjGYYYA/JIxzEg/AfgJSDAoPgfgiDC8COFAoPnaQj6CAAR+CW4TCFA4i6CDIqhCDIfwHoYHCYIN/GgKuBJ4JDBFYUf/C5CBYIZBv/Ag4ZBg4rBBYQTBAQIcBg4FBn5UBAQUfFwIfCEQeAgYfBAQUBFAKbCAQQiCGwIiE+A2BwBFNwE/AoM/EQJoIWwKCCh4cBFYKUERYV/W46uHFYIZGaJA0B/glBGYT0JIITiEMIJvCFQQAEHYQA/ABBlEOIhdGQAIRFSgQIBgQICn4IB8EAjiBCUYglCbQYeBEoQZCTwM/CYIZD/gEBUwIzBJ4UHYAU/EwIrBh4rCAoIXCn4rBCgUDAQN/FYMfBYIXBCYJnCBYXggf8HgQLCwEPEQQuBgJOECwILDCwgiLHIUHBYJFGD4IxBgYWCn4rBBwJoFDIYNBCgPADgKHBRYfDBQN/GAIrBToTLDVwYACDILiCWAb8DAAYzBYAjTCAAI9BAARNCBAoqCBAgQDFgbYCAH4AufgQACf4T8CAAT/CfgQACBwITCAAYOBCYQioh4iEAHQA=='))),
46,
atob("FR4uHyopKyksJSssGA=="),
70+(scale<<8)+(1<<16)
);
};
Graphics.prototype.setMediumFont = function(scale) {
// Actual height 41 (42 - 2)
@ -259,11 +274,12 @@ function draw() {
function drawDate(){
// Draw background
var y = H/5*2 + (settings.fullscreen ? 0 : 8);
var y = H/5*2;
g.reset().clearRect(0,0,W,W);
// Draw date
y -= settings.fullscreen ? 8 : 0;
y = parseInt(y/2);
y += settings.fullscreen ? 2 : 15;
var date = new Date();
var dateStr = date.getDate();
dateStr = ("0" + dateStr).substr(-2);
@ -276,14 +292,14 @@ function drawDate(){
var dayW = Math.max(g.stringWidth(dayStr), g.stringWidth(monthStr));
var fullDateW = dateW + 10 + dayW;
g.setFontAlign(-1,1);
g.setFontAlign(-1,0);
g.setMediumFont();
g.setColor(g.theme.fg);
g.drawString(dateStr, W/2 - fullDateW / 2, y+5);
g.drawString(dateStr, W/2 - fullDateW / 2, y+1);
g.setSmallFont();
g.drawString(monthStr, W/2 - fullDateW/2 + 10 + dateW, y+3);
g.drawString(dayStr, W/2 - fullDateW/2 + 10 + dateW, y-23);
g.drawString(dayStr, W/2 - fullDateW/2 + 10 + dateW, y-12);
g.drawString(monthStr, W/2 - fullDateW/2 + 10 + dateW, y+11);
}
@ -296,9 +312,16 @@ function drawTime(){
// Draw time
g.setColor(g.theme.bg);
g.setFontAlign(0,-1);
var timeStr = locale.time(date,1);
y += settings.fullscreen ? 14 : 10;
g.setFontAlign(0,0);
var hours = String(date.getHours());
var minutes = date.getMinutes();
minutes = minutes < 10 ? String("0") + minutes : minutes;
var colon = settings.hideColon ? "" : ":";
var timeStr = hours + colon + minutes;
// Set y coordinates correctly
y += parseInt((H - y)/2) + 5;
var infoEntry = getInfoEntry();
var infoStr = infoEntry[0];
@ -307,9 +330,13 @@ function drawTime(){
// Show large or small time depending on info entry
if(infoStr == null){
y += 10;
g.setLargeFont();
if(settings.hideColon){
g.setXLargeFont();
} else {
g.setLargeFont();
}
} else {
y -= 15;
g.setMediumFont();
}
g.drawString(timeStr, W/2, y);
@ -319,7 +346,7 @@ function drawTime(){
return;
}
y += H/5*2-5;
y += 35;
g.setFontAlign(0,0);
g.setSmallFont();
var imgWidth = 0;
@ -370,17 +397,6 @@ function queueDraw() {
}
/*
* Load clock, widgets and listen for events
*/
Bangle.loadWidgets();
// Clear the screen once, at startup and set the correct theme.
var bgOrig = g.theme.bg
var fgOrig = g.theme.fg
g.setTheme({bg:fgOrig,fg:bgOrig}).clear();
draw();
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{
if (on) {
@ -446,5 +462,17 @@ E.on("kill", function(){
});
/*
* Draw clock the first time
*/
// The upper part is inverse i.e. light if dark and dark if light theme
// is enabled. In order to draw the widgets correctly, we invert the
// dark/light theme as well as the colors.
g.setTheme({bg:g.theme.fg,fg:g.theme.bg, dark:!g.theme.dark}).clear();
// Load widgets and draw clock the first time
Bangle.loadWidgets();
draw();
// Show launcher when middle button pressed
Bangle.setUI("clock");

View File

@ -1,7 +1,7 @@
{
"id": "bwclk",
"name": "BW Clock",
"version": "0.06",
"version": "0.09",
"description": "BW Clock.",
"readme": "README.md",
"icon": "app.png",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@ -6,6 +6,7 @@
let settings = {
fullscreen: false,
showLock: true,
hideColon: false,
};
let saved_settings = storage.readJSON(SETTINGS_FILE, 1) || settings;
for (const key in saved_settings) {
@ -35,6 +36,14 @@
settings.showLock = !settings.showLock;
save();
},
},
'Hide Colon': {
value: settings.hideColon,
format: () => (settings.hideColon ? 'Yes' : 'No'),
onchange: () => {
settings.hideColon = !settings.hideColon;
save();
},
}
});
})

View File

@ -23,3 +23,4 @@
0.11: New color option: foreground color
Improve performance, reduce memory usage
Small optical adjustments
0.12: Allow configuration of update interval

View File

@ -848,8 +848,8 @@ Bangle.loadWidgets();
// schedule a draw for the next minute
setTimeout(function() {
// draw every 60 seconds
setInterval(draw,60000);
// draw in interval
setInterval(draw, settings.updateInterval * 1000);
}, 60000 - (Date.now() % 60000));
draw();

View File

@ -21,5 +21,6 @@
"circle2colorizeIcon": true,
"circle3colorizeIcon": true,
"circle4colorizeIcon": false,
"hrmValidity": 60
"hrmValidity": 60,
"updateInterval": 60
}

View File

@ -1,7 +1,7 @@
{ "id": "circlesclock",
"name": "Circles clock",
"shortName":"Circles clock",
"version":"0.11",
"version":"0.12",
"description": "A clock with three or four circles for different data at the bottom in a probably familiar style",
"icon": "app.png",
"screenshots": [{"url":"screenshot-dark.png"}, {"url":"screenshot-light.png"}, {"url":"screenshot-dark-4.png"}, {"url":"screenshot-light-4.png"}],

View File

@ -58,6 +58,16 @@
min: 0, max: 2,
format: v => weatherData[v],
onchange: x => save('weatherCircleData', weatherData[x]),
},
/*LANG*/'update interval': {
value: settings.updateInterval,
min: 0,
max : 3600,
step: 30,
format: x => {
return x + 's';
},
onchange: x => save('updateInterval', x),
}
};
E.showMenu(menu);
@ -100,7 +110,7 @@
/*LANG*/'valid period': {
value: settings.hrmValidity,
min: 10,
max : 600,
max : 1800,
step: 10,
format: x => {
return x + "s";
@ -117,9 +127,9 @@
/*LANG*/'< Back': ()=>showMainMenu(),
/*LANG*/'goal': {
value: settings.stepGoal,
min: 2000,
min: 1000,
max : 50000,
step: 2000,
step: 500,
format: x => {
return x;
},
@ -127,9 +137,9 @@
},
/*LANG*/'distance goal': {
value: settings.stepDistanceGoal,
min: 2000,
max : 30000,
step: 1000,
min: 1000,
max : 50000,
step: 500,
format: x => {
return x;
},

1
apps/diceroll/ChangeLog Normal file
View File

@ -0,0 +1 @@
0.01: App created

View File

@ -0,0 +1 @@
E.toArrayBuffer(atob("ICABAAAAAAAAAAAAAAAAAAHAAAAP8AAAfn4AA/APwA+DwfAPg8HwD+AH8Az4HzAMPnwwDAfgMAwBgDAMCYAwDA2YMAwhmDAMIZAwDCGDMA2BgzAMgYAwDAGAMA8BgPADwYPAAPGPgAB9ngAAH/gAAAfgAAABgAAAAAAAAAAAAAAAAAA="))

108
apps/diceroll/app.js Normal file
View File

@ -0,0 +1,108 @@
var init_message = true;
var acc_data;
var die_roll = 1;
var selected_die = 0;
var roll = 0;
const dices = [4, 6, 10, 12, 20];
g.setFontAlign(0,0);
Bangle.on('touch', function(button, xy) {
// Change die if not rolling
if(roll < 1){
if(selected_die <= 3){
selected_die++;
}else{
selected_die = 0;
}
}
//Disable initial message
init_message = false;
});
function rect(){
x1 = g.getWidth()/2 - 35;
x2 = g.getWidth()/2 + 35;
y1 = g.getHeight()/2 - 35;
y2 = g.getHeight()/2 + 35;
g.drawRect(x1, y1, x2, y2);
}
function pentagon(){
x1 = g.getWidth()/2;
y1 = g.getHeight()/2 - 50;
x2 = g.getWidth()/2 - 50;
y2 = g.getHeight()/2 - 10;
x3 = g.getWidth()/2 - 30;
y3 = g.getHeight()/2 + 30;
x4 = g.getWidth()/2 + 30;
y4 = g.getHeight()/2 + 30;
x5 = g.getWidth()/2 + 50;
y5 = g.getHeight()/2 - 10;
g.drawPoly([x1, y1, x2, y2, x3, y3, x4, y4, x5, y5], true);
}
function triangle(){
x1 = g.getWidth()/2;
y1 = g.getHeight()/2 - 57;
x2 = g.getWidth()/2 - 50;
y2 = g.getHeight()/2 + 23;
x3 = g.getWidth()/2 + 50;
y3 = g.getHeight()/2 + 23;
g.drawPoly([x1, y1, x2, y2, x3, y3], true);
}
function drawDie(variant) {
if(variant == 1){
//Rect, 6
rect();
}else if(variant == 3){
//Pentagon, 12
pentagon();
}else{
//Triangle, 4, 10, 20
triangle();
}
}
function initMessage(){
g.setFont("6x8", 2);
g.drawString("Dice-n-Roll", g.getWidth()/2, 20);
g.drawString("Shake to roll", g.getWidth()/2, 60);
g.drawString("Tap to change", g.getWidth()/2, 80);
g.drawString("Tap to start", g.getWidth()/2, 150);
}
function rollDie(){
acc_data = Bangle.getAccel();
if(acc_data.diff > 0.3){
roll = 3;
}
//Mange the die "roll" by chaning the number a few times
if(roll > 0){
g.drawString("Rolling!", g.getWidth()/2, 150);
die_roll = Math.abs(E.hwRand()) % dices[selected_die] + 1;
roll--;
}
//Draw dice graphics
drawDie(selected_die);
//Draw dice number
g.setFontAlign(0,0);
g.setFont("Vector", 45);
g.drawString(die_roll, g.getWidth()/2, g.getHeight()/2);
//Draw selected die in right corner
g.setFont("6x8", 2);
g.drawString(dices[selected_die], g.getWidth()-15, 15);
}
function main() {
g.clear();
if(init_message){
initMessage();
}else{
rollDie();
}
Bangle.setLCDPower(1);
}
var interval = setInterval(main, 300);

BIN
apps/diceroll/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,14 @@
{ "id": "diceroll",
"name": "Dice-n-Roll",
"shortName":"Dice-n-Roll",
"icon": "app.png",
"version":"0.01",
"description": "A dice app with a few different dice.",
"screenshots": [{"url":"diceroll_screenshot.png"}],
"tags": "game",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"diceroll.app.js","url":"app.js"},
{"name":"diceroll.img","url":"app-icon.js","evaluate":true}
]
}

17
apps/dinoClock/README.md Normal file
View File

@ -0,0 +1,17 @@
# dinoClock
Watchface with T-Rex Dinosaur from Chrome.
It displays current temperature and weather.
**Warning**: Element position and styles can change in the future.
Based on the [Weather Clock](https://github.com/espruino/BangleApps/tree/master/apps/weatherClock).
# Requirements
**This clock requires Gadgetbridge and the weather app in order to get weather data!**
See the [Bangle.js Gadgetbridge documentation](https://www.espruino.com/Gadgetbridge) for instructions on setting up Gadgetbridge and weather.
![Screenshot](screens/screen1.png)

219
apps/dinoClock/app.js Normal file
View File

@ -0,0 +1,219 @@
const storage = require('Storage');
const locale = require("locale");
// add modifiied 4x5 numeric font
(function(graphics) {
graphics.prototype.setFont4x5NumPretty = function() {
this.setFontCustom(atob("IQAQDJgH4/An4QXr0Fa/BwnwdrcH63BCHwfr8Ha/"),45,atob("AwIEBAQEBAQEBAQEBA=="),5);
};
})(Graphics);
// add font for days of the week
(function(graphics) {
graphics.prototype.setFontDoW = function() {
this.setFontCustom(atob("///////ADgB//////+AHAD//////gAAAH//////4D8B+A///////4AcAOAH//////4AcAOAAAAAB//////wA4AcAP//////wAAAAAAAA//////4AcAP//////wA4Af//////gAAAH//////5z85+c/OfnOAA4AcAOAH//////4AcAOAAAAAB//////wcAOAHB//////wAAAAAAAA///////ODnBzg5wc4AAAAD//////84OcH//8/+fAAAAAAAAAAAAA/z/5/8/OfnPz/5/8/wAAAD//////84OcH//////AAAAAAAAAAAAA/z/5/8/OfnPz/5/8/wAAAD//////gBwA///////AAAAAAAAAAAAA"),48,24,13);
};
})(Graphics);
const SUN = 1;
const PART_SUN = 2;
const CLOUD = 3;
const SNOW = 4;
const RAIN = 5;
const STORM = 6;
const ERR = 7;
/**
Choose weather icon based on weather const
Weather icons from https://icons8.com/icon/set/weather/ios-glyphs
Error icon from https://icons8.com/icon/set/error-cloud/ios-glyphs
**/
function weatherIcon(weather) {
switch (weather) {
case SUN:
return atob("Hh4BAAAAAAAMAAAAMAAAAMAAAAMAABgMBgBwADgA4AHAAY/GAAB/gAAD/wAAH/4AAP/8AAP/8AfP/8+fP/8+AP/8AAP/8AAH/4AAD/wAAB/gAAY/GAA4AHABwADgBgMBgAAMAAAAMAAAAMAAAAMAAAAAAAA=");
case PART_SUN:
return atob("Hh4BAAAAAAAAAAAMAAAAMAAAEMIAAOAcAAGAYAAAeAAAA/AAAB/gAA5/gAA5/g+AB+D/gA4H/wAR//wGD//4OD//4EH//4AH//4Af//+Af//+A////A////A////A///+Af//+AH//4AAAAAAAAAAAAAAAA=");
case CLOUD:
return atob("Hh4BAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAf+AAA//AAB//gAf//gB///wB///wD///wD///wP///8f///+f///+////////////////////f///+f///+P///8D///wAAAAAAAAAAAAAAAAAAAAAAAAAA=");
case SNOW:
return atob("Hh4BAAAAAAAAAAAAAAAAAHwAAAf8AAA/+AAH/+AAf//AAf8/AA/8/AB/gHgH/wP4H/wP4P/gH8P/8/8P/8/8P///4H///4B///gAAAAAAMAAAAMAAAB/gGAA/AfgA/AfgB/gfgAMAfgAMAGAAAAAAAAAAAA=");
case RAIN:
return atob("Hh4BAAAAAAAAAAAAAAAAAHwAAAf8AAA/+AAH/+AAf//AAf//AA///AB///gH///4H///4P///8P///8P///8P///4H///4B///gAAAAAAAAAABgBgABgBgABhhhgABgBgABgBgAAAAAAAAAAAAAAAAAAAAA=");
case STORM:
return atob("Hh4BAAAAAAAAAAAAAAAAAHwAAAf8AAA/+AAH/+AAf//AAf//AA///AB///gH///4H/x/4P/g/8P/k/8P/E/8P/M/4H+MP4B+cHgAAfgAAA/gABg/AABgHAABgGBgAAGBgAAEBgAAEAAAAAAAAAAAAAAAAAA=");
case ERR:
default:
return atob("Hh4BAAAAAAAAAAAAAAAAAAAAAAAAAAAH4AAAf+AAA//AAB//gAf//gB///wB/z/wD/z/wD/z/wP/z/8f/z/+f/z/+//z//////////////z//f/z/+f///+P///8D///wAAAAAAAAAAAAAAAAAAAAAAAAAA=");
}
}
/**
Choose weather icon to display based on condition.
Based on function from the Bangle weather app so it should handle all of the conditions
sent from gadget bridge.
*/
function chooseIcon(condition) {
condition = condition.toLowerCase();
if (condition.includes("thunderstorm")) return weatherIcon(STORM);
if (condition.includes("freezing")||condition.includes("snow")||
condition.includes("sleet")) {
return weatherIcon(SNOW);
}
if (condition.includes("drizzle")||
condition.includes("shower")) {
return weatherIcon(RAIN);
}
if (condition.includes("rain")) return weatherIcon(RAIN);
if (condition.includes("clear")) return weatherIcon(SUN);
if (condition.includes("few clouds")) return weatherIcon(PART_SUN);
if (condition.includes("scattered clouds")) return weatherIcon(CLOUD);
if (condition.includes("clouds")) return weatherIcon(CLOUD);
if (condition.includes("mist") ||
condition.includes("smoke") ||
condition.includes("haze") ||
condition.includes("sand") ||
condition.includes("dust") ||
condition.includes("fog") ||
condition.includes("ash") ||
condition.includes("squalls") ||
condition.includes("tornado")) {
return weatherIcon(CLOUD);
}
return weatherIcon(CLOUD);
}
/*
* Choose weather icon to display based on weather conditition code
* https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2
*/
function chooseIconByCode(code) {
const codeGroup = Math.round(code / 100);
switch (codeGroup) {
case 2: return weatherIcon(STORM);
case 3: return weatherIcon(RAIN);
case 5: return weatherIcon(RAIN);
case 6: return weatherIcon(SNOW);
case 7: return weatherIcon(CLOUD);
case 8:
switch (code) {
case 800: return weatherIcon(SUN);
case 801: return weatherIcon(PART_SUN);
default: return weatherIcon(CLOUD);
}
default: return weatherIcon(CLOUD);
}
}
/**
Get weather stored in json file by weather app.
*/
function getWeather() {
let jsonWeather = storage.readJSON('weather.json');
return jsonWeather;
}
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
},60000-(Date.now()%60000));
}
// only draw the first time
function drawBg() {
var bgImg = require("heatshrink").decompress(atob("2E7wINKn///+AEaIVUgIUB//wCs/5CtRXrCvMD8AVTg4LFCv4VZ/iSLCrwWMCrMOAQMPCp7cBCojjFCo/xFgIVQgeHCopABCpcH44Vuh/AQQX/wAV7+F/Cq/nCsw/CCqyvRCvgODCqfAgEDCp4QCSIIVQgIOBDQgGDABX/NgIECCp8HCrM/CgP4CqKaCCqSfCCqq1BCqBuB54VqgYVG/gCECp0BwgCDCp8HgYCDCo/wCo0MgHAjACBj7rDABS1Bv4lBv4rPAAsPCo3+gbbPJAIVFiAXMFZ2AUQsAuAQHiOAgJeEA"));
g.reset();
g.drawImage(bgImg,0,101);
}
function square(x,y,w,e) {
g.setColor("#000").fillRect(x,y,x+w,y+w);
g.setColor("#fff").fillRect(x+e,y+e,x+w-e,y+w-e);
}
function draw() {
var d = new Date();
var h = d.getHours(), m = d.getMinutes();
h = ("0"+h).substr(-2);
m = ("0"+m).substr(-2);
var day = d.getDate(), mon = d.getMonth(), dow = d.getDay();
day = ("0"+day).substr(-2);
mon = ("0"+(mon+1)).substr(-2);
dow = ((dow+6)%7).toString();
date = day+"."+mon;
var weatherJson = getWeather();
var wIcon;
var temp;
if(weatherJson && weatherJson.weather){
var currentWeather = weatherJson.weather;
temp = locale.temp(currentWeather.temp-273.15).match(/^(\D*\d*)(.*)$/);
const code = currentWeather.code||-1;
if (code > 0) {
wIcon = chooseIconByCode(code);
} else {
wIcon = chooseIcon(currentWeather.txt);
}
}else{
temp = "";
wIcon = weatherIcon(ERR);
}
g.reset();
g.clearRect(22,35,153,75);
g.setFont("4x5NumPretty",8);
g.fillRect(84,42,92,49);
g.fillRect(84,60,92,67);
g.drawString(h,22,35);
g.drawString(m,98,35);
g.clearRect(22,95,22+4*2*4+2*4,95+2*5);
g.setFont("4x5NumPretty",2);
g.drawString(date,22,95);
g.clearRect(22,79,22+24,79+13);
g.setFont("DoW");
g.drawString(dow,22,79);
g.drawImage(wIcon,126,81);
g.clearRect(108,114,176,114+4*5);
if (temp != "") {
var tempWidth;
const mid=126+15;
if (temp[1][0]=="-") {
// do not account for - when aligning
const minusWidth=3*4;
tempWidth = minusWidth+(temp[1].length-1)*4*4;
x = mid-Math.round((tempWidth-minusWidth)/2)-minusWidth;
} else {
tempWidth = temp[1].length*4*4;
x = mid-Math.round(tempWidth/2);
}
g.setFont("4x5NumPretty",4);
g.drawString(temp[1],x,114);
square(x+tempWidth,114,6,2);
}
// queue draw in one minute
queueDraw();
}
g.clear();
drawBg();
Bangle.setUI("clock"); // Show launcher when middle button pressed
Bangle.loadWidgets();
Bangle.drawWidgets();
draw();

BIN
apps/dinoClock/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

1
apps/dinoClock/icon.js Normal file
View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgJC/AAVh/E/hgFC/O/AoMB8EZwc8AoUYgYFBgFgjAXDAowXBAo8B/ARBn4FGAAsBmAFE2ADBhwFEj4VEn+AgPvAontgfwv+ABIMCMwIVCgf4FIWAAoN3sAFCwERoEB0MHwF3gEF0MPwFEAoW/4ALD/4tCg/hAoYhB/5ZDwF+Aok0gEIkEf/4AB8eMBoM2bkw="))

View File

@ -0,0 +1,17 @@
{
"id": "dinoClock",
"name": "Dino Clock",
"description": "Clock with dino from Chrome",
"screenshots": [{"url":"screens/screen1.png"}],
"icon": "app.png",
"version": "0.01",
"type": "clock",
"tags": "clock, weather, dino, trex, chrome",
"supports": ["BANGLEJS2"],
"allow_emulator": true,
"readme": "README.md",
"storage": [
{"name":"dinoClock.app.js","url":"app.js"},
{"name":"dinoClock.img","url":"icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -11,3 +11,4 @@
0.11: Fix bangle.js 1 white icons not displaying
0.12: On Bangle 2 change to swiping up/down to move between pages as to match page indicator. Swiping from left to right now loads the clock.
0.13: Added swipeExit setting so that left-right to exit is an option
0.14: Don't move pages when doing exit swipe.

View File

@ -29,6 +29,6 @@ Bangle 2:
**Touch** - icon to select, scond touch launches app
**Swipe Left** - move to next page of app icons
**Swipe Left/Up** - move to next page of app icons
**Swipe Right** - move to previous page of app icons
**Swipe Right/Down** - move to previous page of app icons

View File

@ -93,7 +93,7 @@ Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{
if (dirUpDown==-1||dirLeftRight==-1){
++page; if (page>maxPage) page=0;
drawPage(page);
} else if (dirUpDown==1||dirLeftRight==1){
} else if (dirUpDown==1||(dirLeftRight==1 && !settings.swipeExit)){
--page; if (page<0) page=maxPage;
drawPage(page);
}

View File

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

1
apps/f9lander/ChangeLog Normal file
View File

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

33
apps/f9lander/README.md Normal file
View File

@ -0,0 +1,33 @@
# F9 Lander
Land a Falcon 9 booster on a drone ship.
## Game play
Attempt to land your Falcon 9 booster on a drone ship before running out of fuel.
A successful landing requires:
* setting down on the ship
* the booster has to be mostly vertical
* the landing speed cannot be too high
## Controls
The angle of the booster is controlled by tilting the watch side-to-side. The
throttle level is controlled by tilting the watch forward and back:
* screen horizontal (face up) means no throttle
* screen vertical corresponds to full throttle
The fuel burn rate is proportional to the throttle level.
## Creators
Liam Kl. B.
Marko Kl. B.
## Screenshots
![](f9lander_screenshot1.png)
![](f9lander_screenshot2.png)
![](f9lander_screenshot3.png)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwcA/4AD/P8yVJkgCCye27dt2wRE//kCIuSuwRIBwgCCpwRQpIRRnYRQkmdCIvPCJICBEZ4RG/IRP/15CJ/z5IRPz4RM/gQB/n+BxICCn/z/P/BxQCDz7mIAX4Cq31/CJ+ebpiYE/IR/CNP/5IROnn//4jP5DFQ5sJCKAjPk3oCMMk4QRQAX4Ckn7jBAA/5CK8nCJPJNHA"))

150
apps/f9lander/app.js Normal file
View File

@ -0,0 +1,150 @@
const falcon9 = Graphics.createImage(`
xxxxx
xxxxx xxxxx
x x
x x
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxx
xxxxxxxxx
xx xxxxx xx
xx xx`);
const droneShip = Graphics.createImage(`
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
`);
const droneX = Math.floor(Math.random()*(g.getWidth()-droneShip.width-40) + 20)
const cloudOffs = Math.floor(Math.random()*g.getWidth()/2);
const oceanHeight = g.getHeight()*0.1;
const targetY = g.getHeight()-oceanHeight-falcon9.height/2;
var booster = { x : g.getWidth()/4 + Math.random()*g.getWidth()/2,
y : 20,
vx : 0,
vy : 0,
mass : 100,
fuel : 100 };
var exploded = false;
var nExplosions = 0;
var landed = false;
const gravity = 4;
const dt = 0.1;
const fuelBurnRate = 20*(176/g.getHeight());
const maxV = 12;
function flameImageGen (throttle) {
var str = " xxx \n xxx \n";
str += "xxxxx\n".repeat(throttle);
str += " xxx \n x \n";
return Graphics.createImage(str);
}
function drawFalcon(x, y, throttle, angle) {
g.setColor(1, 1, 1).drawImage(falcon9, x, y, {rotate:angle});
if (throttle>0) {
var flameImg = flameImageGen(throttle);
var r = falcon9.height/2 + flameImg.height/2-1;
var xoffs = -Math.sin(angle)*r;
var yoffs = Math.cos(angle)*r;
if (Math.random()>0.7) g.setColor(1, 0.5, 0);
else g.setColor(1, 1, 0);
g.drawImage(flameImg, x+xoffs, y+yoffs, {rotate:angle});
}
}
function drawBG() {
g.setBgColor(0.2, 0.2, 1).clear();
g.setColor(0, 0, 1).fillRect(0, g.getHeight()-oceanHeight, g.getWidth()-1, g.getHeight()-1);
g.setColor(0.5, 0.5, 1).fillCircle(cloudOffs+34, 30, 15).fillCircle(cloudOffs+60, 35, 20).fillCircle(cloudOffs+75, 20, 10);
g.setColor(1, 1, 0).fillCircle(g.getWidth(), 0, 20);
g.setColor(1, 1, 1).drawImage(droneShip, droneX, g.getHeight()-oceanHeight-1);
}
function showFuel() {
g.setColor(0, 0, 0).setFont("4x6:2").setFontAlign(-1, -1, 0).drawString("Fuel: "+Math.abs(booster.fuel).toFixed(0), 4, 4);
}
function renderScreen(input) {
drawBG();
showFuel();
drawFalcon(booster.x, booster.y, Math.floor(input.throttle*12), input.angle);
}
function getInputs() {
var accel = Bangle.getAccel();
var a = Math.PI/2 + Math.atan2(accel.y, accel.x);
var t = (1+accel.z);
if (t > 1) t = 1;
if (t < 0) t = 0;
if (booster.fuel<=0) t = 0;
return {throttle: t, angle: a};
}
function epilogue(str) {
g.setFont("Vector", 24).setFontAlign(0, 0, 0).setColor(0, 0, 0).drawString(str, g.getWidth()/2, g.getHeight()/2).flip();
g.setFont("Vector", 16).drawString("<= again exit =>", g.getWidth()/2, g.getHeight()/2+20);
clearInterval(stepInterval);
Bangle.on("swipe", (d) => { if (d>0) load(); else load('f9lander.app.js'); });
}
function gameStep() {
if (exploded) {
if (nExplosions++ < 15) {
var r = Math.random()*25;
var x = Math.random()*30 - 15;
var y = Math.random()*30 - 15;
g.setColor(1, Math.random()*0.5+0.5, 0).fillCircle(booster.x+x, booster.y+y, r);
if (nExplosions==1) Bangle.buzz(600);
}
else epilogue("You crashed!");
}
else {
var input = getInputs();
if (booster.y >= targetY) {
// console.log(booster.x + " " + booster.y + " " + booster.vy + " " + droneX + " " + input.angle);
if (Math.abs(booster.x-droneX-droneShip.width/2)<droneShip.width/2 && Math.abs(input.angle)<Math.PI/8 && booster.vy<maxV) {
renderScreen({angle:0, throttle:0});
epilogue("You landed!");
}
else exploded = true;
}
else {
booster.x += booster.vx*dt;
booster.y += booster.vy*dt;
booster.vy += gravity*dt;
booster.fuel -= input.throttle*dt*fuelBurnRate;
booster.vy += -Math.cos(input.angle)*input.throttle*gravity*3*dt;
booster.vx += Math.sin(input.angle)*input.throttle*gravity*3*dt;
renderScreen(input);
}
}
}
var stepInterval;
Bangle.setLCDTimeout(0);
renderScreen({angle:0, throttle:0});
g.setFont("Vector", 24).setFontAlign(0, 0, 0).setColor(0, 0, 0).drawString("Swipe to start", g.getWidth()/2, g.getHeight()/2);
Bangle.on("swipe", () => {
stepInterval = setInterval(gameStep, Math.floor(1000*dt));
Bangle.removeListener("swipe");
});

BIN
apps/f9lander/f9lander.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 722 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -0,0 +1,15 @@
{ "id": "f9lander",
"name": "Falcon9 Lander",
"shortName":"F9lander",
"version":"0.01",
"description": "Land a rocket booster",
"icon": "f9lander.png",
"screenshots" : [ { "url":"f9lander_screenshot1.png" }, { "url":"f9lander_screenshot2.png" }, { "url":"f9lander_screenshot3.png" }],
"readme": "README.md",
"tags": "game",
"supports" : ["BANGLEJS", "BANGLEJS2"],
"storage": [
{"name":"f9lander.app.js","url":"app.js"},
{"name":"f9lander.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -1,2 +1,4 @@
0.01: New Clock Nifty A
0.02: Shows the current week number (ISO8601), can be disabled via settings ""
0.02: Shows the current week number (ISO8601), can be disabled via settings
0.03: Call setUI before loading widgets
Improve settings page

View File

@ -1,13 +1,12 @@
# Nifty-A Clock
Colors are black/white - photos have non correct camera color "blue"
Colors are black/white - photos have non correct camera color "blue".
## This is the clock
This is the clock:
![](screenshot_nifty.png)
## The week number (ISO8601) can be turned of in settings
(default is **"On"**)
The week number (ISO8601) can be turned off in settings (default is `On`)
![](screenshot_settings_nifty.png)

View File

@ -1,6 +1,6 @@
const locale = require("locale");
const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
const CFG = require('Storage').readJSON("ffcniftya.json", 1) || {showWeekNum: true};
const is12Hour = Object.assign({ "12hour": false }, require("Storage").readJSON("setting.json", true))["12hour"];
const showWeekNum = Object.assign({ showWeekNum: true }, require('Storage').readJSON("ffcniftya.json", true))["showWeekNum"];
/* Clock *********************************************/
const scale = g.getWidth() / 176;
@ -17,16 +17,17 @@ const center = {
y: Math.round(((viewport.height - widget) / 2) + widget),
}
function ISO8601_week_no(date) { //copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480
var tdt = new Date(date.valueOf());
var dayn = (date.getDay() + 6) % 7;
tdt.setDate(tdt.getDate() - dayn + 3);
var firstThursday = tdt.valueOf();
tdt.setMonth(0, 1);
if (tdt.getDay() !== 4) {
tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7);
}
return 1 + Math.ceil((firstThursday - tdt) / 604800000);
// copied from: https://gist.github.com/IamSilviu/5899269#gistcomment-3035480
function ISO8601_week_no(date) {
var tdt = new Date(date.valueOf());
var dayn = (date.getDay() + 6) % 7;
tdt.setDate(tdt.getDate() - dayn + 3);
var firstThursday = tdt.valueOf();
tdt.setMonth(0, 1);
if (tdt.getDay() !== 4) {
tdt.setMonth(0, 1 + ((4 - tdt.getDay()) + 7) % 7);
}
return 1 + Math.ceil((firstThursday - tdt) / 604800000);
}
function d02(value) {
@ -59,7 +60,7 @@ function draw() {
g.drawString(year, centerDatesScaleX, center.y - 62 * scale);
g.drawString(month, centerDatesScaleX, center.y - 44 * scale);
g.drawString(day, centerDatesScaleX, center.y - 26 * scale);
if (CFG.showWeekNum) g.drawString(d02(ISO8601_week_no(now)), centerDatesScaleX, center.y + 15 * scale);
if (showWeekNum) g.drawString(weekNum, centerDatesScaleX, center.y + 15 * scale);
g.drawString(monthName, centerDatesScaleX, center.y + 48 * scale);
g.drawString(dayName, centerDatesScaleX, center.y + 66 * scale);
}
@ -79,7 +80,6 @@ function clearTickTimer() {
function queueNextTick() {
clearTickTimer();
tickTimer = setTimeout(tick, 60000 - (Date.now() % 60000));
// tickTimer = setTimeout(tick, 3000);
}
function tick() {
@ -91,21 +91,16 @@ function tick() {
// Clear the screen once, at startup
g.clear();
// Start ticking
tick();
// Stop updates when LCD is off, restart when on
Bangle.on('lcdPower', (on) => {
if (on) {
tick(); // Start ticking
tick();
} else {
clearTickTimer(); // stop ticking
clearTickTimer();
}
});
// Load widgets
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();
// Show launcher when middle button pressed
Bangle.setUI("clock");

View File

@ -1,7 +1,7 @@
{
"id": "ffcniftya",
"name": "Nifty-A Clock",
"version": "0.02",
"version": "0.03",
"description": "A nifty clock with time and date",
"icon": "app.png",
"screenshots": [{"url":"screenshot_nifty.png"}],

View File

@ -1,23 +1,15 @@
(function(back) {
var FILE = "ffcniftya.json";
// Load settings
var cfg = require('Storage').readJSON(FILE, 1) || { showWeekNum: true };
(function (back) {
const settings = Object.assign({ showWeekNum: true }, require("Storage").readJSON("ffcniftya.json", true));
function writeSettings() {
require('Storage').writeJSON(FILE, cfg);
}
// Show the menu
E.showMenu({
"" : { "title" : "Nifty-A Clock" },
"< Back" : () => back(),
'week number?': {
value: cfg.showWeekNum,
format: v => v?"On":"Off",
"": { "title": "Nifty-A Clock" },
"< Back": () => back(),
/*LANG*/"Show Week Number": {
value: settings.showWeekNum,
onchange: v => {
cfg.showWeekNum = v;
writeSettings();
settings.showWeekNum = v;
require("Storage").writeJSON("ffcniftya.json", settings);
}
}
});
})
})

View File

@ -1,2 +1,5 @@
0.01: New Clock Nifty B
0.02: Added configuration
0.02: Added configuration
0.03: Call setUI before loading widgets
Fix bug with black being unselectable
Improve settings page

View File

@ -1,9 +1,6 @@
# Nifty Series B Clock
- Display Time and Date
- Color Configuration
##
- Colour Configuration
![](screenshot.png)

View File

@ -1,9 +1,5 @@
const locale = require("locale");
const storage = require('Storage');
const is12Hour = (storage.readJSON("setting.json", 1) || {})["12hour"];
const color = (storage.readJSON("ffcniftyb.json", 1) || {})["color"] || 63488 /* red */;
const is12Hour = Object.assign({ "12hour": false }, require("Storage").readJSON("setting.json", true))["12hour"];
const color = Object.assign({ color: 63488 }, require("Storage").readJSON("ffcniftyb.json", true)).color; // Default to RED
/* Clock *********************************************/
const scale = g.getWidth() / 176;
@ -19,7 +15,7 @@ const center = {
};
function d02(value) {
return ('0' + value).substr(-2);
return ("0" + value).substr(-2);
}
function renderEllipse(g) {
@ -35,8 +31,8 @@ function renderText(g) {
const month = d02(now.getMonth() + 1);
const year = now.getFullYear();
const month2 = locale.month(now, 3);
const day2 = locale.dow(now, 3);
const month2 = require("locale").month(now, 3);
const day2 = require("locale").dow(now, 3);
g.setFontAlign(1, 0).setFont("Vector", 90 * scale);
g.drawString(hour, center.x + 32 * scale, center.y - 31 * scale);
@ -96,7 +92,6 @@ function startTick(run) {
stopTick();
run();
ticker = setTimeout(() => startTick(run), 60000 - (Date.now() % 60000));
// ticker = setTimeout(() => startTick(run), 3000);
}
/* Init **********************************************/
@ -104,7 +99,7 @@ function startTick(run) {
g.clear();
startTick(draw);
Bangle.on('lcdPower', (on) => {
Bangle.on("lcdPower", (on) => {
if (on) {
startTick(draw);
} else {
@ -112,7 +107,6 @@ Bangle.on('lcdPower', (on) => {
}
});
Bangle.setUI("clock");
Bangle.loadWidgets();
Bangle.drawWidgets();
Bangle.setUI("clock");

View File

@ -1,8 +1,8 @@
{
"id": "ffcniftyb",
"name": "Nifty-B Clock",
"version": "0.02",
"description": "A nifty clock (series B) with time, date and color configuration",
"version": "0.03",
"description": "A nifty clock (series B) with time, date and colour configuration",
"icon": "app.png",
"screenshots": [{"url":"screenshot.png"}],
"type": "clock",

View File

@ -1,49 +1,31 @@
(function (back) {
const storage = require('Storage');
const SETTINGS_FILE = "ffcniftyb.json";
const settings = Object.assign({ color: 63488 }, require("Storage").readJSON("ffcniftyb.json", true));
const colors = {
65535: 'White',
63488: 'Red',
65504: 'Yellow',
2047: 'Cyan',
2016: 'Green',
31: 'Blue',
0: 'Black',
65535: /*LANG*/"White",
63488: /*LANG*/"Red",
65504: /*LANG*/"Yellow",
2047: /*LANG*/"Cyan",
2016: /*LANG*/"Green",
31: /*LANG*/"Blue",
0: /*LANG*/"Black"
}
function load(settings) {
return Object.assign(settings, storage.readJSON(SETTINGS_FILE, 1) || {});
}
const menu = {};
menu[""] = { title: "Nifty-B Clock" };
menu["< Back"] = back;
function save(settings) {
storage.write(SETTINGS_FILE, settings)
}
const settings = load({
color: 63488 /* red */,
Object.keys(colors).forEach(color => {
var label = colors[color];
menu[label] = {
value: settings.color == color,
onchange: () => {
settings.color = color;
require("Storage").write("ffcniftyb.json", settings);
setTimeout(load, 10);
}
};
});
const saveColor = (color) => () => {
settings.color = color;
save(settings);
back();
};
function showMenu(items, opt) {
items[''] = opt || {};
items['< Back'] = back;
E.showMenu(items);
}
showMenu(
Object.keys(colors).reduce((menu, color) => {
menu[colors[color]] = saveColor(color);
return menu;
}, {}),
{
title: 'Color',
selected: Object.keys(colors).indexOf(settings.color)
}
);
E.showMenu(menu);
});

View File

@ -13,3 +13,4 @@
0.12: Add setting for Daily Step Goal
0.13: Add support for internationalization
0.14: Move settings
0.15: Fix charts (fix #1366)

View File

@ -2,7 +2,7 @@
Logs health data to a file every 10 minutes, and provides an app to view it
**BETA - requires firmware 2v11**
**BETA - requires firmware 2v11 or later**
## Usage

View File

@ -46,7 +46,7 @@ function menuHRM() {
function stepsPerHour() {
E.showMessage(/*LANG*/"Loading...");
let data = new Uint16Array(24);
var data = new Uint16Array(24);
require("health").readDay(new Date(), h=>data[h.hr]+=h.steps);
g.clear(1);
Bangle.drawWidgets();
@ -57,7 +57,7 @@ function stepsPerHour() {
function stepsPerDay() {
E.showMessage(/*LANG*/"Loading...");
let data = new Uint16Array(31);
var data = new Uint16Array(31);
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.steps);
g.clear(1);
Bangle.drawWidgets();
@ -68,8 +68,8 @@ function stepsPerDay() {
function hrmPerHour() {
E.showMessage(/*LANG*/"Loading...");
let data = new Uint16Array(24);
let cnt = new Uint8Array(23);
var data = new Uint16Array(24);
var cnt = new Uint8Array(23);
require("health").readDay(new Date(), h=>{
data[h.hr]+=h.bpm;
if (h.bpm) cnt[h.hr]++;
@ -84,8 +84,8 @@ function hrmPerHour() {
function hrmPerDay() {
E.showMessage(/*LANG*/"Loading...");
let data = new Uint16Array(31);
let cnt = new Uint8Array(31);
var data = new Uint16Array(31);
var cnt = new Uint8Array(31);
require("health").readDailySummaries(new Date(), h=>{
data[h.day]+=h.bpm;
if (h.bpm) cnt[h.day]++;
@ -100,7 +100,7 @@ function hrmPerDay() {
function movementPerHour() {
E.showMessage(/*LANG*/"Loading...");
let data = new Uint16Array(24);
var data = new Uint16Array(24);
require("health").readDay(new Date(), h=>data[h.hr]+=h.movement);
g.clear(1);
Bangle.drawWidgets();
@ -111,7 +111,7 @@ function movementPerHour() {
function movementPerDay() {
E.showMessage(/*LANG*/"Loading...");
let data = new Uint16Array(31);
var data = new Uint16Array(31);
require("health").readDailySummaries(new Date(), h=>data[h.day]+=h.movement);
g.clear(1);
Bangle.drawWidgets();
@ -183,7 +183,7 @@ function drawBarChart() {
}
// draw a fake 0 height bar if chart_index is outside the bounds of the array
if ((chart_index + bar - 1) >= 0 && (chart_index + bar - 1) < data_len)
if ((chart_index + bar - 1) >= 0 && (chart_index + bar - 1) < data_len && chart_max_datum > 0)
bar_top = bar_bot - 100 * (chart_data[chart_index + bar - 1]) / chart_max_datum;
else
bar_top = bar_bot;

View File

@ -1,7 +1,7 @@
{
"id": "health",
"name": "Health Tracking",
"version": "0.14",
"version": "0.15",
"description": "Logs health data and provides an app to view it",
"icon": "app.png",
"tags": "tool,system,health",

4
apps/homework/README.md Normal file
View File

@ -0,0 +1,4 @@
# This is a simple homework app
Use the touchscreen to navigate.
Requires the "textinput" library. (Tap keyboard)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwwhC/AH4AbhvQCyvd7oYTCwQYTCwgYRCwwYPIgpKQCA4YOBxIYMBhYLLHhgYEC5BsKDAYXHCwUBiUikAYIC4wtDC5IYCA4pEEC5QYBYRUCkQXJAA8K1Wq0AXHhGIxGAC5ZHHC8ZDDC4cM5qaBC8ZHHC68N6czmAXrL94X/C/4XHgUiCYIDDa54XXO/4XHAH4A/ABY="))

212
apps/homework/app.js Normal file
View File

@ -0,0 +1,212 @@
var Layout = require("Layout");
var homework = require("Storage").readJSON("homework.txt", "r");
var mainCheckHomeworkMenu;
var nhwmn = { // New homework Menu
"": {
"title": "New HW Subject:"
}
};
function newHomeworkMenu() {
E.showMessage("Getting subjects...");
var rawsubjects = require("Storage").read("subjects.txt"); // This code reads out the subjects list and removes the newline character at the end
var splitsubjects = rawsubjects.split(",");
var lastItem = splitsubjects[splitsubjects.length - 1];
var thiscurrentsubject;
var command;
lastItem = lastItem.slice(0, -1);
splitsubjects[splitsubjects.length - 1] = lastItem;
for (let i = 0; i < splitsubjects.length; i++) { // loop through array and add to menu
thiscurrentsubject = splitsubjects[i];
command = addNewHomework(thiscurrentsubject);
nhwmn[splitsubjects[i]] = addNewHomework.bind(null, thiscurrentsubject);
}
nhwmn["Back"] = function() {E.showMenu(mainMenu);};
console.log(nhwmn);
E.showMenu(nhwmn);
}
var mode = "mainmenu";
var statusmsg;
var mainMenu = {
"": {
title: "--Main Menu--"
},
"New Homework": function() {
newHomeworkMenu();
mode = "newhomework";
},
"Check Homework": function() {
checkUnfinishedHomeworkAssembler();
},
"Reset Homework": function() {
E.showPrompt("Are you sure you want to delete homework.txt?", {
buttons: {
"No": false,
"Yes": true
}
}).then(function(v) {
if (v) {
require("Storage").write("homework.txt", '{"homework":[]}');
homework = require("Storage").readJSON("homework.txt", "r");
E.showMenu(mainMenu);
}else{
E.showMenu(mainMenu);
}
});
},
};
function checkUnfinishedHomeworkAssembler() {
homework = require("Storage").readJSON("homework.txt", "r");
var hwcount = Object.keys(homework.homework).length;
mainCheckHomeworkMenu = {
'': {
'title': 'Unfinished HW:'
}
};
// This code snippet gets the unfinished HW and puts it in mainCheckHomeworkMenu
// btw mainCheckHomeworkMenu displays all the homework, when tapping on it you get more details with checkPreciseHomework function
for (var i = 0; i < hwcount; ++i) {
if (homework.homework[i].done === false) {
var currentsubject = i; //attempting to pass i
mainCheckHomeworkMenu[homework.homework[i].subject] = checkPreciseHomework.bind(null, currentsubject);
}
}
mainCheckHomeworkMenu["See Archived HW"] = function() {
checkFinishedHomeworkAssembler();
};
mainCheckHomeworkMenu["Back to Main Menu"] = function() {
mode = "mainmenu";
E.showMenu(mainMenu);
};
console.log(mainCheckHomeworkMenu);
E.showMenu(mainCheckHomeworkMenu);
}
function checkFinishedHomeworkAssembler() {
homework = require("Storage").readJSON("homework.txt", "r");
var hwcount = Object.keys(homework.homework).length;
mainCheckHomeworkMenu = {
'': {
'title': 'Archived HW:'
}
};
// This code snippet gets the unfinished HW and puts it in mainCheckHomeworkMenu
// btw mainCheckHomeworkMenu displays all the homework, when tapping on it you get more details with checkPreciseHomework function (currently being written)
for (var i = 0; i < hwcount; ++i) {
if (homework.homework[i].done === true) {
var currentsubject = i; //attempting to pass i
mainCheckHomeworkMenu[homework.homework[i].subject] = checkPreciseHomework.bind(null, currentsubject);
}
}
mainCheckHomeworkMenu["Back"] = function() {
mode = "mainmenu";
E.showMenu(mainMenu);
};
E.showMenu(mainCheckHomeworkMenu);
}
function checkPreciseHomework(subjectnum) { // P A I N
homework = require("Storage").read("homework.txt", "r");
homework = JSON.parse(homework);
var subject = homework.homework[subjectnum].subject;
var task = homework.homework[subjectnum].task;
var taskmsg = "Task: " + homework.homework[subjectnum].task;
if (homework.homework[subjectnum].done === false) {
statusmsg = "Status: Unfinished";
} else {
statusmsg = "Status: Finished";
}
var datetimerecieved = homework.homework[subjectnum].datetimerecievehw;
var datetimerecievedmsg = "Recieved: " + homework.homework[subjectnum].datetimerecievehw;
var checkPreciseHomeworkMenu = {
'': {
'title': subject
},
};
checkPreciseHomeworkMenu[subject] = function() {},
checkPreciseHomeworkMenu[taskmsg] = function() {},
checkPreciseHomeworkMenu[statusmsg] = function() {
status = "Status: Finished";
var d = new Date();
var currenttime = require("locale").time(d, 1);
var currentdate = require("locale").date(d);
var datetime = (currenttime + " " + currentdate);
delete homework.homework[subjectnum];
homework.homework.push({
subject: subject,
task: task,
done: true,
datetimerecievehw: datetimerecieved,
datetimehwdone: datetime
});
require("Storage").write("homework.txt", JSON.stringify(homework));
checkUnfinishedHomeworkAssembler();
},
checkPreciseHomeworkMenu[datetimerecievedmsg] = function() {},
checkPreciseHomeworkMenu["Back"] = function() {
checkUnfinishedHomeworkAssembler();
},
E.showMenu(checkPreciseHomeworkMenu);
}
function pushHomework(subject, status, datetimehwdone) {
homework = require("Storage").readJSON("homework.txt", "r");
}
function addNewHomework(subject) { // Pass subject
console.log(subject);
require("textinput").input().then(result => {
if (result === "") {
mode = "newhomework";
newHomeworkMenu();
} else {
var d = new Date();
// update time and date
var currenttime = require("locale").time(d, 1);
var currentdate = require("locale").date(d);
var datetime = (currenttime + " " + currentdate);
homework.homework.push({
subject: subject,
task: result,
done: false,
datetimerecievehw: datetime
}); // TODO: when HW is done, add datetimeendhw !!!
console.log("subject is" + subject);
//homework.homework[subject] = result;
require("Storage").write("homework.txt", JSON.stringify(homework));
E.showMenu(mainMenu);
}
});
}
function main() { // why does this still exist
if (mode === "mainmenu") {
E.showMenu(mainMenu);
} else if (mode === "newhomework") {
newHomeworkMenu()
}
}
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
main();
//loop = setInterval(main, 1);

BIN
apps/homework/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

View File

@ -0,0 +1,16 @@
{ "id": "homework",
"name": "Homework",
"shortName":"Homework",
"version":"0.1",
"description": "A simple app to manage homework",
"icon": "app.png",
"tags": "tool",
"supports" : ["BANGLEJS2"],
"readme": "README.md",
"custom": "subjects.html",
"storage": [
{"name":"homework.app.js","url":"app.js"},
{"name":"homework.img","url":"app-icon.js","evaluate":true}
]
}

View File

@ -0,0 +1,30 @@
<html>
<head>
<link rel="stylesheet" href="../../css/spectre.min.css">
</head>
<body>
<p>Subjects: <input type="text" id="subjects" class="form-input" value="Seperate subjects by comma, eg. German,Maths,Geography,..."></p>
<p>Click <button id="upload" class="btn btn-primary">Upload</button></p>
<script src="../../core/lib/customize.js"></script>
<script>
// When the 'upload' button is clicked...
document.getElementById("upload").addEventListener("click", function() {
// get the text to add
var text = document.getElementById("subjects").value;
console.log(text);
// build the app's text using a templated String
var app = text;
// send finished app (in addition to contents of app.json)
sendCustomizedApp({
storage:[
{name:"subjects.txt"},
]
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,2 @@
0.01: Initial release
0.02: implemented "direct launch" and "one click exit" settings

12
apps/iconlaunch/README.md Normal file
View File

@ -0,0 +1,12 @@
# Icon launcher
A launcher inspired by smartphones, with an icon-only scrollable menu.
This launcher shows 9 apps per screen, making it much faster to navigate versus the default launcher.
![A screenshot](screenshot1.png)
![Another screenshot](screenshot2.png)
## Technical note
The app uses `E.showScroller`'s code in the app but not the function itself because `E.showScroller` doesn't report the position of a press to the select function.

209
apps/iconlaunch/app.js Normal file
View File

@ -0,0 +1,209 @@
const s = require("Storage");
const settings = s.readJSON("launch.json", true) || { showClocks: true, fullscreen: false,direct:false,oneClickExit:false };
if( settings.oneClickExit)
setWatch(_=> load(), BTN1);
if (!settings.fullscreen) {
Bangle.loadWidgets();
Bangle.drawWidgets();
}
var apps = s
.list(/\.info$/)
.map((app) => {
var a = s.readJSON(app, 1);
return (
a && {
name: a.name,
type: a.type,
icon: a.icon,
sortorder: a.sortorder,
src: a.src,
}
);
})
.filter(
(app) =>
app &&
(app.type == "app" ||
(app.type == "clock" && settings.showClocks) ||
!app.type)
);
apps.sort((a, b) => {
var n = (0 | a.sortorder) - (0 | b.sortorder);
if (n) return n; // do sortorder first
if (a.name < b.name) return -1;
if (a.name > b.name) return 1;
return 0;
});
apps.forEach((app) => {
if (app.icon) app.icon = s.read(app.icon); // should just be a link to a memory area
});
let scroll = 0;
let selectedItem = -1;
const R = Bangle.appRect;
const iconSize = 48;
const appsN = Math.floor(R.w / iconSize);
const whitespace = (R.w - appsN * iconSize) / (appsN + 1);
const itemSize = iconSize + whitespace;
function drawItem(itemI, r) {
g.clearRect(r.x, r.y, r.x + r.w - 1, r.y + r.h - 1);
let x = 0;
for (let i = itemI * appsN; i < appsN * (itemI + 1); i++) {
if (!apps[i]) break;
x += whitespace;
if (!apps[i].icon) {
g.setFontAlign(0,0,0).setFont("12x20:2").drawString("?", x + r.x+iconSize/2, r.y + iconSize/2);
} else {
g.drawImage(apps[i].icon, x + r.x, r.y);
}
if (selectedItem == i) {
g.drawRect(
x + r.x - 1,
r.y - 1,
x + r.x + iconSize + 1,
r.y + iconSize + 1
);
}
x += iconSize;
}
drawText(itemI);
}
function drawItemAuto(i) {
var y = idxToY(i);
g.reset().setClipRect(R.x, y, R.x2, y + itemSize);
drawItem(i, {
x: R.x,
y: y,
w: R.w,
h: itemSize
});
g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
}
let lastIsDown = false;
function drawText(i) {
const selectedApp = apps[selectedItem];
const idy = (selectedItem - (selectedItem % 3)) / 3;
if (!selectedApp || i != idy) return;
const appY = idxToY(idy) + iconSize / 2;
g.setFontAlign(0, 0, 0);
g.setFont("12x20");
const rect = g.stringMetrics(selectedApp.name);
g.clearRect(
R.w / 2 - rect.width / 2,
appY - rect.height / 2,
R.w / 2 + rect.width / 2,
appY + rect.height / 2
);
g.drawString(selectedApp.name, R.w / 2, appY);
}
function selectItem(id, e) {
const iconN = E.clip(Math.floor((e.x - R.x) / itemSize), 0, appsN - 1);
const appId = id * appsN + iconN;
if( settings.direct && apps[appId])
{
load(apps[appId].src);
return;
}
if (appId == selectedItem && apps[appId]) {
const app = apps[appId];
if (!app.src || s.read(app.src) === undefined) {
E.showMessage( /*LANG*/ "App Source\nNot found");
} else {
load(app.src);
}
}
selectedItem = appId;
drawItems();
}
function idxToY(i) {
return i * itemSize + R.y - (scroll & ~1);
}
function YtoIdx(y) {
return Math.floor((y + (scroll & ~1) - R.y) / itemSize);
}
function drawItems() {
g.reset().clearRect(R.x, R.y, R.x2, R.y2);
g.setClipRect(R.x, R.y, R.x2, R.y2);
var a = YtoIdx(R.y);
var b = Math.min(YtoIdx(R.y2), 99);
for (var i = a; i <= b; i++)
drawItem(i, {
x: R.x,
y: idxToY(i),
w: R.w,
h: itemSize,
});
g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
}
drawItems();
g.flip();
const itemsN = Math.ceil(apps.length / appsN);
Bangle.setUI({
mode: "custom",
drag: (e) => {
let dy = e.dy;
if (scroll + R.h - dy > itemsN * itemSize) {
dy = scroll + R.h - itemsN * itemSize;
}
if (scroll - dy < 0) {
dy = scroll;
}
scroll -= dy;
scroll = E.clip(scroll, 0, itemSize * (itemsN - 1));
g.setClipRect(R.x, R.y, R.x2, R.y2);
g.scroll(0, dy);
if (dy < 0) {
g.setClipRect(R.x, R.y2 - (1 - dy), R.x2, R.y2);
let i = YtoIdx(R.y2 - (1 - dy));
let y = idxToY(i);
while (y < R.y2) {
drawItem(i, {
x: R.x,
y: y,
w: R.w,
h: itemSize,
});
i++;
y += itemSize;
}
} else {
// d>0
g.setClipRect(R.x, R.y, R.x2, R.y + dy);
let i = YtoIdx(R.y + dy);
let y = idxToY(i);
while (y > R.y - itemSize) {
drawItem(i, {
x: R.x,
y: y,
w: R.w,
h: itemSize,
});
y -= itemSize;
i--;
}
}
g.setClipRect(0, 0, g.getWidth() - 1, g.getHeight() - 1);
},
touch: (_, e) => {
if (e.y < R.y - 4) return;
var i = YtoIdx(e.y);
selectItem(i, e);
},
});

BIN
apps/iconlaunch/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
{
"id": "iconlaunch",
"name": "Icon Launcher",
"shortName" : "Icon launcher",
"version": "0.02",
"icon": "app.png",
"description": "A launcher inspired by smartphones, with an icon-only scrollable menu.",
"tags": "tool,system,launcher",
"type": "launch",
"supports": ["BANGLEJS2"],
"storage": [
{ "name": "iconlaunch.app.js", "url": "app.js" },
{ "name": "iconlaunch.settings.js", "url": "settings.js" }
],
"screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }],
"readme": "README.md",
"sortorder": -10
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

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