Merge remote-tracking branch 'upstream/master'
|
@ -0,0 +1,12 @@
|
|||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: espruino
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ['http://www.espruino.com/Donate']# Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
@ -1,4 +1,4 @@
|
|||
name: Node CI
|
||||
name: build
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
|
@ -6,29 +6,22 @@ jobs:
|
|||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [16.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository and submodules
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: install testing dependencies
|
||||
run: npm i
|
||||
- name: test all apps and widgets
|
||||
run: npm run test
|
||||
- name: install typescript dependencies
|
||||
node-version: 16.x
|
||||
- name: Install testing dependencies
|
||||
run: npm ci
|
||||
- name: Test all apps and widgets
|
||||
run: npm test
|
||||
- name: Install typescript dependencies
|
||||
working-directory: ./typescript
|
||||
run: npm ci
|
||||
- name: build types
|
||||
working-directory: ./typescript
|
||||
run: npm run build:types
|
||||
- name: build all TS apps and widgets
|
||||
- name: Build all TS apps and widgets
|
||||
working-directory: ./typescript
|
||||
run: npm run build
|
|
@ -1,6 +1,5 @@
|
|||
.htaccess
|
||||
node_modules
|
||||
package-lock.json
|
||||
.DS_Store
|
||||
*.js.bak
|
||||
appdates.csv
|
||||
|
@ -11,3 +10,7 @@ tests/Layout/bin/tmp.*
|
|||
tests/Layout/testresult.bmp
|
||||
apps.local.json
|
||||
_site
|
||||
.jekyll-cache
|
||||
.owncloudsync.log
|
||||
Desktop.ini
|
||||
.sync_*.db*
|
||||
|
|
27
README.md
|
@ -1,7 +1,7 @@
|
|||
Bangle.js App Loader (and Apps)
|
||||
================================
|
||||
|
||||
[](https://app.travis-ci.com/github/espruino/BangleApps)
|
||||
[](https://github.com/espruino/BangleApps/actions/workflows/nodejs.yml)
|
||||
|
||||
* Try the **release version** at [banglejs.com/apps](https://banglejs.com/apps)
|
||||
* Try the **development version** at [espruino.github.io](https://espruino.github.io/BangleApps/)
|
||||
|
@ -191,7 +191,7 @@ widget bar at the top of the screen they can add themselves to the global
|
|||
|
||||
```
|
||||
WIDGETS["mywidget"]={
|
||||
area:"tl", // tl (top left), tr (top right)
|
||||
area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right)
|
||||
sortorder:0, // (Optional) determines order of widgets in the same corner
|
||||
width: 24, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout
|
||||
draw:draw // called to draw the widget
|
||||
|
@ -226,10 +226,8 @@ and which gives information about the app for the Launcher.
|
|||
"name":"Short Name", // for Bangle.js menu
|
||||
"icon":"*myappid", // for Bangle.js menu
|
||||
"src":"-myappid", // source file
|
||||
"type":"widget/clock/app/bootloader", // optional, default "app"
|
||||
// if this is 'widget' then it's not displayed in the menu
|
||||
// if it's 'clock' then it'll be loaded by default at boot time
|
||||
// if this is 'bootloader' then it's code that is run at boot time, but is not in a menu
|
||||
"type":"widget/clock/app/bootloader/...", // optional, default "app"
|
||||
// see 'type' in 'metadata.json format' below for more options/info
|
||||
"version":"1.23",
|
||||
// added by BangleApps loader on upload based on metadata.json
|
||||
"files:"file1,file2,file3",
|
||||
|
@ -252,17 +250,24 @@ and which gives information about the app for the Launcher.
|
|||
"version": "0v01", // the version of this app
|
||||
"description": "...", // long description (can contain markdown)
|
||||
"icon": "icon.png", // icon in apps/
|
||||
"screenshots" : [ { url:"screenshot.png" } ], // optional screenshot for app
|
||||
"screenshots" : [ { "url":"screenshot.png" } ], // optional screenshot for app
|
||||
"type":"...", // optional(if app) -
|
||||
// 'app' - an application
|
||||
// 'clock' - a clock - required for clocks to automatically start
|
||||
// 'widget' - a widget
|
||||
// 'launch' - replacement launcher app
|
||||
// 'bootloader' - code that runs at startup only
|
||||
// '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
|
||||
// 'scheduler' - provides 'sched' library and boot code for scheduling alarms/timers
|
||||
// (currently only 'sched' app)
|
||||
// 'notify' - provides 'notify' library for showing notifications
|
||||
// 'locale' - provides 'locale' library for language-specific date/distance/etc
|
||||
// (a version of 'locale' is included in the firmware)
|
||||
"tags": "", // comma separated tag list for searching
|
||||
"supports": ["BANGLEJS2"], // List of device IDs supported, either BANGLEJS or BANGLEJS2
|
||||
"dependencies" : { "notify":"type" } // optional, app 'types' we depend on
|
||||
"dependencies" : { "notify":"type" } // optional, app 'types' we depend on (see "type" above)
|
||||
"dependencies" : { "messages":"app" } // optional, depend on a specific app ID
|
||||
// for instance this will use notify/notifyfs is they exist, or will pull in 'notify'
|
||||
"readme": "README.md", // if supplied, a link to a markdown-style text file
|
||||
|
@ -319,7 +324,7 @@ and which gives information about the app for the Launcher.
|
|||
```
|
||||
|
||||
* name, icon and description present the app in the app loader.
|
||||
* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher` or empty.
|
||||
* tags is used for grouping apps in the library, separate multiple entries by comma. Known tags are `tool`, `system`, `clock`, `game`, `sound`, `gps`, `widget`, `launcher`, `bluetooth` or empty.
|
||||
* storage is used to identify the app files and how to handle them
|
||||
* data is used to clean up files when the app is uninstalled
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
theme: jekyll-theme-minimal
|
||||
theme: jekyll-theme-slate
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1,11 @@
|
|||
# two of them clock
|
||||
|
||||
You can now wear teh memez on your wrist.
|
||||
|
||||

|
||||
|
||||
Also serves as an example of displaying seconds only when unlocked or charging and only refreshing on the minute otherwise.
|
||||
Widgets not supported
|
||||
|
||||
## Creator
|
||||
- [Kilrah](https://github.com/kilrah)
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwgZC/AH4ADkAPOgVJkgEBAQQAJiQRByEJgmQCJWSpMEAQMkyQJCpASHhAOBpAmBJJgjBCIUJCRg4CCIJxFMQ2SoARCkmACI0EBAJHCCIMLj4RFiUBskAgIXBEAU5A4P34CtCiEJsEJ/AHBCgOBAoQAEi0H////HciQsBwywICIXWzkG4A+BEY0gif46dt6/cgnIgkWnHfLIP/MoUWwHbpvC/kAjEEj0HNYQCCkEfGgP/64RB2EAifHLwMAjg1CCIMD/0H/0B8EAh+HgeAkARCE4IjC/4jBYIMPLIcIAYUPB4OBCIQABhu/AoShCHYIRBx6QBDgUw2//8OHPwcJ39//ILBCIU9LgMBSQgsBJAYRBkE/CIIABgRHD3wRFkk/2zBDAYU//3b/oRB8ARBj6ABgEE7YREEYf+oMkSwINCyClCn//z//+4RBgMkgU3EgUcwFJgEeboOXCIP2EYJCDAAVJkkGWoIuBgf2EYQPDkECCIOGd4ffyEJkgFBAAcSoEkwQCBhw+BwQaByVAkGAKwIFBBANLkEQgAyBCIVIkBpBgmSBYOQoApBgcgiQRCAQIyCCgsSjIFBCIcgRgJNCCgQyBpAgDAQT2BCgIOBBAQUCCIpfBCIwCKP4QRNpCSDCLyJBCIbjBTwYRLboJ0BCI4QD"))
|
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 5.8 KiB |
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"id": "2ofthemclk",
|
||||
"name": "two of them clock",
|
||||
"version": "0.01",
|
||||
"description": "You can now wear teh memez on your wrist.",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"2ofthemclk.app.js","url":"app.js"},
|
||||
{"name":"2ofthemclk.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 7.1 KiB |
|
@ -269,7 +269,7 @@ function actions(v){
|
|||
}
|
||||
|
||||
// Get Messages status
|
||||
var messages = require("Storage").readJSON("messages.json",1)||[];
|
||||
var messages_installed = require("Storage").read("messages") !== undefined;
|
||||
|
||||
//var BTconnected = NRF.getSecurityStatus().connected;
|
||||
//NRF.on('connect',BTconnected = NRF.getSecurityStatus().connected);
|
||||
|
@ -318,7 +318,7 @@ function drawWidgeds() {
|
|||
var x2M = x1M + 25;
|
||||
var y2M = y2B;
|
||||
|
||||
if (messages.some(m=>m.new)) {
|
||||
if (messages_installed && require("messages").status() == "new") {
|
||||
g.setColor(g.theme.fg);
|
||||
g.fillRect(x1M,y1M,x2M,y2M);
|
||||
g.setColor(g.theme.bg);
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: Initial version for upload
|
||||
0.02: better theme support, configurable colors, small improvements
|
||||
0.02: Better theme support, configurable colors, small improvements
|
||||
0.03: Use `messages` library to check for new messages
|
|
@ -1,7 +1,7 @@
|
|||
{ "id": "7x7dotsclock",
|
||||
"name": "7x7 Dots Clock",
|
||||
"shortName":"7x7 Dots Clock",
|
||||
"version":"0.02",
|
||||
"version":"0.03",
|
||||
"description": "A clock with a big 7x7 dots Font",
|
||||
"icon": "dotsfontclock.png",
|
||||
"tags": "clock",
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
0.01: New App!
|
||||
0.02: Fullscreen settings.
|
||||
0.03: Tell clock widgets to hide.
|
||||
|
|
|
@ -115,6 +115,9 @@ function draw() {
|
|||
}
|
||||
}
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
Bangle.setUI("clock");
|
||||
|
||||
Bangle.loadWidgets();
|
||||
|
||||
// Clear the screen once, at startup
|
||||
|
@ -140,5 +143,3 @@ Bangle.on('lock', function(isLocked) {
|
|||
});
|
||||
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
Bangle.setUI("clock");
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "90sclk",
|
||||
"name": "90s Clock",
|
||||
"version": "0.02",
|
||||
"version": "0.03",
|
||||
"description": "A 90s style watch-face",
|
||||
"readme": "README.md",
|
||||
"icon": "app.png",
|
||||
|
|
|
@ -9,7 +9,7 @@ currently-running apps */
|
|||
|
||||
// add your widget
|
||||
WIDGETS["mywidget"]={
|
||||
area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right)
|
||||
area:"tl", // tl (top left), tr (top right), bl (bottom left), br (bottom right), be aware that not all apps support widgets at the bottom of the screen
|
||||
width: 28, // how wide is the widget? You can change this and call Bangle.drawWidgets() to re-layout
|
||||
draw:draw // called to draw the widget
|
||||
};
|
||||
|
|
|
@ -1,2 +1,9 @@
|
|||
0.01: New App!
|
||||
0.02: Fix the settings bug and some tweaking
|
||||
0.03: Do not alarm while charging
|
||||
0.04: Obey system quiet mode
|
||||
0.05: Battery optimisation, add the pause option, bug fixes
|
||||
0.06: Add a temperature threshold to detect (and not alert) if the BJS isn't worn. Better support for the peoples using the app at night
|
||||
0.07: Fix bug on the cutting edge firmware
|
||||
0.08: Use default Bangle formatter for booleans
|
||||
0.09: New app screen (instead of showing settings or the alert) and some optimisations
|
|
@ -1,13 +1,15 @@
|
|||
# Activity reminder
|
||||
|
||||
A reminder to take short walks for the ones with a sedentary lifestyle.
|
||||
The alert will popup only if you didn't take your short walk yet
|
||||
The alert will popup only if you didn't take your short walk yet.
|
||||
|
||||
Different settings can be personalized:
|
||||
- Enable : Enable/Disable the app
|
||||
- Start hour: Hour to start the reminder
|
||||
- End hour: Hour to end the reminder
|
||||
- Max inactivity: Maximum inactivity time to allow before the alert. From 15 to 60 min
|
||||
- Dismiss delay: Delay added before the next alert if the alert is dismissed. From 5 to 15 min
|
||||
- Max inactivity: Maximum inactivity time to allow before the alert. From 15 to 120 min
|
||||
- Dismiss delay: Delay added before the next alert if the alert is dismissed. From 5 to 60 min
|
||||
- Pause delay: Same as Dismiss delay but longer (usefull for meetings and such). From 30 to 240 min
|
||||
- Min steps: Minimal amount of steps to count as an activity
|
||||
- Temp Threshold: Temperature threshold to determine if the watch is worn
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
(function () {
|
||||
// load variable before defining functions cause it can trigger a ReferenceError
|
||||
const activityreminder = require("activityreminder");
|
||||
const storage = require("Storage");
|
||||
let activityreminder_data = activityreminder.loadData();
|
||||
|
||||
function run() {
|
||||
E.showPrompt("Inactivity detected", {
|
||||
title: "Activity reminder",
|
||||
buttons: { "Ok": 1, "Dismiss": 2, "Pause": 3 }
|
||||
}).then(function (v) {
|
||||
if (v == 1) {
|
||||
activityreminder_data.okDate = new Date();
|
||||
}
|
||||
if (v == 2) {
|
||||
activityreminder_data.dismissDate = new Date();
|
||||
}
|
||||
if (v == 3) {
|
||||
activityreminder_data.pauseDate = new Date();
|
||||
}
|
||||
activityreminder.saveData(activityreminder_data);
|
||||
load();
|
||||
});
|
||||
|
||||
// Obey system quiet mode:
|
||||
if (!(storage.readJSON('setting.json', 1) || {}).quiet) {
|
||||
Bangle.buzz(400);
|
||||
}
|
||||
setTimeout(load, 20000);
|
||||
}
|
||||
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
run();
|
||||
|
||||
})();
|
|
@ -1,37 +1,54 @@
|
|||
function drawAlert(){
|
||||
E.showPrompt("Inactivity detected",{
|
||||
title:"Activity reminder",
|
||||
buttons : {"Ok": true,"Dismiss": false}
|
||||
}).then(function(v) {
|
||||
if(v == true){
|
||||
stepsArray = stepsArray.slice(0, activityreminder.maxInnactivityMin - 3);
|
||||
require("activityreminder").saveStepsArray(stepsArray);
|
||||
}
|
||||
if(v == false){
|
||||
stepsArray = stepsArray.slice(0, activityreminder.maxInnactivityMin - activityreminder.dismissDelayMin);
|
||||
require("activityreminder").saveStepsArray(stepsArray);
|
||||
}
|
||||
load();
|
||||
});
|
||||
(function () {
|
||||
// load variable before defining functions cause it can trigger a ReferenceError
|
||||
const activityreminder = require("activityreminder");
|
||||
let activityreminder_data = activityreminder.loadData();
|
||||
let W = g.getWidth();
|
||||
// let H = g.getHeight();
|
||||
|
||||
Bangle.buzz(400);
|
||||
setTimeout(load, 20000);
|
||||
}
|
||||
function getHoursMins(date){
|
||||
var h = date.getHours();
|
||||
var m = date.getMinutes();
|
||||
return (""+h).substr(-2) + ":" + ("0"+m).substr(-2);
|
||||
}
|
||||
|
||||
function run(){
|
||||
if(stepsArray.length == activityreminder.maxInnactivityMin){
|
||||
if (stepsArray[0] - stepsArray[stepsArray.length-1] < activityreminder.minSteps){
|
||||
drawAlert();
|
||||
}
|
||||
}else{
|
||||
eval(require("Storage").read("activityreminder.settings.js"))(()=>load());
|
||||
}
|
||||
}
|
||||
function drawData(name, value, y){
|
||||
g.drawString(name, 10, y);
|
||||
g.drawString(value, 100, y);
|
||||
}
|
||||
|
||||
function drawInfo() {
|
||||
var h=18, y = h;
|
||||
g.setColor(g.theme.fg);
|
||||
g.setFont("Vector",h).setFontAlign(-1,-1);
|
||||
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
activityreminder = require("activityreminder").loadSettings();
|
||||
stepsArray = require("activityreminder").loadStepsArray();
|
||||
run();
|
||||
// Header
|
||||
g.drawLine(0,25,W,25);
|
||||
g.drawLine(0,26,W,26);
|
||||
|
||||
g.drawString("Current Cycle", 10, y+=h);
|
||||
drawData("Start", getHoursMins(activityreminder_data.stepsDate), y+=h);
|
||||
drawData("Steps", getCurrentSteps(), y+=h);
|
||||
|
||||
/*
|
||||
g.drawString("Button Press", 10, y+=h*2);
|
||||
drawData("Ok", getHoursMins(activityreminder_data.okDate), y+=h);
|
||||
drawData("Dismiss", getHoursMins(activityreminder_data.dismissDate), y+=h);
|
||||
drawData("Pause", getHoursMins(activityreminder_data.pauseDate), y+=h);
|
||||
*/
|
||||
}
|
||||
|
||||
function getCurrentSteps(){
|
||||
let health = Bangle.getHealthStatus("day");
|
||||
return health.steps - activityreminder_data.stepsOnDate;
|
||||
}
|
||||
|
||||
function run() {
|
||||
g.clear();
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
drawInfo();
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
})();
|
|
@ -1,29 +1,81 @@
|
|||
function run(){
|
||||
var now = new Date();
|
||||
var h = now.getHours();
|
||||
if(h >= activityreminder.startHour && h < activityreminder.endHour){
|
||||
var health = Bangle.getHealthStatus("day");
|
||||
stepsArray.unshift(health.steps);
|
||||
stepsArray = stepsArray.slice(0, activityreminder.maxInnactivityMin);
|
||||
require("activityreminder").saveStepsArray(stepsArray);
|
||||
(function () {
|
||||
// load variable before defining functions cause it can trigger a ReferenceError
|
||||
const activityreminder = require("activityreminder");
|
||||
const activityreminder_settings = activityreminder.loadSettings();
|
||||
let activityreminder_data = activityreminder.loadData();
|
||||
|
||||
if (activityreminder_data.firstLoad) {
|
||||
activityreminder_data.firstLoad = false;
|
||||
activityreminder.saveData(activityreminder_data);
|
||||
}
|
||||
|
||||
function run() {
|
||||
if (isNotWorn()) return;
|
||||
let now = new Date();
|
||||
let h = now.getHours();
|
||||
|
||||
if (isDuringAlertHours(h)) {
|
||||
let health = Bangle.getHealthStatus("day");
|
||||
if (health.steps - activityreminder_data.stepsOnDate >= activityreminder_settings.minSteps // more steps made than needed
|
||||
|| health.steps < activityreminder_data.stepsOnDate) { // new day or reboot of the watch
|
||||
activityreminder_data.stepsOnDate = health.steps;
|
||||
activityreminder_data.stepsDate = now;
|
||||
activityreminder.saveData(activityreminder_data);
|
||||
/* todo in a futur release
|
||||
Add settimer to trigger like 30 secs after going in this part cause the person have been walking
|
||||
(pass some argument to run() to handle long walks and not triggering so often)
|
||||
*/
|
||||
}
|
||||
|
||||
if (mustAlert(now)) {
|
||||
load('activityreminder.alert.js');
|
||||
}
|
||||
}
|
||||
else{
|
||||
if(stepsArray != []){
|
||||
stepsArray = [];
|
||||
require("activityreminder").saveStepsArray(stepsArray);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function isNotWorn() {
|
||||
return (Bangle.isCharging() || activityreminder_settings.tempThreshold >= E.getTemperature());
|
||||
}
|
||||
|
||||
function isDuringAlertHours(h) {
|
||||
if (activityreminder_settings.startHour < activityreminder_settings.endHour) { // not passing through midnight
|
||||
return (h >= activityreminder_settings.startHour && h < activityreminder_settings.endHour);
|
||||
} else { // passing through midnight
|
||||
return (h >= activityreminder_settings.startHour || h < activityreminder_settings.endHour);
|
||||
}
|
||||
if(stepsArray.length >= activityreminder.maxInnactivityMin){
|
||||
if (stepsArray[0] - stepsArray[stepsArray.length-1] < activityreminder.minSteps){
|
||||
load('activityreminder.app.js');
|
||||
}
|
||||
}
|
||||
|
||||
function mustAlert(now) {
|
||||
if ((now - activityreminder_data.stepsDate) / 60000 > activityreminder_settings.maxInnactivityMin) { // inactivity detected
|
||||
if ((now - activityreminder_data.okDate) / 60000 > 3 && // last alert anwsered with ok was more than 3 min ago
|
||||
(now - activityreminder_data.dismissDate) / 60000 > activityreminder_settings.dismissDelayMin && // last alert was more than dismissDelayMin ago
|
||||
(now - activityreminder_data.pauseDate) / 60000 > activityreminder_settings.pauseDelayMin) { // last alert was more than pauseDelayMin ago
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Bangle.on('midnight', function () {
|
||||
/*
|
||||
Usefull trick to have the app working smothly for people using it at night
|
||||
*/
|
||||
let now = new Date();
|
||||
let h = now.getHours();
|
||||
if (activityreminder_settings.enabled && isDuringAlertHours(h)) {
|
||||
// updating only the steps and keeping the original stepsDate on purpose
|
||||
activityreminder_data.stepsOnDate = 0;
|
||||
activityreminder.saveData(activityreminder_data);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
activityreminder = require("activityreminder").loadSettings();
|
||||
if(activityreminder.enabled) {
|
||||
stepsArray = require("activityreminder").loadStepsArray();
|
||||
if (activityreminder_settings.enabled) {
|
||||
setInterval(run, 60000);
|
||||
}
|
||||
|
||||
/* todo in a futur release
|
||||
increase setInterval time to something that is still sensible (5 mins ?)
|
||||
when we added a settimer
|
||||
*/
|
||||
}
|
||||
})();
|
||||
|
|
|
@ -1,22 +1,44 @@
|
|||
exports.loadSettings = function() {
|
||||
return Object.assign({
|
||||
enabled: true,
|
||||
startHour: 9,
|
||||
endHour: 20,
|
||||
maxInnactivityMin: 30,
|
||||
dismissDelayMin: 15,
|
||||
minSteps: 50
|
||||
}, require("Storage").readJSON("activityreminder.s.json", true) || {});
|
||||
exports.loadSettings = function () {
|
||||
return Object.assign({
|
||||
enabled: true,
|
||||
startHour: 9,
|
||||
endHour: 20,
|
||||
maxInnactivityMin: 30,
|
||||
dismissDelayMin: 15,
|
||||
pauseDelayMin: 120,
|
||||
minSteps: 50,
|
||||
tempThreshold: 27
|
||||
}, require("Storage").readJSON("activityreminder.s.json", true) || {});
|
||||
};
|
||||
|
||||
exports.writeSettings = function(settings){
|
||||
require("Storage").writeJSON("activityreminder.s.json", settings);
|
||||
exports.writeSettings = function (settings) {
|
||||
require("Storage").writeJSON("activityreminder.s.json", settings);
|
||||
};
|
||||
|
||||
exports.saveStepsArray = function(stepsArray) {
|
||||
require("Storage").writeJSON("activityreminder.sa.json", stepsArray);
|
||||
exports.saveData = function (data) {
|
||||
require("Storage").writeJSON("activityreminder.data.json", data);
|
||||
};
|
||||
|
||||
exports.loadStepsArray = function(){
|
||||
return require("Storage").readJSON("activityreminder.sa.json") || [];
|
||||
exports.loadData = function () {
|
||||
let health = Bangle.getHealthStatus("day");
|
||||
let data = Object.assign({
|
||||
firstLoad: true,
|
||||
stepsDate: new Date(),
|
||||
stepsOnDate: health.steps,
|
||||
okDate: new Date(1970),
|
||||
dismissDate: new Date(1970),
|
||||
pauseDate: new Date(1970),
|
||||
},
|
||||
require("Storage").readJSON("activityreminder.data.json") || {});
|
||||
|
||||
if (typeof (data.stepsDate) == "string")
|
||||
data.stepsDate = new Date(data.stepsDate);
|
||||
if (typeof (data.okDate) == "string")
|
||||
data.okDate = new Date(data.okDate);
|
||||
if (typeof (data.dismissDate) == "string")
|
||||
data.dismissDate = new Date(data.dismissDate);
|
||||
if (typeof (data.pauseDate) == "string")
|
||||
data.pauseDate = new Date(data.pauseDate);
|
||||
|
||||
return data;
|
||||
};
|
|
@ -3,7 +3,7 @@
|
|||
"name": "Activity Reminder",
|
||||
"shortName":"Activity Reminder",
|
||||
"description": "A reminder to take short walks for the ones with a sedentary lifestyle",
|
||||
"version":"0.02",
|
||||
"version":"0.09",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
"tags": "tool,activity",
|
||||
|
@ -13,11 +13,12 @@
|
|||
{"name": "activityreminder.app.js", "url":"app.js"},
|
||||
{"name": "activityreminder.boot.js", "url": "boot.js"},
|
||||
{"name": "activityreminder.settings.js", "url": "settings.js"},
|
||||
{"name": "activityreminder.alert.js", "url": "alert.js"},
|
||||
{"name": "activityreminder", "url": "lib.js"},
|
||||
{"name": "activityreminder.img", "url": "app-icon.js", "evaluate": true}
|
||||
],
|
||||
"data": [
|
||||
{"name": "activityreminder.s.json"},
|
||||
{"name": "activityreminder.sa.json"}
|
||||
{"name": "activityreminder.data.json", "storageFile": true}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
(function(back) {
|
||||
(function (back) {
|
||||
// Load settings
|
||||
var settings = require("activityreminder").loadSettings();
|
||||
const activityreminder = require("activityreminder");
|
||||
let settings = activityreminder.loadSettings();
|
||||
|
||||
// Show the menu
|
||||
E.showMenu({
|
||||
"" : { "title" : "Activity Reminder" },
|
||||
"< Back" : () => back(),
|
||||
function getMainMenu(){
|
||||
var mainMenu = {
|
||||
"": { "title": "Activity Reminder" },
|
||||
"< Back": () => back(),
|
||||
'Enable': {
|
||||
value: settings.enabled,
|
||||
format: v => v?"Yes":"No",
|
||||
onchange: v => {
|
||||
settings.enabled = v;
|
||||
require("activityreminder").writeSettings(settings);
|
||||
activityreminder.writeSettings(settings);
|
||||
}
|
||||
},
|
||||
'Start hour': {
|
||||
|
@ -19,46 +19,68 @@
|
|||
min: 0, max: 24,
|
||||
onchange: v => {
|
||||
settings.startHour = v;
|
||||
require("activityreminder").writeSettings(settings);
|
||||
activityreminder.writeSettings(settings);
|
||||
}
|
||||
},
|
||||
'End hour': {
|
||||
},
|
||||
'End hour': {
|
||||
value: settings.endHour,
|
||||
min: 0, max: 24,
|
||||
onchange: v => {
|
||||
settings.endHour = v;
|
||||
require("activityreminder").writeSettings(settings);
|
||||
activityreminder.writeSettings(settings);
|
||||
}
|
||||
},
|
||||
'Max inactivity': {
|
||||
},
|
||||
'Max inactivity': {
|
||||
value: settings.maxInnactivityMin,
|
||||
min: 15, max: 120,
|
||||
onchange: v => {
|
||||
settings.maxInnactivityMin = v;
|
||||
require("activityreminder").writeSettings(settings);
|
||||
activityreminder.writeSettings(settings);
|
||||
},
|
||||
format: x => {
|
||||
return x + " min";
|
||||
}
|
||||
},
|
||||
'Dismiss delay': {
|
||||
format: x => x + "m"
|
||||
},
|
||||
'Dismiss delay': {
|
||||
value: settings.dismissDelayMin,
|
||||
min: 5, max: 15,
|
||||
min: 5, max: 60,
|
||||
onchange: v => {
|
||||
settings.dismissDelayMin = v;
|
||||
require("activityreminder").writeSettings(settings);
|
||||
activityreminder.writeSettings(settings);
|
||||
},
|
||||
format: x => x + "m"
|
||||
},
|
||||
'Pause delay': {
|
||||
value: settings.pauseDelayMin,
|
||||
min: 30, max: 240, step: 5,
|
||||
onchange: v => {
|
||||
settings.pauseDelayMin = v;
|
||||
activityreminder.writeSettings(settings);
|
||||
},
|
||||
format: x => {
|
||||
return x + " min";
|
||||
return x + "m";
|
||||
}
|
||||
},
|
||||
'Min steps': {
|
||||
},
|
||||
'Min steps': {
|
||||
value: settings.minSteps,
|
||||
min: 10, max: 500,
|
||||
min: 10, max: 500, step: 10,
|
||||
onchange: v => {
|
||||
settings.minSteps = v;
|
||||
require("activityreminder").writeSettings(settings);
|
||||
activityreminder.writeSettings(settings);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
'Temp Threshold': {
|
||||
value: settings.tempThreshold,
|
||||
min: 20, max: 40, step: 0.5,
|
||||
format: v => v + "°C",
|
||||
onchange: v => {
|
||||
settings.tempThreshold = v;
|
||||
activityreminder.writeSettings(settings);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return mainMenu;
|
||||
}
|
||||
|
||||
// Show the menu
|
||||
E.showMenu(getMainMenu());
|
||||
})
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
0.01: AdvCasio first version
|
||||
0.02: Remove un-needed fonts to improve memory usage
|
||||
0.03: Tell clock widgets to hide.
|
|
@ -0,0 +1,62 @@
|
|||
# Adv Casio Clock
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2981891/175355586-1dfc0d66-6555-4385-b124-1605fdb71a11.jpg" width="250" />
|
||||
|
||||
An over-engineered clock inspired by Casio watches.<br/>
|
||||
It has a dedicated timer, a scratchpad and can display the weather condition 4 days ahead.<br/>
|
||||
It uses a <a target="_blank" href="https://dotgreg.github.io/advCasioBangleClock/">custom web app</a> to update its content.<br/>
|
||||
Forked from the awesome Cassio Watch.<br/>
|
||||
|
||||
## Todo
|
||||
|
||||
- Improving quality of the background images, right now it is quite blurry.
|
||||
- Improving screenshots quality.
|
||||
- Improving web app look.
|
||||
- Improving bangle app performances (using functions for images and specialized array).
|
||||
|
||||
## Functionalities
|
||||
|
||||
- Current time
|
||||
- Current day and month
|
||||
- Footsteps
|
||||
- Battery
|
||||
- Simple Timer embedded
|
||||
- Weather forecast (7 days)
|
||||
- Scratchpad
|
||||
|
||||
## Screenshots
|
||||
Clock:<br/>
|
||||
<img src="https://user-images.githubusercontent.com/2981891/175519126-049caf93-73d0-4472-9650-33b28f80843c.jpg" width="250" />
|
||||
<img src="https://user-images.githubusercontent.com/2981891/175519128-96926e32-2165-4c61-9364-843440304bb9.jpg" width="250" />
|
||||
<img src="https://user-images.githubusercontent.com/2981891/175519130-4921073c-48fc-4c29-932d-d42acc3b395c.jpg" width="250" />
|
||||
|
||||
Web interface to update weather & scratchpad <br/>
|
||||
<a target="_blank" ref="https://dotgreg.github.io/advCasioBangleClock/">https://dotgreg.github.io/advCasioBangleClock</a>
|
||||
|
||||
<img src="https://user-images.githubusercontent.com/2981891/175519121-851bb209-7192-40db-a014-490c344f7597.jpg" width="250" />
|
||||
|
||||
## Usage
|
||||
### How to update the tasks list / weather
|
||||
- you will need a <a target="_blank" href="https://openweathermap.org/price#weather">free openweathermap.org api key</a>.
|
||||
- go to https://dotgreg.github.io/advCasioBangleClock/
|
||||
- Alternatively you can install it on your own server/heroku/service/github pages, the web-app code is <a target="_blank" href="https://github.com/dotgreg/advCasioBangleClock/tree/master/web-app">here</a>
|
||||
- fill the location and the api key (it will be saved on your browser, no need to do it each time)
|
||||
- edit the scratchpad with what you want
|
||||
- click on sync
|
||||
- reload your clock!
|
||||
|
||||
### How to start/stop the timer
|
||||
- swipe up : add time (+5min)
|
||||
- swipe down : remove time (-5min)
|
||||
- swipe right : start timer
|
||||
- swipe left : stop timer
|
||||
|
||||
## Links
|
||||
### Issues, suggestions and bugtracker
|
||||
<a target="_blank" href="https://github.com/dotgreg/advCasioBangleClock/issues">https://github.com/dotgreg/advCasioBangleClock/issues</a>
|
||||
|
||||
### Code repository (bangle app and web app)
|
||||
<a target="_blank" href="https://github.com/dotgreg/advCasioBangleClock">https://github.com/dotgreg/advCasioBangleClock</a>
|
||||
|
||||
### Creator
|
||||
<a target="_blank" href="https://github.com/dotgreg">https://github.com/dotgreg</a>
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwghC/AH4A/AGsCmUQC6kf/8wC6k///wgEv//zD4PxAQIJBABP//4QBC4IcBh/yEQIXKgP/l4rBl/yGAMP/4iBKJUC/5gBIAQVBBAMR/8gC5IQBAAMQC4IVBFoMjAYIXNmAXBgYXCPgQAJl/xHwPwj/yn5kC/55BUxSlC+JiBVgQ5BUxiDBUIIXBIQQXBcCoA/AH4ADXAUgUAUQBAkPeoTDFgIHBAALQEA4XwC4IOEAAQRBbAQBBCAIgBEYMQC4TnEC4XyeQgBDAAMwC4pIDC4kDAgJLD//xC5QIBNQISCFYIZCC4aEBAQRCDAAPyl4hBOIh3Cn53GNgMRiKxGBAR5BAoYA/AH4A/AH4A5A"))
|
|
@ -0,0 +1,304 @@
|
|||
const storage = require('Storage');
|
||||
|
||||
require("Font8x12").add(Graphics);
|
||||
require("Font7x11Numeric7Seg").add(Graphics);
|
||||
|
||||
function bigThenSmall(big, small, x, y) {
|
||||
g.setFont("7x11Numeric7Seg", 2);
|
||||
g.drawString(big, x, y);
|
||||
x += g.stringWidth(big);
|
||||
g.setFont("8x12");
|
||||
g.drawString(small, x, y);
|
||||
}
|
||||
|
||||
function getClockBg() {
|
||||
return require("heatshrink").decompress(atob("icVgf/ABv8v4DBx4CB+PH8F+nAGB48fwEHBwXjxwqBuPH//+nAGBBwIjCAwI2D/wGBgIyDI4QGDwAGBHYX/4AGBn4UFEYQpCEYYpCAAMfMhP4FIgABwJ8OEBIA=="));
|
||||
}
|
||||
|
||||
|
||||
// sun, cloud, rain, thunder
|
||||
var iconsWeather = [
|
||||
require("heatshrink").decompress(atob("i8Ugf/ACcfA434BA/AAwsAv0/8F/BAcDwEHHIpECFI3wn4GC/gOC+PAGoXggEH/+ODQgXBGQv/wAbBBAnguEACIn4gfxI4JXFwJmG/kPBA3jSynw")), require("heatshrink").decompress(atob("i0Ugf/AEXggIGE/0A/kPBAmBCIN/A4Y8CgAICwEHBYoUE/ACCj4sDn4CBC4YyDwBrDCgYA3A")), require("heatshrink").decompress(atob("h8Rgf/AAuBAgf8h4FDCwM/AgPA/gFC/0HgEBBQPwnEfDoWAg4jC/gOCAoQmBAQXjFIV//8f//4IQP4j/+gAIB4EcHII4CAoI+DLQJXF/AA==")), require("heatshrink").decompress(atob("h0Pgf/AA8fAYX+g4EC8EBAgXADAeAgAECgAOC/wrCDQIOBBYfwgAaC/kAn4EB/EAv4aDHAeBIg38"))
|
||||
];
|
||||
|
||||
|
||||
function getBackgroundImage() {
|
||||
return require("heatshrink").decompress(atob("2GwghC/AH4A/AH4AMl////wAwURiQECgUzmcxBQQCBiYUBBARW+LAcCAgcPBYgFBkAIFG7kQiAKIiIKBgISOAAJBD//zKQfxK4vyAoMQCgn/ERBhBBYR5BAwR1DB4Y2DgYPCGIQRCCQcP+EfGJI0FEgRSCGAQCCX4JXCkAhDn4lI+HyK4YWBFIPzJYJXHAIMSK4cwJ4I3CAYMzA4cfcRMBdwytBK4i6FK4IUCMgYAEGIITBK4cCaAPwgJXB+fzK4sAgYtCK5EfA4pXR+AmBaIZYCK6KcCAwSjDEYXx/8vK5QRCK4kPK6cDkJREBIMBfgIrDK5svUAIQBAwIaCK4w+DK4YGBK7IaBboIuCK4gFCJwYBBiBCCCgQhHHYgGDgArBK5IGDAYMgJ4Xwn53BGgLVDmBXKAAinDLpJXCAAYhHR4YODn/wJIPyTYZXDE4RXD+ECNILIDAIPwj4xIAAYNCR4fyVIYLFA4KEBBAglKAGUCmcykEAiMQBIURBYM/BgIUEgcz+bTKAH4A/AH4A/AHP/AGY1d+BWCh5X/LCpW1K74fgG/5X/AH5X/K9Bg/K63wK/5XWgBX/K6pWBK/5XU+BWBh5J/K6auCK/5XTVwRfFAH5XOKwRX/K6auDh5I/K6SuDWP5XSVwYADWX6vXK/5XQWQpW/K6auDJP5XWV35XT+Cu/K7Ku/K65H/K6hW/K7EPI35XWIv5XWAH5X/K/4A/K/5X/K/4A/K9cAAH4A/AFzz/AHRX/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/5X/AH5X/K/4A/K/5X/K/4A/K/5X/K/4A/K/5X/AH5X/K/5X/AH5X/K/40VAH4A/AFzLb+EPDm4AdK/5X/K+PwgEAHy5X9HgMAK/5XXH6xX/H65X/K/5X/K98AK7sAgBX3DjBWFO644DSTHwGzJXED4RXaDoLqcK7weWDIQcXK8I6YK77KXK4o8DPbY6ZK7qvDDy6vdR7JXDh60EDyw5BAIRXYSwjMbAgIhUDwJZCHwJX0GwjRWNwIAEHSwBCDSpXFH4pXzDS5XIEARXVSYbQEDaYzCK+6vcKaxXNDypX9HwQkbHS40COSpXKK2A6CHgRXcPIhX0SwpXYVuQ6EgBX/K644YODBXkSDJX/K/5X/DtRX6gA3YOkRWbLDZX4KwYA/AG8F5vdABncKH4AGhpRJAYXNAgPAKP4AF5vMJwoDBAQIKE6BR/AAvc5vO9wAB7oCB9veAoPcAoPcK+kwh8AgcA98An//gH/+sD//wCISgBJ4IABAYpaC9vdK4UP/9AAQNQr/zgHwEYNQFYQAh+EP+FegH+A4QBCMQIKBAAPNK4yxBA4RXCV4YZBE4IjChwCDmApCK8VdmHggHgFYf0SQJXE5nMK4anCAoYHC5pXCaQJXBop+BqAGEK7f/AAQeEKwQrBqCtDAILjBCQfNK4JTCAYZXF7qvD//gV4S2DgEFFIYAECgIACMC8PKoIBB8n1K4ivF5vc5xOCWYZbBAYavHU4RXCr4pEAEMDfoNQGoMEgEwYQPwAoIBBAAPM5ipC7oDCVIIAE7hXCD4SdBiEP+gGBgihCFYIAz5pXBAAnN7oIB7nc5gOBK4QA/K4pNCWgSpCBInNK/4AGhncKIStC7gCBA4QAC4BR/AAysCABZW/AHwA="));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// schedule a draw for the next minute
|
||||
let rocketInterval;
|
||||
var drawTimeout;
|
||||
function queueDraw() {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}
|
||||
|
||||
|
||||
function clearIntervals() {
|
||||
if (rocketInterval) clearInterval(rocketInterval);
|
||||
rocketInterval = undefined;
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////
|
||||
// TIMER FUNC
|
||||
//
|
||||
var timer_time = 0;
|
||||
var alreadyListenTouch = false;
|
||||
function initTouchTimer () {
|
||||
if (alreadyListenTouch) return;
|
||||
alreadyListenTouch = true;
|
||||
|
||||
Bangle.on('swipe', function(dirX,dirY) {
|
||||
if (canTouch === false) return;
|
||||
var njson = getDataJson();
|
||||
if (!njson) return;
|
||||
|
||||
if (dirX === -1) {
|
||||
timer_time = 0;
|
||||
delete njson.timer;
|
||||
setDataJson(njson);
|
||||
}
|
||||
else if (dirX === 1) {
|
||||
var now = new Date().getTime();
|
||||
njson.timer = now + (timer_time * 1000 * 60);
|
||||
Bangle.setLocked(true);
|
||||
setDataJson(njson);
|
||||
Bangle.buzz(200, 0);
|
||||
timer_time = 0;
|
||||
}
|
||||
else if (dirY === -1) {
|
||||
if (canTouch === false || njson.timer) return;
|
||||
timer_time = timer_time + 5;
|
||||
}
|
||||
else if (dirY === 1) {
|
||||
if (canTouch === false || njson.timer) return;
|
||||
timer_time = timer_time - 5;
|
||||
}
|
||||
draw();
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
initTouchTimer ();
|
||||
});
|
||||
|
||||
function getTimerTime() {
|
||||
// if timer_time !== -1, take it
|
||||
if (timer_time !== 0) {
|
||||
return timer_time + "m";
|
||||
} else {
|
||||
// else, show diff between njsontime and now
|
||||
var njson = getDataJson();
|
||||
if (!njson) return false;
|
||||
var now = new Date().getTime();
|
||||
var diff = Math.round((njson.timer - now) / (1000 * 60));
|
||||
//console.log(123, njson, diff, now, njson.timer - now);
|
||||
if (diff > 0) return diff + "m";
|
||||
else if (njson.timer) {
|
||||
Bangle.buzz(1000, 1);
|
||||
console.log("END OF TIMER");
|
||||
delete njson.timer;
|
||||
setDataJson(njson);
|
||||
return false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
// if diff is <0, delete timer from json
|
||||
}
|
||||
}
|
||||
function drawTimer() {
|
||||
//g.drawString(getTimerTime(), 100, 100);
|
||||
g.setFont("8x12", 2);
|
||||
var t = 97;
|
||||
var l = 105;
|
||||
var time = getTimerTime();
|
||||
if (time || timer_time !== 0) g.drawString(time, l+5, t+0);
|
||||
if (time && timer_time === 0) g.drawImage(getClockBg(), l-20, t+2, { scale: 1 });
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////
|
||||
// DATA READING
|
||||
//
|
||||
function getDataJson(){
|
||||
var res = {"tasks":"", "weather":[]};
|
||||
try {
|
||||
res = storage.readJSON('advcasio.data.json');
|
||||
} catch(ex) {
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
function setDataJson(resJson){
|
||||
try {
|
||||
res = storage.writeJSON('advcasio.data.json', resJson);
|
||||
} catch(ex) {
|
||||
return res;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
var dataJson = getDataJson();
|
||||
|
||||
////////////////////////////////////////////
|
||||
// WEATHER!
|
||||
//
|
||||
function drawWeather(arr) {
|
||||
g.setFont("6x8", 1);
|
||||
var p = {l: 8, tText: 40, tIcon:20, decal:25};
|
||||
var today = new Date().getTime();
|
||||
var yesterday = today - (1000 * 60 * 60 * 24);
|
||||
var testday = today + (1000 * 60 * 60 * 24 * 2);
|
||||
//12h auj > 12h hier qui est sup a 0h auj
|
||||
//23h59 hier est sup a 0h auj
|
||||
var j = 0;
|
||||
for(var i = 0; i<arr.length;i++) {
|
||||
if (arr[i][2] > yesterday && j < 4) {
|
||||
g.drawString(arr[i][0], p.l + p.decal*j + 4, p.tText);
|
||||
g.drawImage(iconsWeather[arr[i][1]], p.l + p.decal*j, p.tIcon, { scale: 1 });
|
||||
j++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
////////////////////////////////////////////
|
||||
// DRAWING FUNCS
|
||||
//
|
||||
function drawTasks(str) {
|
||||
g.setFont("6x8", 1);
|
||||
var t = 57;
|
||||
var l = 0;
|
||||
g.drawString(str, l+5, t+0);
|
||||
}
|
||||
|
||||
function drawSteps() {
|
||||
g.setFont("8x12", 2);
|
||||
var t = 132;
|
||||
var l = 150;
|
||||
g.drawString(getSteps(), l+5, t+0);
|
||||
}
|
||||
|
||||
|
||||
function drawClock() {
|
||||
g.setFont("7x11Numeric7Seg", 3);
|
||||
g.clearRect(80, 57, 170, 96);
|
||||
g.setColor(255, 255, 255);
|
||||
var l = 77;
|
||||
var t = 57;
|
||||
var w = 170;
|
||||
var h = 116;
|
||||
g.drawRect(l, t, w, h);
|
||||
g.fillRect(l, t, w, h);
|
||||
g.setColor(0, 0, 0);
|
||||
g.drawString(require("locale").time(new Date(), 1), 76, 60);
|
||||
|
||||
// day
|
||||
//g.setFont("8x12", 1);
|
||||
//g.setFont("9x18", 1);
|
||||
//g.drawString(require("locale").dow(new Date(), 2).toUpperCase(), 25, 136);
|
||||
g.setFont("8x12", 2);
|
||||
g.drawString(require("locale").dow(new Date(), 2), 18, 130);
|
||||
|
||||
// month
|
||||
g.setFont("8x12");
|
||||
g.drawString(require("locale").month(new Date(), 2).toUpperCase(), 80, 127);
|
||||
|
||||
// day nb
|
||||
g.setFont("8x12", 2);
|
||||
const time = new Date().getDate();
|
||||
g.drawString(time < 10 ? "0" + time : time, 78, 137);
|
||||
}
|
||||
|
||||
function drawBattery() {
|
||||
bigThenSmall(E.getBattery(), "%", 140, 23);
|
||||
}
|
||||
|
||||
|
||||
function getSteps() {
|
||||
var steps = 0;
|
||||
try{
|
||||
if (WIDGETS.wpedom !== undefined) {
|
||||
steps = WIDGETS.wpedom.getSteps();
|
||||
} else if (WIDGETS.activepedom !== undefined) {
|
||||
steps = WIDGETS.activepedom.getSteps();
|
||||
} else {
|
||||
steps = Bangle.getHealthStatus("day").steps;
|
||||
}
|
||||
} catch(ex) {
|
||||
// In case we failed, we can only show 0 steps.
|
||||
return "? k";
|
||||
}
|
||||
|
||||
steps = Math.round(steps/1000);
|
||||
return steps + "k";
|
||||
}
|
||||
|
||||
|
||||
|
||||
function draw() {
|
||||
|
||||
queueDraw();
|
||||
|
||||
g.reset();
|
||||
g.clear();
|
||||
g.setColor(255, 255, 255);
|
||||
g.fillRect(0, 0, g.getWidth(), g.getHeight());
|
||||
let background = getBackgroundImage();
|
||||
g.drawImage(background, 0, 0, { scale: 1 });
|
||||
|
||||
|
||||
g.setColor(0, 0, 0);
|
||||
if(dataJson && dataJson.weather) drawWeather(dataJson.weather);
|
||||
if(dataJson && dataJson.tasks) drawTasks(dataJson.tasks);
|
||||
|
||||
|
||||
g.setFontAlign(0,-1);
|
||||
g.setFont("8x12", 2);
|
||||
|
||||
drawSteps();
|
||||
g.setFontAlign(-1,-1);
|
||||
drawClock();
|
||||
drawBattery();
|
||||
drawTimer();
|
||||
// Hide widgets
|
||||
for (let wd of WIDGETS) {wd.draw=()=>{};wd.area="";}
|
||||
}
|
||||
|
||||
// save batt power, does not seem to work although...
|
||||
var canTouch = true;
|
||||
Bangle.on("lcdPower", (on) => {
|
||||
if (on) {
|
||||
draw();
|
||||
} else {
|
||||
canTouch = false;
|
||||
clearIntervals();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Bangle.on("lock", (locked) => {
|
||||
clearIntervals();
|
||||
draw();
|
||||
if (!locked) {
|
||||
canTouch = true;
|
||||
} else {
|
||||
canTouch = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Bangle.setUI("clock");
|
||||
|
||||
// Load widgets, but don't show them
|
||||
Bangle.loadWidgets();
|
||||
|
||||
g.reset();
|
||||
g.clear();
|
||||
draw();
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1 @@
|
|||
{"tasks":"", "weather":[]};
|
|
@ -0,0 +1,25 @@
|
|||
{ "id": "advcasio",
|
||||
"name": "Advanced Casio Clock",
|
||||
"shortName":"advcasio",
|
||||
"version":"0.03",
|
||||
"description": "An over-engineered clock inspired by Casio watches. It has a 4 days weather, a timer using swipe and a scratchpad. Can be updated using a dedicated webapp.",
|
||||
"icon": "app.png",
|
||||
"tags": "clock",
|
||||
"type": "clock",
|
||||
"screenshots": [
|
||||
{ "url": "screenshot-clock-1.jpg" },
|
||||
{ "url": "screenshot-clock-2.jpg" },
|
||||
{ "url": "screenshot-clock-3.jpg" },
|
||||
{ "url": "screenshot-webapp.jpg" }
|
||||
],
|
||||
"supports" : ["BANGLEJS", "BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"allow_emulator":true,
|
||||
"storage": [
|
||||
{"name":"advcasio.app.js","url":"app.js"},
|
||||
{"name":"advcasio.img","url":"app-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [
|
||||
{ "name": "advcasio.data.json", "url": "data.json", "storageFile": true }
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 88 KiB |
After Width: | Height: | Size: 87 KiB |
After Width: | Height: | Size: 104 KiB |
After Width: | Height: | Size: 230 KiB |
|
@ -0,0 +1,5 @@
|
|||
0.01: Basic agenda with events from GB
|
||||
0.02: Added settings page to force calendar sync
|
||||
0.03: Disable past events display from settings
|
||||
0.04: Added awareness of allDay field
|
||||
0.05: Displaying calendar colour and name
|
|
@ -0,0 +1,20 @@
|
|||
# Agenda
|
||||
|
||||
Basic agenda reading the events synchronised from GadgetBridge.
|
||||
|
||||
### Functionalities
|
||||
|
||||
* List all events in the next week (or whatever is synchronized)
|
||||
* Optionally view past events (until GB removes them)
|
||||
* Show start time and location of the events in the list
|
||||
* Show the colour of the calendar in the list
|
||||
* Display description, location and calendar name after tapping on events
|
||||
|
||||
### Report a bug
|
||||
|
||||
You can easily open an issue in the espruino repo, but I won't be notified and it might take time.
|
||||
If you want a (hopefully) quicker response, just report [on my fork](https://github.com/glemco/BangleApps).
|
||||
|
||||
### Known Problems
|
||||
|
||||
Any all-day event lasts just one day: that is a GB limitation that we will likely fix in the future.
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwg1yhGIxAPMBwIPFhH//GAC5n/C4oHBC5/IGwoXBHQQAKC4OIFAWOxHv9GO9wAKI4XoC4foEIIWLC4IABC4gIBFxnuE4IqBC4gARC4ZzNAAwXaxe7ACO4C625C4m4xIJBzAeCxGbCAOIFgQOBC4pOBxe4AYIPBAYQKCAYYXE3GL/ADBx/oxb3BC4X+xG4xwOBC4uP/YDB54MBf4Po3eM/4XBx/+C4pTBGIIkBLgOYAYIvB9GJBwI6BL45zCL4aCCL4h3GU64ALdYS1CI55bBAAgXFO4mMO4QDBDIO/////YxBU53IxIVB/GfDAWYa5wtC/GPAYWIL4wXBL4oSBC4jcBC4m4QIWYSwWIIQIAG/CnMMAIAC/JLCMIIvMIwZHFJAJfLC5yPHAYIRDAoy/KCIi7BMon4d4+Od4IXBxAZBEQLtB/+YxIXDL4SLCL4WPzAXCNgRFBLIKnKLIrcEI4gXNAAp3CxGZAAzCBC5KnCKAIAICxBlBC4IAJxG/C4/4wAXLhBgD/IcD3AXMGAIqDDgRGNGAoXDFxxhEI4W4FxwwCaoYWBFx4YDAAQWRAEQ"))
|
|
@ -0,0 +1,138 @@
|
|||
/* CALENDAR is a list of:
|
||||
{id:int,
|
||||
type,
|
||||
timestamp,
|
||||
durationInSeconds,
|
||||
title,
|
||||
description,
|
||||
location,
|
||||
color:int,
|
||||
calName,
|
||||
allDay: bool,
|
||||
}
|
||||
*/
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
var FILE = "android.calendar.json";
|
||||
|
||||
var Locale = require("locale");
|
||||
|
||||
var fontSmall = "6x8";
|
||||
var fontMedium = g.getFonts().includes("6x15")?"6x15":"6x8:2";
|
||||
var fontBig = g.getFonts().includes("12x20")?"12x20":"6x8:2";
|
||||
var fontLarge = g.getFonts().includes("6x15")?"6x15:2":"6x8:4";
|
||||
|
||||
//FIXME maybe write the end from GB already? Not durationInSeconds here (or do while receiving?)
|
||||
var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[];
|
||||
var settings = require("Storage").readJSON("agenda.settings.json",true)||{};
|
||||
|
||||
CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp);
|
||||
|
||||
function getDate(timestamp) {
|
||||
return new Date(timestamp*1000);
|
||||
}
|
||||
function formatDateLong(date, includeDay, allDay) {
|
||||
let shortTime = Locale.time(date,1)+Locale.meridian(date);
|
||||
if(allDay) shortTime = "";
|
||||
if(includeDay || allDay)
|
||||
return Locale.date(date)+" "+shortTime;
|
||||
return shortTime;
|
||||
}
|
||||
function formatDateShort(date, allDay) {
|
||||
return Locale.date(date).replace(/\d\d\d\d/,"")+(allDay?
|
||||
"" : Locale.time(date,1)+Locale.meridian(date));
|
||||
}
|
||||
|
||||
var lines = [];
|
||||
function showEvent(ev) {
|
||||
var bodyFont = fontBig;
|
||||
if(!ev) return;
|
||||
g.setFont(bodyFont);
|
||||
//var lines = [];
|
||||
if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10);
|
||||
var titleCnt = lines.length;
|
||||
var start = getDate(ev.timestamp);
|
||||
var end = getDate((+ev.timestamp) + (+ev.durationInSeconds));
|
||||
var includeDay = true;
|
||||
if (titleCnt) lines.push(""); // add blank line after title
|
||||
if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth())
|
||||
includeDay = false;
|
||||
if(includeDay || ev.allDay) {
|
||||
lines = lines.concat(
|
||||
/*LANG*/"Start:",
|
||||
g.wrapString(formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10),
|
||||
/*LANG*/"End:",
|
||||
g.wrapString(formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10));
|
||||
} else {
|
||||
lines = lines.concat(
|
||||
g.wrapString(Locale.date(start), g.getWidth()-10),
|
||||
g.wrapString(/*LANG*/"Start"+": "+formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10),
|
||||
g.wrapString(/*LANG*/"End"+": "+formatDateLong(end, includeDay, ev.allDay), g.getWidth()-10));
|
||||
}
|
||||
if(ev.location)
|
||||
lines = lines.concat(/*LANG*/"Location"+": ", g.wrapString(ev.location, g.getWidth()-10));
|
||||
if(ev.description)
|
||||
lines = lines.concat("",g.wrapString(ev.description, g.getWidth()-10));
|
||||
if(ev.calName)
|
||||
lines = lines.concat(/*LANG*/"Calendar"+": ", g.wrapString(ev.calName, g.getWidth()-10));
|
||||
lines = lines.concat(["",/*LANG*/"< Back"]);
|
||||
E.showScroller({
|
||||
h : g.getFontHeight(), // height of each menu item in pixels
|
||||
c : lines.length, // number of menu items
|
||||
// a function to draw a menu item
|
||||
draw : function(idx, r) {
|
||||
// FIXME: in 2v13 onwards, clearRect(r) will work fine. There's a bug in 2v12
|
||||
g.setBgColor(idx<titleCnt ? g.theme.bg2 : g.theme.bg).
|
||||
setColor(idx<titleCnt ? g.theme.fg2 : g.theme.fg).
|
||||
clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
|
||||
g.setFont(bodyFont).drawString(lines[idx], r.x, r.y);
|
||||
}, select : function(idx) {
|
||||
if (idx>=lines.length-2)
|
||||
showList();
|
||||
},
|
||||
back : () => showList()
|
||||
});
|
||||
}
|
||||
|
||||
function showList() {
|
||||
//it might take time for GB to delete old events, decide whether to show them grayed out or hide entirely
|
||||
if(!settings.pastEvents) {
|
||||
let now = new Date();
|
||||
//TODO add threshold here?
|
||||
CALENDAR = CALENDAR.filter(ev=>ev.timestamp + ev.durationInSeconds > now/1000);
|
||||
}
|
||||
if(CALENDAR.length == 0) {
|
||||
E.showMessage("No events");
|
||||
return;
|
||||
}
|
||||
E.showScroller({
|
||||
h : 52,
|
||||
c : Math.max(CALENDAR.length,3), // workaround for 2v10.219 firmware (min 3 not needed for 2v11)
|
||||
draw : function(idx, r) {"ram"
|
||||
var ev = CALENDAR[idx];
|
||||
g.setColor(g.theme.fg);
|
||||
g.clearRect(r.x,r.y,r.x+r.w, r.y+r.h);
|
||||
if (!ev) return;
|
||||
var isPast = false;
|
||||
var x = r.x+2, title = ev.title;
|
||||
var body = formatDateShort(getDate(ev.timestamp),ev.allDay)+"\n"+(ev.location?ev.location:/*LANG*/"No location");
|
||||
if(settings.pastEvents) isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000;
|
||||
if (title) g.setFontAlign(-1,-1).setFont(fontBig)
|
||||
.setColor(isPast ? "#888" : g.theme.fg).drawString(title, x+4,r.y+2);
|
||||
if (body) {
|
||||
g.setFontAlign(-1,-1).setFont(fontMedium).setColor(isPast ? "#888" : g.theme.fg);
|
||||
g.drawString(body, x+10,r.y+20);
|
||||
}
|
||||
g.setColor("#888").fillRect(r.x,r.y+r.h-1,r.x+r.w-1,r.y+r.h-1); // dividing line between items
|
||||
if(ev.color) {
|
||||
g.setColor("#"+(0x1000000+Number(ev.color)).toString(16).padStart(6,"0"));
|
||||
g.fillRect(r.x,r.y+4,r.x+3, r.y+r.h-4);
|
||||
}
|
||||
},
|
||||
select : idx => showEvent(CALENDAR[idx]),
|
||||
back : () => load()
|
||||
});
|
||||
}
|
||||
showList();
|
After Width: | Height: | Size: 3.0 KiB |
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "agenda",
|
||||
"name": "Agenda",
|
||||
"version": "0.05",
|
||||
"description": "Simple agenda",
|
||||
"icon": "agenda.png",
|
||||
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],
|
||||
"tags": "agenda",
|
||||
"supports": ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"agenda.app.js","url":"agenda.js"},
|
||||
{"name":"agenda.settings.js","url":"settings.js"},
|
||||
{"name":"agenda.img","url":"agenda-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [{"name":"agenda.settings.json"}]
|
||||
}
|
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 3.7 KiB |
|
@ -0,0 +1,48 @@
|
|||
(function(back) {
|
||||
function gbSend(message) {
|
||||
Bluetooth.println("");
|
||||
Bluetooth.println(JSON.stringify(message));
|
||||
}
|
||||
var settings = require("Storage").readJSON("agenda.settings.json",1)||{};
|
||||
function updateSettings() {
|
||||
require("Storage").writeJSON("agenda.settings.json", settings);
|
||||
}
|
||||
var CALENDAR = require("Storage").readJSON("android.calendar.json",true)||[];
|
||||
var mainmenu = {
|
||||
"" : { "title" : "Agenda" },
|
||||
"< Back" : back,
|
||||
/*LANG*/"Connected" : { value : NRF.getSecurityStatus().connected?/*LANG*/"Yes":/*LANG*/"No" },
|
||||
/*LANG*/"Force calendar sync" : () => {
|
||||
if(NRF.getSecurityStatus().connected) {
|
||||
E.showPrompt(/*LANG*/"Do you want to also clear the internal database first?", {
|
||||
buttons: {/*LANG*/"Yes": 1, /*LANG*/"No": 2, /*LANG*/"Cancel": 3}
|
||||
}).then((v)=>{
|
||||
switch(v) {
|
||||
case 1:
|
||||
require("Storage").writeJSON("android.calendar.json",[]);
|
||||
CALENDAR = [];
|
||||
/* falls through */
|
||||
case 2:
|
||||
gbSend({t:"force_calendar_sync", ids: CALENDAR.map(e=>e.id)});
|
||||
E.showAlert(/*LANG*/"Request sent to the phone").then(()=>E.showMenu(mainmenu));
|
||||
break;
|
||||
case 3:
|
||||
default:
|
||||
E.showMenu(mainmenu);
|
||||
return;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
E.showAlert(/*LANG*/"You are not connected").then(()=>E.showMenu(mainmenu));
|
||||
}
|
||||
},
|
||||
/*LANG*/"Show past events" : {
|
||||
value : !!settings.pastEvents,
|
||||
onchange: v => {
|
||||
settings.pastEvents = v;
|
||||
updateSettings();
|
||||
}
|
||||
},
|
||||
};
|
||||
E.showMenu(mainmenu);
|
||||
})
|
|
@ -0,0 +1,2 @@
|
|||
0.01: First, proof of concept
|
||||
0.02: Load AGPS data on app start and automatically in background
|
|
@ -0,0 +1,19 @@
|
|||
# A-GPS Data
|
||||
|
||||
Load assisted GPS (A-GPS) data directly to your Bangle.js using the new http requests on Android GadgetBridge.
|
||||
|
||||
Will download A-GPS data in background (if enabled in settings).
|
||||
|
||||
The GNSS type can be configured in the settings.
|
||||
|
||||
Make sure:
|
||||
* your GadgetBridge version supports http requests
|
||||
* turn on internet access in GadgetBridge settings
|
||||
|
||||
Currently proof of concept on Bangle.js 2 only.
|
||||
|
||||
## Creator
|
||||
[@pidajo](https://github.com/pidajo)
|
||||
|
||||
## Contributor
|
||||
[@myxor](https://github.com/myxor)
|
|
@ -0,0 +1 @@
|
|||
atob("MDCEAAAAAAAAAAAAAAAAiIgAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiAAAAAAAAAAAAAAAAAAAAAAAAAAAAIiIiAAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAACIiIiIAAAAAAAAAAAAAAAAAAAAAAAAAAiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAAiIiIiIgAAAAAAAAAAAAAAAAAAAAAAAAIiIOIiIiAAAAAAAAAAAAAAAAAAAAAAAAIiDOIiIiAAAAAAAAAAAAAAAAAAAAAAAAIiDOIiIiIAAAAAAAAAAAAAAAAAAAAAACIiPOIiIiIAAAAAAAAAAAAAAAAAAAAAAiIj/OIiIiIgAAAAAAAAAAAAAAAAAAAAAiI//OIiIiIgAAAAAAAAAAAAAAAAAAAAAiI//OIiIiIiAAAAAAAAAAAAAAAAAAAAAiD//OIiIiIiAAAAAAAAAAAAAAAAAAAAIiP//OIiIiIiAAAAAAAAAAAAAAAAAAAAIg///OIiIiIiIAAAAAAAAAAAAAAAAAACIj///OIiIiIiIAAAAAAAAAAAAAAAAAACIP///OIiIiIiIgAAAAAAAAAAAAAAAAACI////OIiIiIiIgAAAAAAAAAAAAAAAAAiD////OIiIiIiIiAAAAAAAAAAAAAAAAAiP////OIiIiIiIiAAAAAAAAAAAAAAAAIiP////OIiIiIiIiIAAAAAAAAAAAAAAAIj/////OIiIiIiIiIAAAAAAAAAAAAAACIj/////OIiIiIiIiIgAAAAAAAAAAAAACI//////OIiIiIiIiIgAAAAAAAAAAAAAiI//////OIiIiIiIiIgAAAAAAAAAAAAAiIiIiIiIgzMzMzMziIiAAAAAAAAAAAAAiIiIiIiIj///////+IiAAAAAAAAAAAAIiIiIiIiIj////////4iIAAAAAAAAAAAIiIiIiIiIgzMzMzMzM4iIAAAAAAAAAACIP///////OIiIiIiIiIiIgAAAAAAAAACI////////OIiIiIiIiIiIgAAAAAAAAAiI////////OIiIiIiIiIiIiAAAAAAAAAiP////////OIiIiIiIiIiIiAAAAAAAAIiP////////OIiIiIiIiIiIiIAAAAAAAIj/////////OIiIiIiIiIiIiIAAAAAACIj////////ziIiIiIiIiIiIiIgAAAAACIP///////+IiIiIiIiIiIiIiIgAAAAACI//////84gYiIiIiIiIiIiIiIgAAAAAiD////84iIiAAAiIiIiIiIiIiIiAAAAAiP///ziIiIAAAAAIiIiIiIiIiIiAAAAIg/8ziIiIAAAAAAAAAIiIiIiIiIiIAAAIj/iIiIAAAAAAAAAAAAAIiIiIiIiIAACIiIiBgAAAAAAAAAAAAAAACIiIiIiIgACIiIgAAAAAAAAAAAAAAAAAAACIiIiIgACIgAAAAAAAAAAAAAAAAAAAAAAAiIiIgA==")
|
After Width: | Height: | Size: 1.6 KiB |
|
@ -0,0 +1,54 @@
|
|||
function display(text1, text2) {
|
||||
g.reset();
|
||||
g.clear();
|
||||
var img = require("Storage").read("agpsdata.img");
|
||||
if (img) {
|
||||
g.drawImage(img, g.getWidth() - 48, g.getHeight() - 48 - 24);
|
||||
}
|
||||
g.setFont("Vector", 18);
|
||||
g.setFontAlign(0, 1);
|
||||
g.drawString(text1, g.getWidth() / 2, g.getHeight() / 3 + 24);
|
||||
if (text2 != undefined) {
|
||||
g.setFont("Vector", 12);
|
||||
g.setFontAlign(-1, -1);
|
||||
g.drawString(text2, 5, g.getHeight() / 3 + 29);
|
||||
}
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
// Load widgets
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
let waiting = false;
|
||||
|
||||
function start() {
|
||||
g.reset();
|
||||
g.clear();
|
||||
waiting = false;
|
||||
display("Retry?", "touch to retry");
|
||||
Bangle.on("touch", () => { updateAgps(); });
|
||||
}
|
||||
|
||||
function updateAgps() {
|
||||
g.reset();
|
||||
g.clear();
|
||||
if (!waiting) {
|
||||
waiting = true;
|
||||
display("Updating A-GPS...");
|
||||
require("agpsdata").pull(function() {
|
||||
waiting = false;
|
||||
display("A-GPS updated.", "touch to close");
|
||||
Bangle.on("touch", () => { load(); });
|
||||
},
|
||||
function(error) {
|
||||
waiting = false;
|
||||
E.showAlert(error, "Error")
|
||||
.then(() => { start(); });
|
||||
});
|
||||
} else {
|
||||
display("Waiting...");
|
||||
}
|
||||
}
|
||||
updateAgps();
|
|
@ -0,0 +1,33 @@
|
|||
(function() {
|
||||
let waiting = false;
|
||||
let settings = require("Storage").readJSON("agpsdata.settings.json", 1) || {
|
||||
enabled: true,
|
||||
refresh: 1440
|
||||
};
|
||||
|
||||
if (settings.refresh == undefined) settings.refresh = 1440;
|
||||
|
||||
function successCallback(){
|
||||
waiting = false;
|
||||
}
|
||||
|
||||
function errorCallback(){
|
||||
waiting = false;
|
||||
}
|
||||
|
||||
if (settings.enabled) {
|
||||
let lastUpdate = settings.lastUpdate;
|
||||
if (!lastUpdate || lastUpdate + settings.refresh * 1000 * 60 < Date.now()){
|
||||
if (!waiting){
|
||||
waiting = true;
|
||||
require("agpsdata").pull(successCallback, errorCallback);
|
||||
}
|
||||
}
|
||||
setInterval(() => {
|
||||
if (!waiting && NRF.getSecurityStatus().connected){
|
||||
waiting = true;
|
||||
require("agpsdata").pull(successCallback, errorCallback);
|
||||
}
|
||||
}, settings.refresh * 1000 * 60);
|
||||
}
|
||||
})();
|
|
@ -0,0 +1 @@
|
|||
{"enabled":true,"refresh":1440,"gnsstype":1}
|
|
@ -0,0 +1,75 @@
|
|||
function readSettings() {
|
||||
settings = Object.assign(
|
||||
require('Storage').readJSON("agpsdata.default.json", true) || {},
|
||||
require('Storage').readJSON(FILE, true) || {});
|
||||
}
|
||||
|
||||
var FILE = "agpsdata.settings.json";
|
||||
var settings;
|
||||
readSettings();
|
||||
|
||||
function setAGPS(data) {
|
||||
var js = jsFromBase64(data);
|
||||
try {
|
||||
eval(js);
|
||||
return true;
|
||||
}
|
||||
catch(e) {
|
||||
console.log("error:", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function jsFromBase64(b64) {
|
||||
var bin = atob(b64);
|
||||
var chunkSize = 128;
|
||||
var js = "Bangle.setGPSPower(1);\n"; // turn GPS on
|
||||
var gnsstype = settings.gnsstype || 1; // default GPS
|
||||
js += `Serial1.println("${CASIC_CHECKSUM("$PCAS04,"+gnsstype)}")\n`; // set GNSS mode
|
||||
// What about:
|
||||
// NAV-TIMEUTC (0x01 0x10)
|
||||
// NAV-PV (0x01 0x03)
|
||||
// or AGPS.zip uses AID-INI (0x0B 0x01)
|
||||
|
||||
for (var i=0;i<bin.length;i+=chunkSize) {
|
||||
var chunk = bin.substr(i,chunkSize);
|
||||
js += `Serial1.write(atob("${btoa(chunk)}"))\n`;
|
||||
}
|
||||
return js;
|
||||
}
|
||||
|
||||
function CASIC_CHECKSUM(cmd) {
|
||||
var cs = 0;
|
||||
for (var i=1;i<cmd.length;i++)
|
||||
cs = cs ^ cmd.charCodeAt(i);
|
||||
return cmd+"*"+cs.toString(16).toUpperCase().padStart(2, '0');
|
||||
}
|
||||
|
||||
function updateLastUpdate() {
|
||||
const file = "agpsdata.json";
|
||||
let data = require("Storage").readJSON(file, 1) || {};
|
||||
data.lastUpdate = Math.round(Date.now());
|
||||
require("Storage").writeJSON(file, data);
|
||||
}
|
||||
|
||||
exports.pull = function(successCallback, failureCallback) {
|
||||
let uri = "https://www.espruino.com/agps/casic.base64";
|
||||
if (Bangle.http){
|
||||
Bangle.http(uri, {timeout:10000}).then(event => {
|
||||
let result = setAGPS(event.resp);
|
||||
if (result) {
|
||||
updateLastUpdate();
|
||||
if (successCallback) successCallback();
|
||||
} else {
|
||||
console.log("error applying AGPS data");
|
||||
if (failureCallback) failureCallback("Error applying AGPS data");
|
||||
}
|
||||
}).catch((e)=>{
|
||||
console.log("error", e);
|
||||
if (failureCallback) failureCallback(e);
|
||||
});
|
||||
} else {
|
||||
console.log("error: No http method found");
|
||||
if (failureCallback) failureCallback(/*LANG*/"No http method");
|
||||
}
|
||||
};
|
|
@ -0,0 +1,24 @@
|
|||
{ "id": "agpsdata",
|
||||
"name": "A-GPS Data",
|
||||
"shortName":"A-GPS Data",
|
||||
"icon": "agpsdata.png",
|
||||
"version":"0.02",
|
||||
"description": "Download assisted GPS (A-GPS) data directly to your Bangle.js **using Gadgetbridge**",
|
||||
"tags": "boot,tool,assisted,gps,agps,http",
|
||||
"allow_emulator":true,
|
||||
"supports": ["BANGLEJS2"],
|
||||
"readme":"README.md",
|
||||
"screenshots" : [ { "url":"screenshot.png" }, { "url":"screenshot2.png" } ],
|
||||
"storage": [
|
||||
{"name":"agpsdata.app.js","url":"app.js"},
|
||||
{"name":"agpsdata.img","url":"agpsdata-icon.js","evaluate":true},
|
||||
{"name":"agpsdata.default.json","url":"default.json"},
|
||||
{"name":"agpsdata.boot.js","url":"boot.js"},
|
||||
{"name":"agpsdata","url":"lib.js"},
|
||||
{"name":"agpsdata.settings.js","url":"settings.js"}
|
||||
],
|
||||
"data": [
|
||||
{"name": "agpsdata.json"},
|
||||
{"name": "agpsdata.settings.json"}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 2.4 KiB |
After Width: | Height: | Size: 2.6 KiB |
|
@ -0,0 +1,71 @@
|
|||
(function(back) {
|
||||
function writeSettings(key, value) {
|
||||
var s = Object.assign(
|
||||
require('Storage').readJSON(settingsDefaultFile, true) || {},
|
||||
require('Storage').readJSON(settingsFile, true) || {});
|
||||
s[key] = value;
|
||||
require('Storage').writeJSON(settingsFile, s);
|
||||
readSettings();
|
||||
}
|
||||
|
||||
function readSettings() {
|
||||
settings = Object.assign(
|
||||
require('Storage').readJSON(settingsDefaultFile, true) || {},
|
||||
require('Storage').readJSON(settingsFile, true) || {});
|
||||
}
|
||||
|
||||
var settingsFile = "agpsdata.settings.json";
|
||||
var settingsDefaultFile = "agpsdata.default.json";
|
||||
|
||||
var settings;
|
||||
readSettings();
|
||||
|
||||
const gnsstypes = [
|
||||
"", "GPS", "BDS", "GPS+BDS", "GLONASS", "GPS+GLONASS", "BDS+GLONASS",
|
||||
"GPS+BDS+GLON."
|
||||
];
|
||||
|
||||
function buildMainMenu() {
|
||||
var mainmenu = {
|
||||
'' : {'title' : 'AGPS download'},
|
||||
'< Back' : back,
|
||||
"Enabled" : {
|
||||
value : !!settings.enabled,
|
||||
onchange : v => { writeSettings("enabled", v); }
|
||||
},
|
||||
"Refresh every" : {
|
||||
value : settings.refresh / 60,
|
||||
min : 1,
|
||||
max : 168,
|
||||
step : 1,
|
||||
format : v => v + "h",
|
||||
onchange : v => { writeSettings("refresh", Math.round(v * 60)); }
|
||||
},
|
||||
"GNSS type" : {
|
||||
value : settings.gnsstype,
|
||||
min : 1,
|
||||
max : 7,
|
||||
step : 1,
|
||||
format : v => gnsstypes[v],
|
||||
onchange : x => writeSettings('gnsstype', x)
|
||||
},
|
||||
"Force refresh" : () => {
|
||||
E.showMessage("Loading A-GPS data");
|
||||
require("agpsdata")
|
||||
.pull(
|
||||
function() {
|
||||
E.showAlert("Success").then(
|
||||
() => { E.showMenu(buildMainMenu()); });
|
||||
},
|
||||
function(error) {
|
||||
E.showAlert(error, "Error")
|
||||
.then(() => { E.showMenu(buildMainMenu()); });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return mainmenu;
|
||||
}
|
||||
|
||||
E.showMenu(buildMainMenu());
|
||||
});
|
|
@ -24,3 +24,14 @@
|
|||
0.23: Fix regression with Days of Week (#1735)
|
||||
0.24: Automatically save the alarm/timer when the user returns to the main menu using the back arrow
|
||||
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
|
||||
0.30: Fix "Enable All"
|
||||
0.31: Add seconds to timers
|
||||
0.32: Fix wrong hidden filter
|
||||
Add option for auto-delete a timer after it expires
|
||||
0.33: Allow hiding timers&alarms
|
||||
|
||||
|
|
|
@ -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,240 +1,386 @@
|
|||
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)
|
||||
let alarms = require("sched").getAlarms();
|
||||
var alarms = require("sched").getAlarms();
|
||||
|
||||
function getCurrentTime() {
|
||||
let time = new Date();
|
||||
return (
|
||||
time.getHours() * 3600000 +
|
||||
time.getMinutes() * 60000 +
|
||||
time.getSeconds() * 1000
|
||||
);
|
||||
function handleFirstDayOfWeek(dow) {
|
||||
if (firstDayOfWeek == 1) {
|
||||
if ((dow & 1) == 1) {
|
||||
// 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 saveAndReload() {
|
||||
require("sched").setAlarms(alarms);
|
||||
require("sched").reload();
|
||||
}
|
||||
// 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() {
|
||||
// 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)
|
||||
"": { "title": /*LANG*/"Alarms & Timers" },
|
||||
"< Back": () => load(),
|
||||
/*LANG*/"New...": () => showNewMenu()
|
||||
};
|
||||
alarms.forEach((alarm,idx)=>{
|
||||
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() {
|
||||
if (alarm.timer) editTimer(idx, alarm);
|
||||
else editAlarm(idx, alarm);
|
||||
}
|
||||
|
||||
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)
|
||||
};
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
menu[/*LANG*/"Advanced"] = () => showAdvancedMenu();
|
||||
|
||||
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)
|
||||
};
|
||||
for (let i = 0; i < 7; i++) (i => {
|
||||
let dayOfWeek = require("locale").dow({ getDay: () => i });
|
||||
menu[dayOfWeek] = {
|
||||
value: !!(dow&(1<<i)),
|
||||
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
|
||||
onchange: v => v ? dow |= 1<<i : dow &= ~(1<<i),
|
||||
};
|
||||
})(i);
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function editAlarm(alarmIndex, alarm) {
|
||||
let newAlarm = alarmIndex < 0;
|
||||
let a = require("sched").newDefaultAlarm();
|
||||
if (!newAlarm) Object.assign(a, alarms[alarmIndex]);
|
||||
if (alarm) Object.assign(a,alarm);
|
||||
let t = require("sched").decodeTime(a.t);
|
||||
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': /*LANG*/'Alarm' },
|
||||
/*LANG*/'< Back': () => {
|
||||
saveAlarm(newAlarm, alarmIndex, a, t);
|
||||
"": { "title": isNew ? /*LANG*/"New Alarm" : /*LANG*/"Edit Alarm" },
|
||||
"< Back": () => {
|
||||
prepareAlarmForSave(alarm, alarmIndex, time);
|
||||
saveAndReload();
|
||||
showMainMenu();
|
||||
},
|
||||
/*LANG*/'Hours': {
|
||||
value: t.hrs, min : 0, max : 23, wrap : true,
|
||||
onchange: v => t.hrs=v
|
||||
/*LANG*/"Hour": {
|
||||
value: time.h,
|
||||
format: v => ("0" + v).substr(-2),
|
||||
min: 0,
|
||||
max: 23,
|
||||
wrap: true,
|
||||
onchange: v => time.h = v
|
||||
},
|
||||
/*LANG*/'Minutes': {
|
||||
value: t.mins, min : 0, max : 59, wrap : true,
|
||||
onchange: v => t.mins=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: a.on,
|
||||
format: v => v ? /*LANG*/"On" : /*LANG*/"Off",
|
||||
onchange: v=>a.on=v
|
||||
/*LANG*/"Enabled": {
|
||||
value: alarm.on,
|
||||
onchange: v => alarm.on = v
|
||||
},
|
||||
/*LANG*/'Repeat': {
|
||||
value: a.rp,
|
||||
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
|
||||
onchange: v => a.rp = v
|
||||
},
|
||||
/*LANG*/'Days': {
|
||||
value: "SMTWTFS".split("").map((d,n)=>a.dow&(1<<n)?d:".").join(""),
|
||||
onchange: () => editDOW(a.dow, d => {
|
||||
a.dow = d;
|
||||
a.t = require("sched").encodeTime(t);
|
||||
editAlarm(alarmIndex, a);
|
||||
/*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(a.vibrate, v => a.vibrate=v ),
|
||||
/*LANG*/'Auto Snooze': {
|
||||
value: a.as,
|
||||
format: v => v ? /*LANG*/"Yes" : /*LANG*/"No",
|
||||
onchange: v => a.as = v
|
||||
/*LANG*/"Vibrate": require("buzz_menu").pattern(alarm.vibrate, v => alarm.vibrate = v),
|
||||
/*LANG*/"Auto Snooze": {
|
||||
value: alarm.as,
|
||||
onchange: v => alarm.as = v
|
||||
},
|
||||
/*LANG*/"Hidden": {
|
||||
value: alarm.hidden || false,
|
||||
onchange: v => alarm.hidden = 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 prepareAlarmForSave(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;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
};
|
||||
|
||||
menu[/*LANG*/"Cancel"] = () => showMainMenu();
|
||||
|
||||
if (!newAlarm) {
|
||||
menu[/*LANG*/"Delete"] = function () {
|
||||
alarms.splice(alarmIndex, 1);
|
||||
saveAndReload();
|
||||
showMainMenu();
|
||||
};
|
||||
}
|
||||
|
||||
return E.showMenu(menu);
|
||||
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) {
|
||||
let newAlarm = alarmIndex < 0;
|
||||
let a = require("sched").newDefaultTimer();
|
||||
if (!newAlarm) Object.assign(a, alarms[alarmIndex]);
|
||||
if (alarm) Object.assign(a,alarm);
|
||||
let t = require("sched").decodeTime(a.timer);
|
||||
|
||||
function showCustomDaysMenu(dow, dowChangeCallback, originalRepeat, originalDow) {
|
||||
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 ),
|
||||
"": { "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)
|
||||
}
|
||||
};
|
||||
|
||||
menu[/*LANG*/"Cancel"] = () => showMainMenu();
|
||||
|
||||
if (!newAlarm) {
|
||||
menu[/*LANG*/"Delete"] = function() {
|
||||
alarms.splice(alarmIndex,1);
|
||||
saveAndReload();
|
||||
showMainMenu();
|
||||
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)))
|
||||
};
|
||||
}
|
||||
return E.showMenu(menu);
|
||||
});
|
||||
|
||||
menu[/*LANG*/"Cancel"] = () => setTimeout(showEditRepeatMenu, 10, originalRepeat, originalDow, dowChangeCallback)
|
||||
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
||||
function saveTimer(newAlarm, alarmIndex, a, t) {
|
||||
a.timer = require("sched").encodeTime(t);
|
||||
a.t = getCurrentTime() + a.timer;
|
||||
a.last = 0;
|
||||
function showEditTimerMenu(selectedTimer, timerIndex) {
|
||||
var isNew = timerIndex === undefined;
|
||||
|
||||
if (newAlarm) {
|
||||
alarms.push(a);
|
||||
} else {
|
||||
alarms[alarmIndex] = a;
|
||||
var timer = require("sched").newDefaultTimer();
|
||||
|
||||
if (selectedTimer) {
|
||||
Object.assign(timer, selectedTimer);
|
||||
}
|
||||
|
||||
saveAndReload();
|
||||
var time = require("time_utils").decodeTime(timer.timer);
|
||||
|
||||
const menu = {
|
||||
"": { "title": isNew ? /*LANG*/"New Timer" : /*LANG*/"Edit Timer" },
|
||||
"< Back": () => {
|
||||
prepareTimerForSave(timer, timerIndex, time);
|
||||
saveAndReload();
|
||||
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*/"Seconds": {
|
||||
value: time.s,
|
||||
min: 0,
|
||||
max: 59,
|
||||
step: 1,
|
||||
wrap: true,
|
||||
onchange: v => time.s = v
|
||||
},
|
||||
/*LANG*/"Enabled": {
|
||||
value: timer.on,
|
||||
onchange: v => timer.on = v
|
||||
},
|
||||
/*LANG*/"Delete After Expiration": {
|
||||
value: timer.del,
|
||||
onchange: v => timer.del = v
|
||||
},
|
||||
/*LANG*/"Hidden": {
|
||||
value: timer.hidden || false,
|
||||
onchange: v => timer.hidden = v
|
||||
},
|
||||
/*LANG*/"Vibrate": require("buzz_menu").pattern(timer.vibrate, v => timer.vibrate = v),
|
||||
/*LANG*/"Cancel": () => showMainMenu()
|
||||
};
|
||||
|
||||
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 prepareTimerForSave(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;
|
||||
}
|
||||
}
|
||||
|
||||
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, i) => {
|
||||
alarm.on = on;
|
||||
if (on) {
|
||||
if (alarm.timer) {
|
||||
prepareTimerForSave(alarm, i, require("time_utils").decodeTime(alarm.timer))
|
||||
} else {
|
||||
prepareAlarmForSave(alarm, i, require("time_utils").decodeTime(alarm.t))
|
||||
}
|
||||
}
|
||||
});
|
||||
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.24",
|
||||
"version": "0.33",
|
||||
"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 |
|
@ -2,7 +2,7 @@ WIDGETS["alarm"]={area:"tl",width:0,draw:function() {
|
|||
if (this.width) g.reset().drawImage(atob("GBgBAAAAAAAAABgADhhwDDwwGP8YGf+YMf+MM//MM//MA//AA//AA//AA//AA//AA//AB//gD//wD//wAAAAADwAABgAAAAAAAAA"),this.x,this.y);
|
||||
},reload:function() {
|
||||
// don't include library here as we're trying to use as little RAM as possible
|
||||
WIDGETS["alarm"].width = (require('Storage').readJSON('sched.json',1)||[]).some(alarm=>alarm.on&&(alarm.hidden!==false)) ? 24 : 0;
|
||||
WIDGETS["alarm"].width = (require('Storage').readJSON('sched.json',1)||[]).some(alarm=>alarm.on&&(alarm.hidden!==true)) ? 24 : 0;
|
||||
}
|
||||
};
|
||||
WIDGETS["alarm"].reload();
|
||||
|
|
|
@ -7,3 +7,9 @@
|
|||
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
|
||||
0.12: Use default Bangle formatter for booleans
|
||||
0.13: Added Bangle.http function (see Readme file for more info)
|
||||
0.14: Fix timeout of http function not being cleaned up
|
||||
0.15: Allow method/body/headers to be specified for `http` (needs Gadgetbridge 0.68.0b or later)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
@ -33,6 +32,25 @@ Responses are sent back to Gadgetbridge simply as one line of JSON.
|
|||
|
||||
More info on message formats on http://www.espruino.com/Gadgetbridge
|
||||
|
||||
## Functions provided
|
||||
|
||||
The boot code also provides some useful functions:
|
||||
|
||||
* `Bangle.messageResponse = function(msg,response)` - send a yes/no response to a message. `msg` is a message object, and `response` is a boolean.
|
||||
* `Bangle.musicControl = function(cmd)` - control music, cmd = `play/pause/next/previous/volumeup/volumedown`
|
||||
* `Bangle.http = function(url,options)` - make an HTTPS request to a URL and return a promise with the data. Requires the [internet enabled `Bangle.js Gadgetbridge` app](http://www.espruino.com/Gadgetbridge#http-requests). `options` can contain:
|
||||
* `id` - a custom (string) ID
|
||||
* `timeout` - a timeout for the request in milliseconds (default 30000ms)
|
||||
* `xpath` an xPath query to run on the request (but right now the URL requested must be XML - HTML is rarely XML compliant)
|
||||
|
||||
eg:
|
||||
|
||||
```
|
||||
Bangle.http("https://pur3.co.uk/hello.txt").then(data=>{
|
||||
console.log("Got ",data);
|
||||
});
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Bangle.js can only hold one connection open at a time, so it's hard to see
|
||||
|
|
|
@ -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,26 +78,93 @@
|
|||
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);
|
||||
sched.reload();
|
||||
},
|
||||
//TODO perhaps move those in a library (like messages), used also for viewing events?
|
||||
//simple package with events all together
|
||||
"calendarevents" : function() {
|
||||
require("Storage").writeJSON("android.calendar.json", event.events);
|
||||
},
|
||||
//add and remove events based on activity on phone (pebble-like)
|
||||
"calendar" : function() {
|
||||
var cal = require("Storage").readJSON("android.calendar.json",true);
|
||||
if (!cal || !Array.isArray(cal)) cal = [];
|
||||
var i = cal.findIndex(e=>e.id==event.id);
|
||||
if(i<0)
|
||||
cal.push(event);
|
||||
else
|
||||
cal[i] = event;
|
||||
require("Storage").writeJSON("android.calendar.json", cal);
|
||||
},
|
||||
"calendar-" : function() {
|
||||
var cal = require("Storage").readJSON("android.calendar.json",true);
|
||||
//if any of those happen we are out of sync!
|
||||
if (!cal || !Array.isArray(cal)) return;
|
||||
cal = cal.filter(e=>e.id!=event.id);
|
||||
require("Storage").writeJSON("android.calendar.json", cal);
|
||||
},
|
||||
//triggered by GB, send all ids
|
||||
"force_calendar_sync_start" : function() {
|
||||
var cal = require("Storage").readJSON("android.calendar.json",true);
|
||||
if (!cal || !Array.isArray(cal)) cal = [];
|
||||
gbSend({t:"force_calendar_sync", ids: cal.map(e=>e.id)});
|
||||
},
|
||||
"http":function() {
|
||||
//get the promise and call the promise resolve
|
||||
if (Bangle.httpRequest === undefined) return;
|
||||
var request=Bangle.httpRequest[event.id];
|
||||
if (request === undefined) return; //already timedout or wrong id
|
||||
delete Bangle.httpRequest[event.id];
|
||||
clearTimeout(request.t); //t = timeout variable
|
||||
if(event.err!==undefined) //if is error
|
||||
request.j(event.err); //r = reJect function
|
||||
else
|
||||
request.r(event); //r = resolve function
|
||||
}
|
||||
};
|
||||
var h = HANDLERS[event.t];
|
||||
if (h) h(); else console.log("GB Unknown",event);
|
||||
};
|
||||
// HTTP request handling - see the readme
|
||||
// options = {id,timeout,xpath}
|
||||
Bangle.http = (url,options)=>{
|
||||
options = options||{};
|
||||
if (Bangle.httpRequest === undefined)
|
||||
Bangle.httpRequest={};
|
||||
if (options.id === undefined) {
|
||||
// try and create a unique ID
|
||||
do {
|
||||
options.id = Math.random().toString().substr(2);
|
||||
} while( Bangle.httpRequest[options.id]!==undefined);
|
||||
}
|
||||
//send the request
|
||||
var req = {t: "http", url:url, id:options.id};
|
||||
if (options.xpath) req.xpath = options.xpath;
|
||||
if (options.method) req.method = options.method;
|
||||
if (options.body) req.body = options.body;
|
||||
if (options.headers) req.headers = options.headers;
|
||||
gbSend(req);
|
||||
//create the promise
|
||||
var promise = new Promise(function(resolve,reject) {
|
||||
//save the resolve function in the dictionary and create a timeout (30 seconds default)
|
||||
Bangle.httpRequest[options.id]={r:resolve,j:reject,t:setTimeout(()=>{
|
||||
//if after "timeoutMillisec" it still hasn't answered -> reject
|
||||
delete Bangle.httpRequest[options.id];
|
||||
reject("Timeout");
|
||||
},options.timeout||30000)};
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
|
||||
// Battery monitor
|
||||
function sendBattery() { gbSend({ t: "status", bat: E.getBattery(), chg: Bangle.isCharging()?1:0 }); }
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "android",
|
||||
"name": "Android Integration",
|
||||
"shortName": "Android",
|
||||
"version": "0.08",
|
||||
"version": "0.15",
|
||||
"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",
|
||||
|
@ -15,6 +15,6 @@
|
|||
{"name":"android.img","url":"app-icon.js","evaluate":true},
|
||||
{"name":"android.boot.js","url":"boot.js"}
|
||||
],
|
||||
"data": [{"name":"android.settings.json"}],
|
||||
"data": [{"name":"android.settings.json"}, {"name":"android.calendar.json"}],
|
||||
"sortorder": -8
|
||||
}
|
||||
|
|
|
@ -18,34 +18,12 @@
|
|||
}),
|
||||
/*LANG*/"Keep Msgs" : {
|
||||
value : !!settings.keep,
|
||||
format : v=>v?/*LANG*/"Yes":/*LANG*/"No",
|
||||
onchange: v => {
|
||||
settings.keep = v;
|
||||
updateSettings();
|
||||
}
|
||||
},
|
||||
/*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);
|
||||
})
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
0.01: New App!
|
||||
0.02: Fix bug if image clock wasn't installed
|
||||
0.03: Update to use setUI
|
||||
0.04: Tell clock widgets to hide. Move loadWidgets() so it only runs on
|
||||
startup and not on every draw.
|
||||
|
|
|
@ -87,7 +87,6 @@ if (g.drawImages) {
|
|||
draw();
|
||||
var secondInterval = setInterval(draw,100);
|
||||
// load widgets
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
// Stop when LCD goes off
|
||||
Bangle.on('lcdPower',on=>{
|
||||
|
@ -104,3 +103,5 @@ if (g.drawImages) {
|
|||
}
|
||||
// Show launcher when button pressed
|
||||
Bangle.setUI("clock");
|
||||
|
||||
Bangle.loadWidgets();
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "animclk",
|
||||
"name": "Animated Clock",
|
||||
"shortName": "Anim Clock",
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "An animated clock face using Mark Ferrari's amazing 8 bit game art and palette cycling: http://www.markferrari.com/art/8bit-game-art",
|
||||
"icon": "app.png",
|
||||
"type": "clock",
|
||||
|
|
|
@ -10,3 +10,4 @@
|
|||
week is buffered until date or timezone changes
|
||||
0.07: align default settings with app.js (otherwise the initial displayed settings will be confusing to users)
|
||||
0.08: fixed calendar weeknumber not shortened to two digits
|
||||
0.09: Use default Bangle formatter for booleans
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "antonclk",
|
||||
"name": "Anton Clock",
|
||||
"version": "0.08",
|
||||
"version": "0.09",
|
||||
"description": "A clock using the bold Anton font, optionally showing seconds and date in ISO-8601 format.",
|
||||
"readme":"README.md",
|
||||
"icon": "app.png",
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
(function(back) {
|
||||
var FILE = "antonclk.json";
|
||||
// Load settings
|
||||
var settings = Object.assign({
|
||||
secondsOnUnlock: false,
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
@ -41,7 +40,6 @@
|
|||
"Date": stringInSettings("dateOnMain", ["Long", "Short", "ISO8601"]),
|
||||
"Show Weekday": {
|
||||
value: (settings.weekDay !== undefined ? settings.weekDay : true),
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.weekDay = v;
|
||||
writeSettings();
|
||||
|
@ -49,7 +47,6 @@
|
|||
},
|
||||
"Show CalWeek": {
|
||||
value: (settings.calWeek !== undefined ? settings.calWeek : false),
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.calWeek = v;
|
||||
writeSettings();
|
||||
|
@ -57,7 +54,6 @@
|
|||
},
|
||||
"Uppercase": {
|
||||
value: (settings.upperCase !== undefined ? settings.upperCase : true),
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.upperCase = v;
|
||||
writeSettings();
|
||||
|
@ -65,7 +61,6 @@
|
|||
},
|
||||
"Vector font": {
|
||||
value: (settings.vectorFont !== undefined ? settings.vectorFont : false),
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.vectorFont = v;
|
||||
writeSettings();
|
||||
|
@ -82,7 +77,6 @@
|
|||
"Show": stringInSettings("secondsMode", ["Never", "Unlocked", "Always"]),
|
||||
"With \":\"": {
|
||||
value: (settings.secondsWithColon !== undefined ? settings.secondsWithColon : true),
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.secondsWithColon = v;
|
||||
writeSettings();
|
||||
|
@ -90,7 +84,6 @@
|
|||
},
|
||||
"Color": {
|
||||
value: (settings.secondsColoured !== undefined ? settings.secondsColoured : true),
|
||||
format: v => v ? "On" : "Off",
|
||||
onchange: v => {
|
||||
settings.secondsColoured = v;
|
||||
writeSettings();
|
||||
|
@ -99,9 +92,6 @@
|
|||
"Date": stringInSettings("dateOnSecs", ["Year", "Weekday", "No"])
|
||||
};
|
||||
|
||||
// Actually display the menu
|
||||
E.showMenu(mainmenu);
|
||||
|
||||
});
|
||||
|
||||
// end of file
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
0.01: Create astral clock app
|
||||
0.02: Fixed Whirlpool galaxy RA/DA, larger compass display, fixed moonphase overlapping battery widget
|
||||
0.03: Update to use Bangle.setUI instead of setWatch
|
||||
0.04: Tell clock widgets to hide.
|
||||
|
|
|
@ -767,6 +767,24 @@ function draw() {
|
|||
g.clear();
|
||||
current_moonphase = getMoonPhase();
|
||||
|
||||
Bangle.setUI("clockupdown", btn => {
|
||||
if (btn==0) {
|
||||
if (!processing) {
|
||||
if (!modeswitch) {
|
||||
modeswitch = true;
|
||||
if (mode == "planetary") mode = "extras";
|
||||
else mode = "planetary";
|
||||
}
|
||||
else
|
||||
modeswitch = false;
|
||||
}
|
||||
} else {
|
||||
if (!processing)
|
||||
ready_to_compute = true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Load widgets
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
@ -799,23 +817,6 @@ Bangle.setGPSPower(1);
|
|||
// Show launcher when button pressed
|
||||
Bangle.setClockMode();
|
||||
|
||||
Bangle.setUI("clockupdown", btn => {
|
||||
if (btn==0) {
|
||||
if (!processing) {
|
||||
if (!modeswitch) {
|
||||
modeswitch = true;
|
||||
if (mode == "planetary") mode = "extras";
|
||||
else mode = "planetary";
|
||||
}
|
||||
else
|
||||
modeswitch = false;
|
||||
}
|
||||
} else {
|
||||
if (!processing)
|
||||
ready_to_compute = true;
|
||||
}
|
||||
});
|
||||
|
||||
setWatch(function () {
|
||||
if (!astral_settings.astral_default) {
|
||||
colours_switched = true;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "astral",
|
||||
"name": "Astral Clock",
|
||||
"version": "0.03",
|
||||
"version": "0.04",
|
||||
"description": "Clock that calculates and displays Alt Az positions of all planets, Sun as well as several other astronomy targets (customizable) and current Moon phase. Coordinates are calculated by GPS & time and onscreen compass assists orienting. See Readme before using.",
|
||||
"icon": "app-icon.png",
|
||||
"type": "clock",
|
||||
|
|
|
@ -7,3 +7,9 @@
|
|||
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
|
||||
0.12: Add settings to hide date,widgets
|
||||
0.13: Add font setting
|
||||
0.14: Use ClockFace_menu.addItems
|
||||
0.15: Add Power saving option
|
|
@ -4,3 +4,8 @@ A simple digital clock showing seconds as a horizontal bar.
|
|||
| 24hr style | 12hr style |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
## Settings
|
||||
* `Show date`: display date at the bottom of screen
|
||||
* `Font`: choose between bitmap or vector fonts
|
||||
* `Power saving`: (Bangle.js 2 only) don't draw the seconds bar while the watch is locked
|
|
@ -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,51 +10,25 @@ 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();
|
||||
|
||||
let barW = 0, prevX = 0;
|
||||
function renderBar(l) {
|
||||
if (!this.fraction) {
|
||||
// zero-size fillRect stills draws one line of pixels, we don't want that
|
||||
return;
|
||||
}
|
||||
const width = this.fraction*l.w;
|
||||
g.fillRect(l.x, l.y, l.x+width-1, l.y+l.height-1);
|
||||
"ram";
|
||||
if (l) prevX = 0; // called from Layout: drawing area was cleared
|
||||
else l = clock.layout.bar;
|
||||
let x2 = l.x+barW;
|
||||
if (clock.powerSave && Bangle.isLocked()) x2 = 0; // hide bar
|
||||
if (x2===prevX) return; // nothing to do
|
||||
if (x2===0) x2--; // don't leave 1px line
|
||||
if (x2<Math.max(0, prevX)) g.setBgColor(l.bgCol || g.theme.bg).clearRect(x2+1, l.y, prevX, l.y2);
|
||||
else g.setColor(l.col || g.theme.fg).fillRect(prevX+1, l.y, x2, l.y2);
|
||||
prevX = x2;
|
||||
}
|
||||
|
||||
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 +41,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 +51,74 @@ 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);
|
||||
};
|
||||
const ClockFace = require("ClockFace"),
|
||||
clock = new ClockFace({
|
||||
precision: 1,
|
||||
settingsFile: "barclock.settings.json",
|
||||
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}, // updated below
|
||||
{id: "ampm", label: " ", type: "txt", font: "6x8:2", col: g.theme.fg, bgCol: g.theme.bg},
|
||||
],
|
||||
},
|
||||
{id: "bar", type: "custom", fillx: 1, height: 6, col: g.theme.fg2, render: renderBar},
|
||||
this.showDate ? {height: 40} : {},
|
||||
this.showDate ? {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 && locale.hasMeridian) {
|
||||
// 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));
|
||||
}
|
||||
let bar = this.layout.bar;
|
||||
bar.height = thickness+1;
|
||||
if (this.font===1) { // vector
|
||||
const B2 = process.env.HWVERSION>1;
|
||||
if (this.is12Hour && locale.hasMeridian) {
|
||||
this.layout.time.font = "Vector:"+(B2 ? 50 : 60);
|
||||
this.layout.ampm.font = "Vector:"+(B2 ? 20 : 40);
|
||||
} else {
|
||||
this.layout.time.font = "Vector:"+(B2 ? 60 : 80);
|
||||
}
|
||||
} else {
|
||||
this.layout.time.font = "6x8:"+thickness;
|
||||
}
|
||||
this.layout.update();
|
||||
bar.y2 = bar.y+bar.height-1;
|
||||
},
|
||||
update: function(date, c) {
|
||||
"ram";
|
||||
if (c.m) this.layout.time.label = timeText(date);
|
||||
if (c.h) this.layout.ampm.label = ampmText(date);
|
||||
if (c.d && this.showDate) this.layout.date.label = dateText(date);
|
||||
if (c.m) this.layout.render();
|
||||
if (c.s) {
|
||||
barW = Math.round(date.getSeconds()/60*this.layout.bar.w);
|
||||
renderBar();
|
||||
}
|
||||
},
|
||||
resume: function() {
|
||||
prevX = 0; // force redraw of bar
|
||||
this.layout.forgetLazyState();
|
||||
},
|
||||
});
|
||||
|
||||
// Show launcher when button pressed
|
||||
Bangle.setUI("clock");
|
||||
Bangle.on("lcdPower", function(on) {
|
||||
if (on) {
|
||||
draw(true);
|
||||
}
|
||||
});
|
||||
g.reset().clear();
|
||||
Bangle.drawWidgets();
|
||||
draw();
|
||||
// power saving: only update once a minute while locked, hide bar
|
||||
if (clock.powerSave) {
|
||||
Bangle.on("lock", lock => {
|
||||
clock.precision = lock ? 60 : 1;
|
||||
clock.tick();
|
||||
renderBar(); // hide/redraw bar right away
|
||||
});
|
||||
}
|
||||
|
||||
clock.start();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "barclock",
|
||||
"name": "Bar Clock",
|
||||
"version": "0.09",
|
||||
"version": "0.15",
|
||||
"description": "A simple digital clock showing seconds as a bar",
|
||||
"icon": "clock-bar.png",
|
||||
"screenshots": [{"url":"screenshot.png"},{"url":"screenshot_pm.png"}],
|
||||
|
@ -12,6 +12,10 @@
|
|||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{"name":"barclock.app.js","url":"clock-bar.js"},
|
||||
{"name":"barclock.settings.js","url":"settings.js"},
|
||||
{"name":"barclock.img","url":"clock-bar-icon.js","evaluate":true}
|
||||
],
|
||||
"data": [
|
||||
{"name":"barclock.settings.json"}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
(function(back) {
|
||||
let s = require("Storage").readJSON("barclock.settings.json", true) || {};
|
||||
|
||||
function save(key, value) {
|
||||
s[key] = value;
|
||||
require("Storage").writeJSON("barclock.settings.json", s);
|
||||
}
|
||||
|
||||
const fonts = [/*LANG*/"Bitmap",/*LANG*/"Vector"];
|
||||
let menu = {
|
||||
"": {"title": /*LANG*/"Bar Clock"},
|
||||
/*LANG*/"< Back": back,
|
||||
/*LANG*/"Font": {
|
||||
value: s.font|0,
|
||||
min: 0, max: 1, wrap: true,
|
||||
format: v => fonts[v],
|
||||
onchange: v => save("font", v),
|
||||
},
|
||||
};
|
||||
let items = {
|
||||
showDate: s.showDate,
|
||||
loadWidgets: s.loadWidgets,
|
||||
};
|
||||
// Power saving for Bangle.js 1 doesn't make sense (no updates while screen is off anyway)
|
||||
if (process.env.HWVERSION>1) {
|
||||
items.powerSave = s.powerSave;
|
||||
}
|
||||
require("ClockFace_menu").addItems(menu, save, items);
|
||||
E.showMenu(menu);
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
0.01: Please forgive me
|
||||
0.02: Now tells time!
|
||||
0.03: Interaction
|
||||
0.04: Shows day of week
|
||||
0.05: Shows day of month
|
||||
0.06: Updates every 5 minutes when locked, or when unlock occurs. Also shows nr of steps.
|
||||
0.07: Step count resets at midnight
|
||||
0.08: Step count stored in memory to survive reloads. Now shows step count daily and since last reboot.
|
||||
0.09: NOW it really should reset daily (instead of every other day...)
|
||||
0.10: Tell clock widgets to hide.
|
|
@ -0,0 +1,23 @@
|
|||
# Barcode clockwatchface
|
||||
|
||||
A scannable EAN-8 compatible clockwatchface for your Bangle 2
|
||||
|
||||
The format of the bars are
|
||||
|
||||
`||HHmm||MMwc||`
|
||||
|
||||
* Left section: HHmm
|
||||
* H: Hours
|
||||
* m: Minutes
|
||||
* Right section: MM9c
|
||||
* M: Day of month
|
||||
* w: Day of week
|
||||
* c: Calculated EAN-8 digit checksum
|
||||
|
||||
Apart from that
|
||||
|
||||
* The upper left section displays total number of steps per day
|
||||
* The upper right section displays total number of steps from last boot ("stepuptime")
|
||||
* The face updates every 5 minutes or on demant by pressing the hardware button
|
||||
|
||||
This clockwathface is aware of theme choice, so it will adapt to Light/Dark themes.
|