Merge branch master into lcars-flash-warning
|
@ -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
|
||||
|
|
|
@ -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> <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>
|
|
@ -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
|
||||
|
|
|
@ -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` → Configure a new alarm
|
||||
- `Repeat` → Select when the alarm will fire. You can select a predefined option (_Once_, _Every Day_, _Workdays_ or _Weekends_ or you can configure the days freely)
|
||||
- `New Timer` → Configure a new timer
|
||||
- `Advanced`
|
||||
- `Scheduler settings` → Open the [Scheduler](https://github.com/espruino/BangleApps/tree/master/apps/sched) settings page, see its [README](https://github.com/espruino/BangleApps/blob/master/apps/sched/README.md) for details
|
||||
- `Enable All` → Enable _all_ disabled alarms & timers
|
||||
- `Disable All` → Disable _all_ enabled alarms & timers
|
||||
- `Delete All` → 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).
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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" }
|
||||
]
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 1.7 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.2 KiB |
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"}],
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"}]
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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());
|
||||
})
|
||||
});
|
||||
|
|
|
@ -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.
|
|
@ -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>
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "bwclk",
|
||||
"name": "BW Clock",
|
||||
"version": "0.06",
|
||||
"version": "0.09",
|
||||
"description": "BW Clock.",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
|
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.1 KiB |
|
@ -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();
|
||||
},
|
||||
}
|
||||
});
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -21,5 +21,6 @@
|
|||
"circle2colorizeIcon": true,
|
||||
"circle3colorizeIcon": true,
|
||||
"circle4colorizeIcon": false,
|
||||
"hrmValidity": 60
|
||||
"hrmValidity": 60,
|
||||
"updateInterval": 60
|
||||
}
|
||||
|
|
|
@ -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"}],
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: App created
|
|
@ -0,0 +1 @@
|
|||
E.toArrayBuffer(atob("ICABAAAAAAAAAAAAAAAAAAHAAAAP8AAAfn4AA/APwA+DwfAPg8HwD+AH8Az4HzAMPnwwDAfgMAwBgDAMCYAwDA2YMAwhmDAMIZAwDCGDMA2BgzAMgYAwDAGAMA8BgPADwYPAAPGPgAB9ngAAH/gAAAfgAAABgAAAAAAAAAAAAAAAAAA="))
|
|
@ -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);
|
After Width: | Height: | Size: 637 B |
After Width: | Height: | Size: 2.4 KiB |
|
@ -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}
|
||||
]
|
||||
}
|
|
@ -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.
|
||||
|
||||

|
||||
|
|
@ -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();
|
||||
|
After Width: | Height: | Size: 8.4 KiB |
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwgJC/AAVh/E/hgFC/O/AoMB8EZwc8AoUYgYFBgFgjAXDAowXBAo8B/ARBn4FGAAsBmAFE2ADBhwFEj4VEn+AgPvAontgfwv+ABIMCMwIVCgf4FIWAAoN3sAFCwERoEB0MHwF3gEF0MPwFEAoW/4ALD/4tCg/hAoYhB/5ZDwF+Aok0gEIkEf/4AB8eMBoM2bkw="))
|
|
@ -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}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 2.4 KiB |
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -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
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
|
@ -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"))
|
|
@ -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");
|
||||
});
|
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 722 B |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 3.2 KiB |
|
@ -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}
|
||||
]
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
||||

|
||||
|
||||
## 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`)
|
||||
|
||||

|
||||
|
||||
|
|
|
@ -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");
|
|
@ -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"}],
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,9 +1,6 @@
|
|||
# Nifty Series B Clock
|
||||
|
||||
- Display Time and Date
|
||||
- Color Configuration
|
||||
|
||||
##
|
||||
- Colour Configuration
|
||||
|
||||

|
||||
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
# This is a simple homework app
|
||||
Use the touchscreen to navigate.
|
||||
|
||||
Requires the "textinput" library. (Tap keyboard)
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwhC/AH4AbhvQCyvd7oYTCwQYTCwgYRCwwYPIgpKQCA4YOBxIYMBhYLLHhgYEC5BsKDAYXHCwUBiUikAYIC4wtDC5IYCA4pEEC5QYBYRUCkQXJAA8K1Wq0AXHhGIxGAC5ZHHC8ZDDC4cM5qaBC8ZHHC68N6czmAXrL94X/C/4XHgUiCYIDDa54XXO/4XHAH4A/ABY="))
|
|
@ -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);
|
After Width: | Height: | Size: 684 B |
|
@ -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}
|
||||
]
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,2 @@
|
|||
0.01: Initial release
|
||||
0.02: implemented "direct launch" and "one click exit" settings
|
|
@ -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.
|
||||
|
||||

|
||||

|
||||
|
||||
## 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.
|
|
@ -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);
|
||||
},
|
||||
});
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -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
|
||||
}
|
After Width: | Height: | Size: 4.6 KiB |