Merge remote-tracking branch 'upstream/master'

pull/2129/head
hughbarney 2022-09-18 15:53:13 +01:00
commit 03b34ad9ed
1148 changed files with 47323 additions and 6031 deletions

12
.github/FUNDING.yml vendored Normal file
View File

@ -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']

View File

@ -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
- name: Build all TS apps and widgets
working-directory: ./typescript
run: npm run build:types
- name: build all TS apps and widgets
working-directory: ./typescript
run: npm run build
run: npm run build

5
.gitignore vendored
View File

@ -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*

View File

@ -1,7 +1,7 @@
Bangle.js App Loader (and Apps)
================================
[![Build Status](https://app.travis-ci.com/espruino/BangleApps.svg?branch=master)](https://app.travis-ci.com/github/espruino/BangleApps)
[![Build Status](https://github.com/espruino/BangleApps/actions/workflows/nodejs.yml/badge.svg)](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/)
@ -186,12 +186,12 @@ The widget example is available in [`apps/_example_widget`](apps/_example_widget
Widgets are just small bits of code that run whenever an app that supports them
calls `Bangle.loadWidgets()`. If they want to display something in the 24px high
widget bar at the top of the screen they can add themselves to the global
widget bar at the top of the screen they can add themselves to the global
`WIDGETS` array with:
```
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
@ -415,7 +420,7 @@ Example `settings.js`
// make sure to enclose the function in parentheses
(function(back) {
let settings = require('Storage').readJSON('myappid.json',1)||{};
if (typeof settings.monkeys !== "number") settings.monkeys = 12; // default value
if (typeof settings.monkeys !== "number") settings.monkeys = 12; // default value
function save(key, value) {
settings[key] = value;
require('Storage').write('myappid.json', settings);

View File

@ -1 +1 @@
theme: jekyll-theme-minimal
theme: jekyll-theme-slate

352
android.html Normal file
View File

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

View File

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

11
apps/2ofthemclk/README.md Normal file
View File

@ -0,0 +1,11 @@
# two of them clock
You can now wear teh memez on your wrist.
![](screenshot.png)
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)

View File

@ -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"))

90
apps/2ofthemclk/app.js Normal file

File diff suppressed because one or more lines are too long

BIN
apps/2ofthemclk/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
apps/2ofthemclk/bg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -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}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -149,11 +149,11 @@ function drawHSeg(x1,y1,x2,y2,Num,Color,Size) {
if (Color == "fg") {
g.setColor(g.theme.fg);
} else {
g.setColor(mColor[0],mColor[1],mColor[2]);
g.setColor(mColor[0],mColor[1],mColor[2]);
}
g.fillCircle(x1+Dx+(i-1)*(x2-x1)/7,y1+Dy+(j-1)*(y2-y1)/7,Size);
} else {
g.setColor(bColor[0],bColor[1],bColor[2]);
g.setColor(bColor[0],bColor[1],bColor[2]);
g.fillCircle(x1+Dx+(i-1)*(x2-x1)/7,y1+Dy+(j-1)*(y2-y1)/7,1);
}
}
@ -166,7 +166,7 @@ function drawSSeg(x1,y1,x2,y2,Num,Color,Size) {
for (let j = 1; j < 8; j++) {
if (Font[Num][j-1][i-1] == 1) {
if (Color == "fg") {
g.setColor(sColor[0],sColor[1],sColor[2]);
g.setColor(sColor[0],sColor[1],sColor[2]);
} else {
g.setColor(g.theme.fg);
//g.setColor(0.7,0.7,0.7);
@ -253,8 +253,8 @@ function actions(v){
if(BTN1.read() === true) {
print("BTN pressed");
Bangle.showLauncher();
}
}
if(v==-1){
print("up swipe event");
if(settings.swupApp != "") load(settings.swupApp);
@ -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);
@ -289,27 +289,27 @@ function drawWidgeds() {
g.setColor((g.getBPP()>8) ? "#07f" : (g.theme.dark ? "#0ff" : "#00f"));
else
g.setColor(g.theme.dark ? "#666" : "#999");
g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),x1Bt,y1Bt);
g.drawImage(atob("CxQBBgDgFgJgR4jZMawfAcA4D4NYybEYIwTAsBwDAA=="),x1Bt,y1Bt);
//Battery
//print(E.getBattery());
//print(Bangle.isCharging());
var x1B = 130;
var y1B = 2;
var x2B = x1B + 20;
var y2B = y1B + 15;
g.setColor(g.theme.bg);
g.clearRect(x1B,y1B,x2B,y2B);
g.setColor(g.theme.fg);
g.drawRect(x1B,y1B,x2B,y2B);
g.fillRect(x1B,y1B,x1B+(E.getBattery()*(x2B-x1B)/100),y2B);
g.fillRect(x2B,y1B+(y2B-y1B)/2-3,x2B+4,y1B+(y2B-y1B)/2+3);
//Messages
@ -318,25 +318,25 @@ 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);
g.drawLine(x1M,y1M,x1M+(x2M-x1M)/2,y1M+(y2M-y1M)/2);
g.drawLine(x1M+(x2M-x1M)/2,y1M+(y2M-y1M)/2,x2M,y1M);
}
var strDow = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
var d = new Date();
var dow = d.getDay(),day = d.getDate(), month = d.getMonth() + 1, year = d.getFullYear();
print(strDow[dow] + ' ' + day + '.' + month + ' ' + year);
g.setColor(g.theme.fg);
g.setFontAlign(-1, -1,0);
g.setFont("Vector", 20);
g.drawString(strDow[dow] + ' ' + day, 0, 0, true);
}
@ -354,7 +354,7 @@ function SetFull(on) {
} else {
Ys = 30;
Bangle.setUI("updown",actions);
Bangle.on('swipe', function(direction) {
Bangle.on('swipe', function(direction) {
switch (direction) {
case 1:
print("swipe left event");
@ -362,7 +362,7 @@ function SetFull(on) {
print(settings.swleftApp);
break;
case -1:
print("swipe right event");
print("swipe right event");
if(settings.swrightApp != "") load(settings.swrightApp);
print(settings.swrightApp);
break;
@ -374,7 +374,7 @@ function SetFull(on) {
SegH = (Ye-Ys)/2;
Dy = SegH/16;
draw();
if (on != true) {

View File

@ -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

View File

@ -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",

View File

@ -1,2 +1,3 @@
0.01: New App!
0.02: Fullscreen settings.
0.02: Fullscreen settings.
0.03: Tell clock widgets to hide.

View File

@ -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");

View File

@ -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",

View File

@ -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
};

View File

@ -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

View File

@ -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

View File

@ -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();
})();

View File

@ -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();
function getHoursMins(date){
var h = date.getHours();
var m = date.getMinutes();
return (""+h).substr(-2) + ":" + ("0"+m).substr(-2);
}
function drawData(name, value, y){
g.drawString(name, 10, y);
g.drawString(value, 100, y);
}
Bangle.buzz(400);
setTimeout(load, 20000);
}
function drawInfo() {
var h=18, y = h;
g.setColor(g.theme.fg);
g.setFont("Vector",h).setFontAlign(-1,-1);
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());
}
}
// 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);
*/
}
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
activityreminder = require("activityreminder").loadSettings();
stepsArray = require("activityreminder").loadStepsArray();
run();
function getCurrentSteps(){
let health = Bangle.getHealthStatus("day");
return health.steps - activityreminder_data.stepsOnDate;
}
function run() {
g.clear();
Bangle.loadWidgets();
Bangle.drawWidgets();
drawInfo();
}
run();
})();

View File

@ -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
*/
}
})();

View File

@ -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;
};

View File

@ -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}
]
}

View File

@ -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());
})

3
apps/advcasio/ChangeLog Normal file
View File

@ -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.

62
apps/advcasio/README.md Normal file
View File

@ -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>

View File

@ -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"))

304
apps/advcasio/app.js Normal file
View File

@ -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();

BIN
apps/advcasio/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

1
apps/advcasio/data.json Normal file
View File

@ -0,0 +1 @@
{"tasks":"", "weather":[]};

View File

@ -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 }
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

5
apps/agenda/ChangeLog Normal file
View File

@ -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

20
apps/agenda/README.md Normal file
View File

@ -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.

View File

@ -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"))

138
apps/agenda/agenda.js Normal file
View File

@ -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();

BIN
apps/agenda/agenda.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

18
apps/agenda/metadata.json Normal file
View File

@ -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"}]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

48
apps/agenda/settings.js Normal file
View File

@ -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);
})

2
apps/agpsdata/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: First, proof of concept
0.02: Load AGPS data on app start and automatically in background

19
apps/agpsdata/README.md Normal file
View File

@ -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)

View File

@ -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==")

BIN
apps/agpsdata/agpsdata.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

54
apps/agpsdata/app.js Normal file
View File

@ -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();

33
apps/agpsdata/boot.js Normal file
View File

@ -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);
}
})();

View File

@ -0,0 +1 @@
{"enabled":true,"refresh":1440,"gnsstype":1}

75
apps/agpsdata/lib.js Normal file
View File

@ -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");
}
};

View File

@ -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"}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

71
apps/agpsdata/settings.js Normal file
View File

@ -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());
});

View File

@ -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

View File

@ -1,7 +1,31 @@
Alarms & Timers
===============
# Alarms & Timers
This app allows you to add/modify any alarms and timers.
It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched)
to handle the alarm scheduling in an efficient way that can work alongside other apps.
It uses the [`sched` library](https://github.com/espruino/BangleApps/blob/master/apps/sched) to handle the alarm scheduling in an efficient way that can work alongside other apps.
## Menu overview
- `New...`
- `New Alarm` &rarr; Configure a new alarm
- `Repeat` &rarr; Select when the alarm will fire. You can select a predefined option (_Once_, _Every Day_, _Workdays_ or _Weekends_ or you can configure the days freely)
- `New Timer` &rarr; Configure a new timer
- `Advanced`
- `Scheduler settings` &rarr; Open the [Scheduler](https://github.com/espruino/BangleApps/tree/master/apps/sched) settings page, see its [README](https://github.com/espruino/BangleApps/blob/master/apps/sched/README.md) for details
- `Enable All` &rarr; Enable _all_ disabled alarms & timers
- `Disable All` &rarr; Disable _all_ enabled alarms & timers
- `Delete All` &rarr; Delete _all_ alarms & timers
## Creator
- [Gordon Williams](https://github.com/gfwilliams)
## Main Contributors
- [Alessandro Cocco](https://github.com/alessandrococco) - New UI, full rewrite, new features
- [Sabin Iacob](https://github.com/m0n5t3r) - Auto snooze support
- [storm64](https://github.com/storm64) - Fix redrawing in submenus
## Attributions
All icons used in this app are from [icons8](https://icons8.com).

View File

@ -1,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();

View File

@ -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" }
]
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -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();

View File

@ -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)

View File

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

View File

@ -3,6 +3,7 @@
Bluetooth.println("");
Bluetooth.println(JSON.stringify(message));
}
var lastMsg;
var settings = require("Storage").readJSON("android.settings.json",1)||{};
//default alarm settings
@ -18,7 +19,17 @@
/* TODO: Call handling, fitness */
var HANDLERS = {
// {t:"notify",id:int, src,title,subject,body,sender,tel:string} add
"notify" : function() { Object.assign(event,{t:"add",positive:true, negative:true});require("messages").pushMessage(event); },
"notify" : function() {
Object.assign(event,{t:"add",positive:true, negative:true});
// Detect a weird GadgetBridge bug and fix it
// For some reason SMS messages send two GB notifications, with different sets of info
if (lastMsg && event.body == lastMsg.body && lastMsg.src == undefined && event.src == "Messages") {
// Mutate the other message
event.id = lastMsg.id;
}
lastMsg = event;
require("messages").pushMessage(event);
},
// {t:"notify~",id:int, title:string} // modified
"notify~" : function() { event.t="modify";require("messages").pushMessage(event); },
// {t:"notify-",id:int} // remove
@ -67,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 }); }

View File

@ -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
}

View File

@ -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);
})

View File

@ -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.

View File

@ -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();

View File

@ -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",

View File

@ -9,4 +9,5 @@
when weekday name and calendar weeknumber are on then display is <weekday short> #<calweek>
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.08: fixed calendar weeknumber not shortened to two digits
0.09: Use default Bangle formatter for booleans

View File

@ -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",

View File

@ -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

View 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.

View File

@ -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;

View File

@ -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",

View File

@ -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

View File

@ -4,3 +4,8 @@ A simple digital clock showing seconds as a horizontal bar.
| 24hr style | 12hr style |
| --- | --- |
| ![24-hour bar clock](screenshot.png) | ![12-hour bar clock with meridian](screenshot_pm.png) |
## 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

View File

@ -3,7 +3,6 @@
* A simple digital clock showing seconds as a bar
**/
// Check settings for what type our clock should be
const is12Hour = (require("Storage").readJSON("setting.json", 1) || {})["12hour"];
let locale = require("locale");
{ // add some more info to locale
let date = new Date();
@ -11,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();

View File

@ -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"}
]
}

30
apps/barclock/settings.js Normal file
View File

@ -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);
});

10
apps/barcode/ChangeLog Normal file
View File

@ -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.

23
apps/barcode/README.md Normal file
View File

@ -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.

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