Merge branch 'espruino:master' into quicklaunch
|
@ -2,3 +2,4 @@
|
|||
0.02: Load AGPS data on app start and automatically in background
|
||||
0.03: Do not load AGPS data on boot
|
||||
Increase minimum interval to 6 hours
|
||||
0.04: Write AGPS data chunks with delay to improve reliability
|
||||
|
|
|
@ -36,7 +36,7 @@ function updateAgps() {
|
|||
g.clear();
|
||||
if (!waiting) {
|
||||
waiting = true;
|
||||
display("Updating A-GPS...");
|
||||
display("Updating A-GPS...", "takes ~ 10 seconds");
|
||||
require("agpsdata").pull(function() {
|
||||
waiting = false;
|
||||
display("A-GPS updated.", "touch to close");
|
||||
|
|
|
@ -8,41 +8,52 @@ 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 setAGPS(b64) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var initCommands = "Bangle.setGPSPower(1);\n"; // turn GPS on
|
||||
const gnsstype = settings.gnsstype || 1; // default GPS
|
||||
initCommands += `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)
|
||||
|
||||
eval(initCommands);
|
||||
|
||||
try {
|
||||
writeChunks(atob(b64), resolve);
|
||||
} catch (e) {
|
||||
console.log("error:", e);
|
||||
reject();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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)
|
||||
var chunkI = 0;
|
||||
function writeChunks(bin, resolve) {
|
||||
return new Promise(function(resolve2) {
|
||||
const chunkSize = 128;
|
||||
setTimeout(function() {
|
||||
if (chunkI < bin.length) {
|
||||
var chunk = bin.substr(chunkI, chunkSize);
|
||||
js = `Serial1.write(atob("${btoa(chunk)}"))\n`;
|
||||
eval(js);
|
||||
|
||||
for (var i=0;i<bin.length;i+=chunkSize) {
|
||||
var chunk = bin.substr(i,chunkSize);
|
||||
js += `Serial1.write(atob("${btoa(chunk)}"))\n`;
|
||||
}
|
||||
return js;
|
||||
chunkI += chunkSize;
|
||||
writeChunks(bin, resolve);
|
||||
} else {
|
||||
if (resolve)
|
||||
resolve(); // call outer resolve
|
||||
}
|
||||
}, 200);
|
||||
});
|
||||
}
|
||||
|
||||
function CASIC_CHECKSUM(cmd) {
|
||||
var cs = 0;
|
||||
for (var i=1;i<cmd.length;i++)
|
||||
for (var i = 1; i < cmd.length; i++)
|
||||
cs = cs ^ cmd.charCodeAt(i);
|
||||
return cmd+"*"+cs.toString(16).toUpperCase().padStart(2, '0');
|
||||
return cmd + "*" + cs.toString(16).toUpperCase().padStart(2, '0');
|
||||
}
|
||||
|
||||
function updateLastUpdate() {
|
||||
|
@ -53,23 +64,30 @@ function updateLastUpdate() {
|
|||
}
|
||||
|
||||
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);
|
||||
});
|
||||
const uri = "https://www.espruino.com/agps/casic.base64";
|
||||
if (Bangle.http) {
|
||||
Bangle.http(uri, {timeout : 10000})
|
||||
.then(event => {
|
||||
setAGPS(event.resp)
|
||||
.then(r => {
|
||||
updateLastUpdate();
|
||||
if (successCallback)
|
||||
successCallback();
|
||||
})
|
||||
.catch((e) => {
|
||||
console.log("error", e);
|
||||
if (failureCallback)
|
||||
failureCallback(e);
|
||||
});
|
||||
})
|
||||
.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");
|
||||
if (failureCallback)
|
||||
failureCallback(/*LANG*/ "No http method");
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"name": "A-GPS Data Downloader App",
|
||||
"shortName":"A-GPS Data",
|
||||
"icon": "agpsdata.png",
|
||||
"version":"0.03",
|
||||
"version":"0.04",
|
||||
"description": "Once installed, this app allows you to download assisted GPS (A-GPS) data directly to your Bangle.js **via Gadgetbridge on an Android phone** when you run the app. If you just want to upload the latest AGPS data from this app loader, please use the `Assisted GPS Update (AGPS)` app.",
|
||||
"tags": "boot,tool,assisted,gps,agps,http",
|
||||
"allow_emulator":true,
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: First version
|
|
@ -0,0 +1,5 @@
|
|||
# BarWatch - an experimental watch
|
||||
|
||||
For too long the watches have shown the time with digits or hands. No more!
|
||||
With this stylish watch the time is represented by bars. Up to 24 as the day goes by.
|
||||
Practical? Not really, but a different look!
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("l0uwkE/4A/AH4A/AB0gicQmUB+EPgEigExh8gj8A+ECAgMQn4WCgcACyotWC34W/C34W/CycACw0wgYWFBYIWCAAc/+YGHCAgNFACkxl8hGYwAMLYUvCykQC34WycoIW/C34W0gAWTmUjkUzkbmSAFY="))
|
|
@ -0,0 +1,76 @@
|
|||
// timeout used to update every minute
|
||||
var drawTimeout;
|
||||
|
||||
// schedule a draw for the next minute
|
||||
function queueDraw() {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
}
|
||||
|
||||
|
||||
function draw() {
|
||||
g.reset();
|
||||
|
||||
if(g.theme.dark){
|
||||
g.setColor(1,1,1);
|
||||
}else{
|
||||
g.setColor(0,0,0);
|
||||
}
|
||||
|
||||
// work out how to display the current time
|
||||
var d = new Date();
|
||||
var h = d.getHours(), m = d.getMinutes();
|
||||
|
||||
// hour bars
|
||||
var bx_offset = 10, by_offset = 35;
|
||||
var b_width = 8, b_height = 60;
|
||||
var b_space = 5;
|
||||
|
||||
for(var i=0; i<h; i++){
|
||||
if(i > 11){
|
||||
by_offset = 105;
|
||||
}
|
||||
var iter = i % 12;
|
||||
//console.log(iter);
|
||||
g.fillRect(bx_offset+(b_width*(iter+1))+(b_space*iter),
|
||||
by_offset,
|
||||
bx_offset+(b_width*iter)+(b_space*iter),
|
||||
by_offset+b_height);
|
||||
}
|
||||
|
||||
// minute bar
|
||||
if(h > 11){
|
||||
by_offset = 105;
|
||||
}
|
||||
var m_bar = h % 12;
|
||||
if(m != 0){
|
||||
g.fillRect(bx_offset+(b_width*(m_bar+1))+(b_space*m_bar),
|
||||
by_offset+b_height-m,
|
||||
bx_offset+(b_width*m_bar)+(b_space*m_bar),
|
||||
by_offset+b_height);
|
||||
}
|
||||
|
||||
// queue draw in one minute
|
||||
queueDraw();
|
||||
}
|
||||
|
||||
// Clear the screen once, at startup
|
||||
g.clear();
|
||||
// draw immediately at first
|
||||
draw();
|
||||
// Stop updates when LCD is off, restart when on
|
||||
Bangle.on('lcdPower',on=>{
|
||||
if (on) {
|
||||
draw(); // draw immediately, queue redraw
|
||||
} else { // stop draw timer
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
Bangle.setUI("clock");
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
After Width: | Height: | Size: 973 B |
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"id": "barwatch",
|
||||
"name": "BarWatch",
|
||||
"shortName":"BarWatch",
|
||||
"version":"0.01",
|
||||
"description": "A watch that displays the time using bars. One bar for each hour.",
|
||||
"readme": "README.md",
|
||||
"icon": "screenshot.png",
|
||||
"tags": "clock",
|
||||
"type": "clock",
|
||||
"allow_emulator":true,
|
||||
"screenshots" : [ { "url": "screenshot.png" } ],
|
||||
"supports" : ["BANGLEJS2"],
|
||||
"storage": [
|
||||
{"name":"barwatch.app.js","url":"app.js"},
|
||||
{"name":"barwatch.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -30,3 +30,7 @@
|
|||
Allow recording unmodified internal HR
|
||||
Better connection retry handling
|
||||
0.13: Less time used during boot if disabled
|
||||
0.14: Allow bonding (Debug menu)
|
||||
Prevent mixing of BT and internal HRM events if both are enabled
|
||||
Always use a grace period (default 0 ms) to decouple some connection steps
|
||||
Device not found errors now utilize increasing timeouts
|
||||
|
|
|
@ -16,5 +16,6 @@
|
|||
"gracePeriodNotification": 0,
|
||||
"gracePeriodConnect": 0,
|
||||
"gracePeriodService": 0,
|
||||
"gracePeriodRequest": 0
|
||||
"gracePeriodRequest": 0,
|
||||
"bonding": false
|
||||
}
|
||||
|
|
|
@ -109,6 +109,7 @@ exports.enable = () => {
|
|||
if (supportedCharacteristics["0x2a37"].active) stopFallback();
|
||||
if (bpmTimeout) clearTimeout(bpmTimeout);
|
||||
bpmTimeout = setTimeout(()=>{
|
||||
bpmTimeout = undefined;
|
||||
supportedCharacteristics["0x2a37"].active = false;
|
||||
startFallback();
|
||||
}, 3000);
|
||||
|
@ -154,8 +155,8 @@ exports.enable = () => {
|
|||
src: "bthrm"
|
||||
};
|
||||
|
||||
log("Emitting HRM", repEvent);
|
||||
Bangle.emit("HRM_int", repEvent);
|
||||
log("Emitting aggregated HRM", repEvent);
|
||||
Bangle.emit("HRM_R", repEvent);
|
||||
}
|
||||
|
||||
var newEvent = {
|
||||
|
@ -280,7 +281,11 @@ exports.enable = () => {
|
|||
log("Disconnect: " + reason);
|
||||
log("GATT", gatt);
|
||||
log("Characteristics", characteristics);
|
||||
clearRetryTimeout(reason != "Connection Timeout");
|
||||
|
||||
var retryTimeResetNeeded = true;
|
||||
retryTimeResetNeeded &= reason != "Connection Timeout";
|
||||
retryTimeResetNeeded &= reason != "No device found matching filters";
|
||||
clearRetryTimeout(retryTimeResetNeeded);
|
||||
supportedCharacteristics["0x2a37"].active = false;
|
||||
startFallback();
|
||||
blockInit = false;
|
||||
|
@ -312,13 +317,13 @@ exports.enable = () => {
|
|||
result = result.then(()=>{
|
||||
log("Starting notifications", newCharacteristic);
|
||||
var startPromise = newCharacteristic.startNotifications().then(()=>log("Notifications started", newCharacteristic));
|
||||
if (settings.gracePeriodNotification > 0){
|
||||
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
|
||||
startPromise = startPromise.then(()=>{
|
||||
log("Wait after connect");
|
||||
return waitingPromise(settings.gracePeriodNotification);
|
||||
});
|
||||
}
|
||||
|
||||
log("Add " + settings.gracePeriodNotification + "ms grace period after starting notifications");
|
||||
startPromise = startPromise.then(()=>{
|
||||
log("Wait after connect");
|
||||
return waitingPromise(settings.gracePeriodNotification);
|
||||
});
|
||||
|
||||
return startPromise;
|
||||
});
|
||||
}
|
||||
|
@ -429,30 +434,30 @@ exports.enable = () => {
|
|||
var connectPromise = gatt.connect(connectSettings).then(function() {
|
||||
log("Connected.");
|
||||
});
|
||||
if (settings.gracePeriodConnect > 0){
|
||||
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
|
||||
connectPromise = connectPromise.then(()=>{
|
||||
log("Wait after connect");
|
||||
return waitingPromise(settings.gracePeriodConnect);
|
||||
});
|
||||
}
|
||||
log("Add " + settings.gracePeriodConnect + "ms grace period after connecting");
|
||||
connectPromise = connectPromise.then(()=>{
|
||||
log("Wait after connect");
|
||||
return waitingPromise(settings.gracePeriodConnect);
|
||||
});
|
||||
return connectPromise;
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
/* promise = promise.then(() => {
|
||||
log(JSON.stringify(gatt.getSecurityStatus()));
|
||||
if (gatt.getSecurityStatus()['bonded']) {
|
||||
log("Already bonded");
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
log("Start bonding");
|
||||
return gatt.startBonding()
|
||||
.then(() => console.log(gatt.getSecurityStatus()));
|
||||
}
|
||||
});*/
|
||||
|
||||
if (settings.bonding){
|
||||
promise = promise.then(() => {
|
||||
log(JSON.stringify(gatt.getSecurityStatus()));
|
||||
if (gatt.getSecurityStatus()['bonded']) {
|
||||
log("Already bonded");
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
log("Start bonding");
|
||||
return gatt.startBonding()
|
||||
.then(() => console.log(gatt.getSecurityStatus()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
promise = promise.then(()=>{
|
||||
if (!characteristics || characteristics.length === 0){
|
||||
|
@ -476,13 +481,11 @@ exports.enable = () => {
|
|||
log("Supporting service", service.uuid);
|
||||
result = attachServicePromise(result, service);
|
||||
}
|
||||
if (settings.gracePeriodService > 0) {
|
||||
log("Add " + settings.gracePeriodService + "ms grace period after services");
|
||||
result = result.then(()=>{
|
||||
log("Wait after services");
|
||||
return waitingPromise(settings.gracePeriodService);
|
||||
});
|
||||
}
|
||||
log("Add " + settings.gracePeriodService + "ms grace period after services");
|
||||
result = result.then(()=>{
|
||||
log("Wait after services");
|
||||
return waitingPromise(settings.gracePeriodService);
|
||||
});
|
||||
return result;
|
||||
});
|
||||
} else {
|
||||
|
@ -538,35 +541,33 @@ exports.enable = () => {
|
|||
};
|
||||
|
||||
if (settings.replace){
|
||||
// register a listener for original HRM events and emit as HRM_int
|
||||
Bangle.on("HRM", (e) => {
|
||||
e.modified = true;
|
||||
Bangle.emit("HRM_int", e);
|
||||
if (fallbackActive){
|
||||
// if fallback to internal HRM is active, emit as HRM_R to which everyone listens
|
||||
Bangle.emit("HRM_R", e);
|
||||
}
|
||||
});
|
||||
|
||||
Bangle.origOn = Bangle.on;
|
||||
Bangle.on = function(name, callback) {
|
||||
if (name == "HRM") {
|
||||
Bangle.origOn("HRM_int", callback);
|
||||
} else {
|
||||
Bangle.origOn(name, callback);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.origRemoveListener = Bangle.removeListener;
|
||||
Bangle.removeListener = function(name, callback) {
|
||||
if (name == "HRM") {
|
||||
Bangle.origRemoveListener("HRM_int", callback);
|
||||
} else {
|
||||
Bangle.origRemoveListener(name, callback);
|
||||
}
|
||||
};
|
||||
// force all apps wanting to listen to HRM to actually get events for HRM_R
|
||||
Bangle.on = ( o => (name, cb) => {
|
||||
o = o.bind(Bangle);
|
||||
if (name == "HRM") o("HRM_R", cb);
|
||||
else o(name, cb);
|
||||
})(Bangle.on);
|
||||
|
||||
Bangle.removeListener = ( o => (name, cb) => {
|
||||
o = o.bind(Bangle);
|
||||
if (name == "HRM") o("HRM_R", cb);
|
||||
else o(name, cb);
|
||||
})(Bangle.removeListener);
|
||||
}
|
||||
|
||||
Bangle.origSetHRMPower = Bangle.setHRMPower;
|
||||
|
||||
if (settings.startWithHrm){
|
||||
|
||||
Bangle.setHRMPower = function(isOn, app) {
|
||||
log("setHRMPower for " + app + ": " + (isOn?"on":"off"));
|
||||
if (settings.enabled){
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "bthrm",
|
||||
"name": "Bluetooth Heart Rate Monitor",
|
||||
"shortName": "BT HRM",
|
||||
"version": "0.13",
|
||||
"version": "0.14",
|
||||
"description": "Overrides Bangle.js's build in heart rate monitor with an external Bluetooth one.",
|
||||
"icon": "app.png",
|
||||
"type": "app",
|
||||
|
|
|
@ -96,6 +96,12 @@
|
|||
writeSettings("debuglog",v);
|
||||
}
|
||||
},
|
||||
'Use bonding': {
|
||||
value: !!settings.bonding,
|
||||
onchange: v => {
|
||||
writeSettings("bonding",v);
|
||||
}
|
||||
},
|
||||
'Grace periods': function() { E.showMenu(submenu_grace); }
|
||||
};
|
||||
|
||||
|
|
|
@ -3,3 +3,5 @@
|
|||
0.03: Support for different screen sizes and touchscreen
|
||||
0.04: Display current operation on LHS
|
||||
0.05: Grid positioning and swipe controls to switch between numbers, operators and special (for Bangle.js 2)
|
||||
0.06: Bangle.js 2: Exit with a short press of the physical button
|
||||
0.07: Bangle.js 2: Exit by pressing upper left corner of the screen
|
||||
|
|
|
@ -12,12 +12,20 @@ Basic calculator reminiscent of MacOs's one. Handy for small calculus.
|
|||
|
||||
## Controls
|
||||
|
||||
Bangle.js 1
|
||||
- UP: BTN1
|
||||
- DOWN: BTN3
|
||||
- LEFT: BTN4
|
||||
- RIGHT: BTN5
|
||||
- SELECT: BTN2
|
||||
|
||||
Bangle.js 2
|
||||
- Swipes to change visible buttons
|
||||
- Click physical button to exit
|
||||
- Press upper left corner of screen to exit (where the red back button would be)
|
||||
## Creator
|
||||
|
||||
<https://twitter.com/fredericrous>
|
||||
|
||||
## Contributors
|
||||
[thyttan](https://github.com/thyttan)
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
*
|
||||
* Original Author: Frederic Rousseau https://github.com/fredericrous
|
||||
* Created: April 2020
|
||||
*
|
||||
* Contributors: thyttan https://github.com/thyttan
|
||||
*/
|
||||
|
||||
g.clear();
|
||||
|
@ -402,43 +404,42 @@ if (process.env.HWVERSION==1) {
|
|||
swipeEnabled = false;
|
||||
drawGlobal();
|
||||
} else { // touchscreen?
|
||||
selected = "NONE";
|
||||
selected = "NONE";
|
||||
swipeEnabled = true;
|
||||
prepareScreen(numbers, numbersGrid, COLORS.DEFAULT);
|
||||
prepareScreen(operators, operatorsGrid, COLORS.OPERATOR);
|
||||
prepareScreen(specials, specialsGrid, COLORS.SPECIAL);
|
||||
drawNumbers();
|
||||
Bangle.on('touch',(n,e)=>{
|
||||
for (var key in screen) {
|
||||
if (typeof screen[key] == "undefined") break;
|
||||
var r = screen[key].xy;
|
||||
if (e.x>=r[0] && e.y>=r[1] &&
|
||||
e.x<r[2] && e.y<r[3]) {
|
||||
//print("Press "+key);
|
||||
buttonPress(""+key);
|
||||
|
||||
Bangle.setUI({
|
||||
mode : 'custom',
|
||||
back : load, // Clicking physical button or pressing upper left corner turns off (where red back button would be)
|
||||
touch : (n,e)=>{
|
||||
for (var key in screen) {
|
||||
if (typeof screen[key] == "undefined") break;
|
||||
var r = screen[key].xy;
|
||||
if (e.x>=r[0] && e.y>=r[1] && e.x<r[2] && e.y<r[3]) {
|
||||
//print("Press "+key);
|
||||
buttonPress(""+key);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
var lastX = 0, lastY = 0;
|
||||
Bangle.on('drag', (e) => {
|
||||
if (!e.b) {
|
||||
if (lastX > 50) { // right
|
||||
},
|
||||
swipe : (LR, UD) => {
|
||||
if (LR == 1) { // right
|
||||
drawSpecials();
|
||||
} else if (lastX < -50) { // left
|
||||
}
|
||||
if (LR == -1) { // left
|
||||
drawOperators();
|
||||
} else if (lastY > 50) { // down
|
||||
drawNumbers();
|
||||
} else if (lastY < -50) { // up
|
||||
}
|
||||
if (UD == 1) { // down
|
||||
drawNumbers();
|
||||
}
|
||||
if (UD == -1) { // up
|
||||
drawNumbers();
|
||||
}
|
||||
lastX = 0;
|
||||
lastY = 0;
|
||||
} else {
|
||||
lastX = lastX + e.dx;
|
||||
lastY = lastY + e.dy;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
displayOutput(0);
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "calculator",
|
||||
"name": "Calculator",
|
||||
"shortName": "Calculator",
|
||||
"version": "0.05",
|
||||
"version": "0.07",
|
||||
"description": "Basic calculator reminiscent of MacOs's one. Handy for small calculus.",
|
||||
"icon": "calculator.png",
|
||||
"screenshots": [{"url":"screenshot_calculator.png"}],
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
0.03: Made the code shorter and somewhat more readable by writing some functions. Also made it work as a library where it returns the text once finished. The keyboard is now made to exit correctly when the 'back' event is called. The keyboard now uses theme colors correctly, although it still looks best with dark theme. The numbers row is now solidly green - except for highlights.
|
||||
0.04: Now displays the opened text string at launch.
|
||||
0.05: Now scrolls text when string gets longer than screen width.
|
||||
0.06: The code is now more reliable and the input snappier. Widgets will be drawn if present.
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
//Keep banglejs screen on for 100 sec at 0.55 power level for development purposes
|
||||
//Bangle.setLCDTimeout(30);
|
||||
//Bangle.setLCDPower(1);
|
||||
|
||||
exports.input = function(options) {
|
||||
options = options||{};
|
||||
var text = options.text;
|
||||
if ("string"!=typeof text) text="";
|
||||
|
||||
|
||||
var R = Bangle.appRect;
|
||||
var BGCOLOR = g.theme.bg;
|
||||
var HLCOLOR = g.theme.fg;
|
||||
var ABCCOLOR = g.toColor(1,0,0);//'#FF0000';
|
||||
|
@ -17,35 +14,38 @@ exports.input = function(options) {
|
|||
var SMALLFONTWIDTH = parseInt(SMALLFONT.charAt(0)*parseInt(SMALLFONT.charAt(-1)));
|
||||
|
||||
var ABC = 'abcdefghijklmnopqrstuvwxyz'.toUpperCase();
|
||||
var ABCPADDING = (g.getWidth()-6*ABC.length)/2;
|
||||
var ABCPADDING = ((R.x+R.w)-6*ABC.length)/2;
|
||||
|
||||
var NUM = ' 1234567890!?,.- ';
|
||||
var NUMHIDDEN = ' 1234567890!?,.- ';
|
||||
var NUMPADDING = (g.getWidth()-6*NUM.length)/2;
|
||||
var NUMPADDING = ((R.x+R.w)-6*NUM.length)/2;
|
||||
|
||||
var rectHeight = 40;
|
||||
|
||||
|
||||
var delSpaceLast;
|
||||
|
||||
function drawAbcRow() {
|
||||
g.clear();
|
||||
try { // Draw widgets if they are present in the current app.
|
||||
if (WIDGETS) Bangle.drawWidgets();
|
||||
} catch (_) {}
|
||||
g.setFont(SMALLFONT);
|
||||
g.setColor(ABCCOLOR);
|
||||
g.drawString(ABC, ABCPADDING, g.getHeight()/2);
|
||||
g.fillRect(0, g.getHeight()-26, g.getWidth(), g.getHeight());
|
||||
g.setFontAlign(-1, -1, 0);
|
||||
g.drawString(ABC, ABCPADDING, (R.y+R.h)/2);
|
||||
g.fillRect(0, (R.y+R.h)-26, (R.x+R.w), (R.y+R.h));
|
||||
}
|
||||
|
||||
function drawNumRow() {
|
||||
g.setFont(SMALLFONT);
|
||||
g.setColor(NUMCOLOR);
|
||||
g.drawString(NUM, NUMPADDING, g.getHeight()/4);
|
||||
g.setFontAlign(-1, -1, 0);
|
||||
g.drawString(NUM, NUMPADDING, (R.y+R.h)/4);
|
||||
|
||||
g.fillRect(NUMPADDING, g.getHeight()-rectHeight*4/3, g.getWidth()-NUMPADDING, g.getHeight()-rectHeight*2/3);
|
||||
g.fillRect(NUMPADDING, (R.y+R.h)-rectHeight*4/3, (R.x+R.w)-NUMPADDING, (R.y+R.h)-rectHeight*2/3);
|
||||
}
|
||||
|
||||
function updateTopString() {
|
||||
"ram"
|
||||
g.setColor(BGCOLOR);
|
||||
g.fillRect(0,4+20,176,13+20);
|
||||
g.setColor(0.2,0,0);
|
||||
|
@ -54,13 +54,10 @@ exports.input = function(options) {
|
|||
g.setColor(0.7,0,0);
|
||||
g.fillRect(rectLen+5,4+20,rectLen+10,13+20);
|
||||
g.setColor(1,1,1);
|
||||
g.setFontAlign(-1, -1, 0);
|
||||
g.drawString(text.length<=27? text.substr(-27, 27) : '<- '+text.substr(-24,24), 5, 5+20);
|
||||
}
|
||||
|
||||
drawAbcRow();
|
||||
drawNumRow();
|
||||
updateTopString();
|
||||
|
||||
var abcHL;
|
||||
var abcHLPrev = -10;
|
||||
var numHL;
|
||||
|
@ -68,194 +65,182 @@ exports.input = function(options) {
|
|||
var type = '';
|
||||
var typePrev = '';
|
||||
var largeCharOffset = 6;
|
||||
|
||||
|
||||
function resetChars(char, HLPrev, typePadding, heightDivisor, rowColor) {
|
||||
"ram"
|
||||
"ram";
|
||||
// Small character in list
|
||||
g.setColor(rowColor);
|
||||
g.setFont(SMALLFONT);
|
||||
g.drawString(char, typePadding + HLPrev*6, g.getHeight()/heightDivisor);
|
||||
g.setFontAlign(-1, -1, 0);
|
||||
g.drawString(char, typePadding + HLPrev*6, (R.y+R.h)/heightDivisor);
|
||||
// Large character
|
||||
g.setColor(BGCOLOR);
|
||||
g.fillRect(0,g.getHeight()/3,176,g.getHeight()/3+24);
|
||||
//g.drawString(charSet.charAt(HLPrev), typePadding + HLPrev*6 -largeCharOffset, g.getHeight()/3);; //Old implementation where I find the shape and place of letter to remove instead of just a rectangle.
|
||||
g.fillRect(0,(R.y+R.h)/3,176,(R.y+R.h)/3+24);
|
||||
//g.drawString(charSet.charAt(HLPrev), typePadding + HLPrev*6 -largeCharOffset, (R.y+R.h)/3);; //Old implementation where I find the shape and place of letter to remove instead of just a rectangle.
|
||||
// mark in the list
|
||||
}
|
||||
function showChars(char, HL, typePadding, heightDivisor) {
|
||||
"ram"
|
||||
"ram";
|
||||
// mark in the list
|
||||
g.setColor(HLCOLOR);
|
||||
g.setFont(SMALLFONT);
|
||||
if (char != 'del' && char != 'space') g.drawString(char, typePadding + HL*6, g.getHeight()/heightDivisor);
|
||||
g.setFontAlign(-1, -1, 0);
|
||||
if (char != 'del' && char != 'space') g.drawString(char, typePadding + HL*6, (R.y+R.h)/heightDivisor);
|
||||
// show new large character
|
||||
g.setFont(BIGFONT);
|
||||
g.drawString(char, typePadding + HL*6 -largeCharOffset, g.getHeight()/3);
|
||||
g.drawString(char, typePadding + HL*6 -largeCharOffset, (R.y+R.h)/3);
|
||||
g.setFont(SMALLFONT);
|
||||
}
|
||||
|
||||
|
||||
function initDraw() {
|
||||
//var R = Bangle.appRect; // To make sure it's properly updated. Not sure if this is needed.
|
||||
drawAbcRow();
|
||||
drawNumRow();
|
||||
updateTopString();
|
||||
}
|
||||
initDraw();
|
||||
//setTimeout(initDraw, 0); // So Bangle.appRect reads the correct environment. It would draw off to the side sometimes otherwise.
|
||||
|
||||
function changeCase(abcHL) {
|
||||
g.setColor(BGCOLOR);
|
||||
g.drawString(ABC, ABCPADDING, g.getHeight()/2);
|
||||
g.setFontAlign(-1, -1, 0);
|
||||
g.drawString(ABC, ABCPADDING, (R.y+R.h)/2);
|
||||
if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) ABC = ABC.toLowerCase();
|
||||
else ABC = ABC.toUpperCase();
|
||||
g.setColor(ABCCOLOR);
|
||||
g.drawString(ABC, ABCPADDING, g.getHeight()/2);
|
||||
g.drawString(ABC, ABCPADDING, (R.y+R.h)/2);
|
||||
}
|
||||
return new Promise((resolve,reject) => {
|
||||
// Interpret touch input
|
||||
// Interpret touch input
|
||||
Bangle.setUI({
|
||||
mode: 'custom',
|
||||
back: ()=>{
|
||||
Bangle.setUI();
|
||||
g.clearRect(Bangle.appRect);
|
||||
resolve(text);
|
||||
},
|
||||
drag: function(event) {
|
||||
mode: 'custom',
|
||||
back: ()=>{
|
||||
Bangle.setUI();
|
||||
g.clearRect(Bangle.appRect);
|
||||
resolve(text);
|
||||
},
|
||||
drag: function(event) {
|
||||
"ram";
|
||||
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
// Choose character by draging along red rectangle at bottom of screen
|
||||
if (event.y >= ( (R.y+R.h) - 12 )) {
|
||||
// Translate x-position to character
|
||||
if (event.x < ABCPADDING) { abcHL = 0; }
|
||||
else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
|
||||
else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
|
||||
|
||||
// ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
// Choose character by draging along red rectangle at bottom of screen
|
||||
if (event.y >= ( g.getHeight() - 12 )) {
|
||||
// Translate x-position to character
|
||||
if (event.x < ABCPADDING) { abcHL = 0; }
|
||||
else if (event.x >= 176-ABCPADDING) { abcHL = 25; }
|
||||
else { abcHL = Math.floor((event.x-ABCPADDING)/6); }
|
||||
// Datastream for development purposes
|
||||
//print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
|
||||
|
||||
// Datastream for development purposes
|
||||
//print(event.x, event.y, event.b, ABC.charAt(abcHL), ABC.charAt(abcHLPrev));
|
||||
// Unmark previous character and mark the current one...
|
||||
// Handling switching between letters and numbers/punctuation
|
||||
if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
|
||||
// Unmark previous character and mark the current one...
|
||||
// Handling switching between letters and numbers/punctuation
|
||||
if (typePrev != 'abc') resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
|
||||
if (abcHL != abcHLPrev) {
|
||||
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
|
||||
if (abcHL != abcHLPrev) {
|
||||
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
showChars(ABC.charAt(abcHL), abcHL, ABCPADDING, 2);
|
||||
}
|
||||
// Print string at top of screen
|
||||
if (event.b == 0) {
|
||||
text = text + ABC.charAt(abcHL);
|
||||
updateTopString();
|
||||
|
||||
// Autoswitching letter case
|
||||
if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
|
||||
}
|
||||
// Update previous character to current one
|
||||
abcHLPrev = abcHL;
|
||||
typePrev = 'abc';
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 12345678901234567890
|
||||
// Choose number or puctuation by draging on green rectangle
|
||||
else if ((event.y < ( g.getHeight() - 12 )) && (event.y > ( g.getHeight() - 52 ))) {
|
||||
// Translate x-position to character
|
||||
if (event.x < NUMPADDING) { numHL = 0; }
|
||||
else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
|
||||
else { numHL = Math.floor((event.x-NUMPADDING)/6); }
|
||||
|
||||
// Datastream for development purposes
|
||||
//print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
|
||||
|
||||
// Unmark previous character and mark the current one...
|
||||
// Handling switching between letters and numbers/punctuation
|
||||
if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
|
||||
if (numHL != numHLPrev) {
|
||||
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
|
||||
}
|
||||
// Print string at top of screen
|
||||
if (event.b == 0) {
|
||||
g.setColor(HLCOLOR);
|
||||
// Backspace if releasing before list of numbers/punctuation
|
||||
if (event.x < NUMPADDING) {
|
||||
// show delete sign
|
||||
showChars('del', 0, g.getWidth()/2 +6 -27 , 4);
|
||||
delSpaceLast = 1;
|
||||
text = text.slice(0, -1);
|
||||
updateTopString();
|
||||
//print(text);
|
||||
}
|
||||
// Append space if releasing after list of numbers/punctuation
|
||||
else if (event.x > g.getWidth()-NUMPADDING) {
|
||||
//show space sign
|
||||
showChars('space', 0, g.getWidth()/2 +6 -6*3*5/2 , 4);
|
||||
delSpaceLast = 1;
|
||||
text = text + ' ';
|
||||
updateTopString();
|
||||
//print(text);
|
||||
}
|
||||
// Append selected number/punctuation
|
||||
else {
|
||||
text = text + NUMHIDDEN.charAt(numHL);
|
||||
// Print string at top of screen
|
||||
if (event.b == 0) {
|
||||
text = text + ABC.charAt(abcHL);
|
||||
updateTopString();
|
||||
|
||||
// Autoswitching letter case
|
||||
if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
|
||||
if (ABC.charAt(abcHL) == ABC.charAt(abcHL).toUpperCase()) changeCase(abcHL);
|
||||
}
|
||||
// Update previous character to current one
|
||||
abcHLPrev = abcHL;
|
||||
typePrev = 'abc';
|
||||
}
|
||||
// Update previous character to current one
|
||||
numHLPrev = numHL;
|
||||
typePrev = 'num';
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// Make a space or backspace by swiping right or left on screen above green rectangle
|
||||
else if (event.y > 20+4) {
|
||||
if (event.b == 0) {
|
||||
g.setColor(HLCOLOR);
|
||||
if (event.x < g.getWidth()/2) {
|
||||
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
|
||||
// 12345678901234567890
|
||||
// Choose number or puctuation by draging on green rectangle
|
||||
else if ((event.y < ( (R.y+R.h) - 12 )) && (event.y > ( (R.y+R.h) - 52 ))) {
|
||||
// Translate x-position to character
|
||||
if (event.x < NUMPADDING) { numHL = 0; }
|
||||
else if (event.x > 176-NUMPADDING) { numHL = NUM.length-1; }
|
||||
else { numHL = Math.floor((event.x-NUMPADDING)/6); }
|
||||
|
||||
// Datastream for development purposes
|
||||
//print(event.x, event.y, event.b, NUM.charAt(numHL), NUM.charAt(numHLPrev));
|
||||
|
||||
// Unmark previous character and mark the current one...
|
||||
// Handling switching between letters and numbers/punctuation
|
||||
if (typePrev != 'num') resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
|
||||
if (numHL != numHLPrev) {
|
||||
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
|
||||
// show delete sign
|
||||
showChars('del', 0, g.getWidth()/2 +6 -27 , 4);
|
||||
delSpaceLast = 1;
|
||||
|
||||
// Backspace and draw string upper right corner
|
||||
text = text.slice(0, -1);
|
||||
updateTopString();
|
||||
if (text.length==0) changeCase(abcHL);
|
||||
//print(text, 'undid');
|
||||
showChars(NUM.charAt(numHL), numHL, NUMPADDING, 4);
|
||||
}
|
||||
else {
|
||||
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
// Print string at top of screen
|
||||
if (event.b == 0) {
|
||||
g.setColor(HLCOLOR);
|
||||
// Backspace if releasing before list of numbers/punctuation
|
||||
if (event.x < NUMPADDING) {
|
||||
// show delete sign
|
||||
showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
|
||||
delSpaceLast = 1;
|
||||
text = text.slice(0, -1);
|
||||
updateTopString();
|
||||
//print(text);
|
||||
}
|
||||
// Append space if releasing after list of numbers/punctuation
|
||||
else if (event.x > (R.x+R.w)-NUMPADDING) {
|
||||
//show space sign
|
||||
showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
|
||||
delSpaceLast = 1;
|
||||
text = text + ' ';
|
||||
updateTopString();
|
||||
//print(text);
|
||||
}
|
||||
// Append selected number/punctuation
|
||||
else {
|
||||
text = text + NUMHIDDEN.charAt(numHL);
|
||||
updateTopString();
|
||||
|
||||
//show space sign
|
||||
showChars('space', 0, g.getWidth()/2 +6 -6*3*5/2 , 4);
|
||||
delSpaceLast = 1;
|
||||
// Autoswitching letter case
|
||||
if ((text.charAt(text.length-1) == '.') || (text.charAt(text.length-1) == '!')) changeCase();
|
||||
}
|
||||
}
|
||||
// Update previous character to current one
|
||||
numHLPrev = numHL;
|
||||
typePrev = 'num';
|
||||
}
|
||||
|
||||
// Append space and draw string upper right corner
|
||||
text = text + NUMHIDDEN.charAt(0);
|
||||
updateTopString();
|
||||
//print(text, 'made space');
|
||||
// Make a space or backspace by swiping right or left on screen above green rectangle
|
||||
else if (event.y > 20+4) {
|
||||
if (event.b == 0) {
|
||||
g.setColor(HLCOLOR);
|
||||
if (event.x < (R.x+R.w)/2) {
|
||||
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
|
||||
// show delete sign
|
||||
showChars('del', 0, (R.x+R.w)/2 +6 -27 , 4);
|
||||
delSpaceLast = 1;
|
||||
|
||||
// Backspace and draw string upper right corner
|
||||
text = text.slice(0, -1);
|
||||
updateTopString();
|
||||
if (text.length==0) changeCase(abcHL);
|
||||
//print(text, 'undid');
|
||||
}
|
||||
else {
|
||||
resetChars(ABC.charAt(abcHLPrev), abcHLPrev, ABCPADDING, 2, ABCCOLOR);
|
||||
resetChars(NUM.charAt(numHLPrev), numHLPrev, NUMPADDING, 4, NUMCOLOR);
|
||||
|
||||
//show space sign
|
||||
showChars('space', 0, (R.x+R.w)/2 +6 -6*3*5/2 , 4);
|
||||
delSpaceLast = 1;
|
||||
|
||||
// Append space and draw string upper right corner
|
||||
text = text + NUMHIDDEN.charAt(0);
|
||||
updateTopString();
|
||||
//print(text, 'made space');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
/* return new Promise((resolve,reject) => {
|
||||
Bangle.setUI({mode:"custom", back:()=>{
|
||||
Bangle.setUI();
|
||||
g.clearRect(Bangle.appRect);
|
||||
Bangle.setUI();
|
||||
resolve(text);
|
||||
}});
|
||||
}); */
|
||||
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "dragboard",
|
||||
"name": "Dragboard",
|
||||
"version":"0.05",
|
||||
"version":"0.06",
|
||||
"description": "A library for text input via swiping keyboard",
|
||||
"icon": "app.png",
|
||||
"type":"textinput",
|
||||
|
|
|
@ -14,3 +14,8 @@
|
|||
0.14: Don't move pages when doing exit swipe - Bangle 2.
|
||||
0.15: 'Swipe to exit'-code is slightly altered to be more reliable - Bangle 2.
|
||||
0.16: Use default Bangle formatter for booleans
|
||||
0.17: Bangle 2: Fast loading on exit to clock face. Added option for exit to
|
||||
clock face by timeout.
|
||||
0.18: Move interactions inside setUI. Replace "one click exit" with
|
||||
back-functionality through setUI, adding the red back button as well. Hardware
|
||||
button to exit is no longer an option.
|
||||
|
|
|
@ -1,28 +1,27 @@
|
|||
{ // must be inside our own scope here so that when we are unloaded everything disappears
|
||||
|
||||
/* Desktop launcher
|
||||
*
|
||||
*/
|
||||
|
||||
var settings = Object.assign({
|
||||
let settings = Object.assign({
|
||||
showClocks: true,
|
||||
showLaunchers: true,
|
||||
direct: false,
|
||||
oneClickExit:false,
|
||||
swipeExit: false
|
||||
swipeExit: false,
|
||||
timeOut: "Off"
|
||||
}, require('Storage').readJSON("dtlaunch.json", true) || {});
|
||||
|
||||
if( settings.oneClickExit)
|
||||
setWatch(_=> load(), BTN1);
|
||||
|
||||
var s = require("Storage");
|
||||
var apps = s.list(/\.info$/).map(app=>{
|
||||
var a=s.readJSON(app,1);
|
||||
let s = require("Storage");
|
||||
var apps = s.list(/\.info$/).map(app=>{
|
||||
let a=s.readJSON(app,1);
|
||||
return a && {
|
||||
name:a.name, type:a.type, icon:a.icon, sortorder:a.sortorder, src:a.src
|
||||
};}).filter(
|
||||
app=>app && (app.type=="app" || (app.type=="clock" && settings.showClocks) || (app.type=="launch" && settings.showLaunchers) || !app.type));
|
||||
|
||||
apps.sort((a,b)=>{
|
||||
var n=(0|a.sortorder)-(0|b.sortorder);
|
||||
let n=(0|a.sortorder)-(0|b.sortorder);
|
||||
if (n) return n; // do sortorder first
|
||||
if (a.name<b.name) return -1;
|
||||
if (a.name>b.name) return 1;
|
||||
|
@ -33,29 +32,28 @@ apps.forEach(app=>{
|
|||
app.icon = s.read(app.icon); // should just be a link to a memory area
|
||||
});
|
||||
|
||||
var Napps = apps.length;
|
||||
var Npages = Math.ceil(Napps/4);
|
||||
var maxPage = Npages-1;
|
||||
var selected = -1;
|
||||
var oldselected = -1;
|
||||
var page = 0;
|
||||
let Napps = apps.length;
|
||||
let Npages = Math.ceil(Napps/4);
|
||||
let maxPage = Npages-1;
|
||||
let selected = -1;
|
||||
let oldselected = -1;
|
||||
let page = 0;
|
||||
const XOFF = 24;
|
||||
const YOFF = 30;
|
||||
|
||||
function draw_icon(p,n,selected) {
|
||||
var x = (n%2)*72+XOFF;
|
||||
var y = n>1?72+YOFF:YOFF;
|
||||
let drawIcon= function(p,n,selected) {
|
||||
let x = (n%2)*72+XOFF;
|
||||
let y = n>1?72+YOFF:YOFF;
|
||||
(selected?g.setColor(g.theme.fgH):g.setColor(g.theme.bg)).fillRect(x+11,y+3,x+60,y+52);
|
||||
g.clearRect(x+12,y+4,x+59,y+51);
|
||||
g.setColor(g.theme.fg);
|
||||
try{g.drawImage(apps[p*4+n].icon,x+12,y+4);} catch(e){}
|
||||
g.setFontAlign(0,-1,0).setFont("6x8",1);
|
||||
var txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ");
|
||||
var lineY = 0;
|
||||
var line = "";
|
||||
while (txt.length > 0){
|
||||
var c = txt.shift();
|
||||
|
||||
let txt = apps[p*4+n].name.replace(/([a-z])([A-Z])/g, "$1 $2").split(" ");
|
||||
let lineY = 0;
|
||||
let line = "";
|
||||
while (txt.length > 0){
|
||||
let c = txt.shift();
|
||||
if (c.length + 1 + line.length > 13){
|
||||
if (line.length > 0){
|
||||
g.drawString(line.trim(),x+36,y+54+lineY*8);
|
||||
|
@ -67,29 +65,34 @@ function draw_icon(p,n,selected) {
|
|||
}
|
||||
}
|
||||
g.drawString(line.trim(),x+36,y+54+lineY*8);
|
||||
}
|
||||
};
|
||||
|
||||
function drawPage(p){
|
||||
let drawPage = function(p){
|
||||
g.reset();
|
||||
g.clearRect(0,24,175,175);
|
||||
var O = 88+YOFF/2-12*(Npages/2);
|
||||
for (var j=0;j<Npages;j++){
|
||||
var y = O+j*12;
|
||||
let O = 88+YOFF/2-12*(Npages/2);
|
||||
for (let j=0;j<Npages;j++){
|
||||
let y = O+j*12;
|
||||
g.setColor(g.theme.fg);
|
||||
if (j==page) g.fillCircle(XOFF/2,y,4);
|
||||
else g.drawCircle(XOFF/2,y,4);
|
||||
}
|
||||
for (var i=0;i<4;i++) {
|
||||
for (let i=0;i<4;i++) {
|
||||
if (!apps[p*4+i]) return i;
|
||||
draw_icon(p,i,selected==i && !settings.direct);
|
||||
drawIcon(p,i,selected==i && !settings.direct);
|
||||
}
|
||||
g.flip();
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{
|
||||
Bangle.loadWidgets();
|
||||
//g.clear();
|
||||
//Bangle.drawWidgets();
|
||||
drawPage(0);
|
||||
|
||||
let swipeListenerDt = function(dirLeftRight, dirUpDown){
|
||||
selected = 0;
|
||||
oldselected=-1;
|
||||
if(settings.swipeExit && dirLeftRight==1) load();
|
||||
if(settings.swipeExit && dirLeftRight==1) returnToClock();
|
||||
if (dirUpDown==-1||dirLeftRight==-1){
|
||||
++page; if (page>maxPage) page=0;
|
||||
drawPage(page);
|
||||
|
@ -97,24 +100,24 @@ Bangle.on("swipe",(dirLeftRight, dirUpDown)=>{
|
|||
--page; if (page<0) page=maxPage;
|
||||
drawPage(page);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function isTouched(p,n){
|
||||
let isTouched = function(p,n){
|
||||
if (n<0 || n>3) return false;
|
||||
var x1 = (n%2)*72+XOFF; var y1 = n>1?72+YOFF:YOFF;
|
||||
var x2 = x1+71; var y2 = y1+81;
|
||||
let x1 = (n%2)*72+XOFF; let y1 = n>1?72+YOFF:YOFF;
|
||||
let x2 = x1+71; let y2 = y1+81;
|
||||
return (p.x>x1 && p.y>y1 && p.x<x2 && p.y<y2);
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.on("touch",(_,p)=>{
|
||||
var i;
|
||||
let touchListenerDt = function(_,p){
|
||||
let i;
|
||||
for (i=0;i<4;i++){
|
||||
if((page*4+i)<Napps){
|
||||
if (isTouched(p,i)) {
|
||||
draw_icon(page,i,true && !settings.direct);
|
||||
drawIcon(page,i,true && !settings.direct);
|
||||
if (selected>=0 || settings.direct) {
|
||||
if (selected!=i && !settings.direct){
|
||||
draw_icon(page,selected,false);
|
||||
drawIcon(page,selected,false);
|
||||
} else {
|
||||
load(apps[page*4+i].src);
|
||||
}
|
||||
|
@ -125,12 +128,32 @@ Bangle.on("touch",(_,p)=>{
|
|||
}
|
||||
}
|
||||
if ((i==4 || (page*4+i)>Napps) && selected>=0) {
|
||||
draw_icon(page,selected,false);
|
||||
drawIcon(page,selected,false);
|
||||
selected=-1;
|
||||
}
|
||||
};
|
||||
|
||||
const returnToClock = function() {
|
||||
Bangle.setUI();
|
||||
setTimeout(eval, 0, s.read(".bootcde"));
|
||||
};
|
||||
|
||||
Bangle.setUI({
|
||||
mode : 'custom',
|
||||
back : returnToClock,
|
||||
swipe : swipeListenerDt,
|
||||
touch : touchListenerDt
|
||||
});
|
||||
|
||||
Bangle.loadWidgets();
|
||||
g.clear();
|
||||
Bangle.drawWidgets();
|
||||
drawPage(0);
|
||||
// taken from Icon Launcher with minor alterations
|
||||
var timeoutToClock;
|
||||
const updateTimeoutToClock = function(){
|
||||
if (settings.timeOut!="Off"){
|
||||
let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt
|
||||
if (timeoutToClock) clearTimeout(timeoutToClock);
|
||||
timeoutToClock = setTimeout(returnToClock,time*1000);
|
||||
}
|
||||
};
|
||||
updateTimeoutToClock();
|
||||
|
||||
} // end of app scope
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "dtlaunch",
|
||||
"name": "Desktop Launcher",
|
||||
"version": "0.16",
|
||||
"version": "0.18",
|
||||
"description": "Desktop style App Launcher with six (four for Bangle 2) apps per page - fast access if you have lots of apps installed.",
|
||||
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
|
||||
"icon": "icon.png",
|
||||
|
|
|
@ -5,51 +5,56 @@
|
|||
showClocks: true,
|
||||
showLaunchers: true,
|
||||
direct: false,
|
||||
oneClickExit:false,
|
||||
swipeExit: false
|
||||
swipeExit: false,
|
||||
timeOut: "Off"
|
||||
}, require('Storage').readJSON(FILE, true) || {});
|
||||
|
||||
function writeSettings() {
|
||||
require('Storage').writeJSON(FILE, settings);
|
||||
}
|
||||
|
||||
const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"];
|
||||
|
||||
E.showMenu({
|
||||
"" : { "title" : "Desktop launcher" },
|
||||
"< Back" : () => back(),
|
||||
'Show clocks': {
|
||||
/*LANG*/"< Back" : () => back(),
|
||||
/*LANG*/'Show clocks': {
|
||||
value: settings.showClocks,
|
||||
onchange: v => {
|
||||
settings.showClocks = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Show launchers': {
|
||||
/*LANG*/'Show launchers': {
|
||||
value: settings.showLaunchers,
|
||||
onchange: v => {
|
||||
settings.showLaunchers = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Direct launch': {
|
||||
/*LANG*/'Direct launch': {
|
||||
value: settings.direct,
|
||||
onchange: v => {
|
||||
settings.direct = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'Swipe Exit': {
|
||||
/*LANG*/'Swipe Exit': {
|
||||
value: settings.swipeExit,
|
||||
onchange: v => {
|
||||
settings.swipeExit = v;
|
||||
writeSettings();
|
||||
}
|
||||
},
|
||||
'One click exit': {
|
||||
value: settings.oneClickExit,
|
||||
/*LANG*/'Time Out': { // Adapted from Icon Launcher
|
||||
value: timeOutChoices.indexOf(settings.timeOut),
|
||||
min: 0,
|
||||
max: timeOutChoices.length-1,
|
||||
format: v => timeOutChoices[v],
|
||||
onchange: v => {
|
||||
settings.oneClickExit = v;
|
||||
settings.timeOut = timeOutChoices[v];
|
||||
writeSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
});
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.1: New App!
|
|
@ -0,0 +1,9 @@
|
|||
Enton - Enhanced Anton Clock
|
||||
|
||||
This clock face is based on the 'Anton Clock'.
|
||||
|
||||
Things I changed:
|
||||
|
||||
- The main font for the time is now Audiowide
|
||||
- Removed the written out day name and replaced it with steps and bpm
|
||||
- Changed the date string to a (for me) more readable string
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkE/4A/AH4A/AH4A/AH4Aw+cikf/mQDCAAIFBAwQDBBYgXCgEDAQIABn4JBkAFBgIKDgQwFmMD+UCmcgl/zEIMzmcQmYKBmYiCAAfxC4QrBl8wBwcgkYsGC4sAiMAF4UxiIGBn8QAgMSC48wgMRiEDBAISCiYcFC48v//yC4PzgJAGiAXIiczPgPzC4JyBmf/AYQXI+KcCj8wmYFCgEjAYQ3G+cjbQIABJIMzAoUin7XIADpSEK4rWGI4MhmRJBn8j+U/d4MimUTkUzIw5dBl4UBMgIXBAgMyLYKOBmQXHiSbCDgMyl8z+UjmJ1BHgJbHCgM/IYQABAgQJBYYYA/AH4AtaQU/mTvBBozWBd44KBkUSkLnBEo8jkcvBI0/CgMiDAIXHHYIXImUzJQJHH+Y+Bn6Z/ABQA=="))
|
|
@ -0,0 +1,67 @@
|
|||
Graphics.prototype.setFontAudiowide = function() {
|
||||
// Actual height 33 (36 - 4)
|
||||
var widths = atob("CiAsESQjJSQkHyQkDA==");
|
||||
var font = atob("AAAAAAAAAAAAAAAAAAAAAPAAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAfgAAAAAAPAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAADgAAAAAAHgAAAAAAfgAAAAAA/gAAAAAD/gAAAAAH/gAAAAAf/AAAAAB/8AAAAAD/4AAAAAP/gAAAAAf/AAAAAB/8AAAAAD/4AAAAAP/gAAAAAf+AAAAAB/8AAAAAH/wAAAAAP/gAAAAA/+AAAAAB/8AAAAAD/wAAAAAD/gAAAAAD+AAAAAAD4AAAAAADwAAAAAADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD/AAAAAA//+AAAAB///AAAAH///wAAAP///4AAAf///8AAA////+AAA/4AP+AAB/gAD/AAB/AA9/AAD+AB+/gAD+AD+/gAD+AD+/gAD8AH+fgAD8AP8fgAD8AP4fgAD8Af4fgAD8A/wfgAD8A/gfgAD8B/gfgAD8D/AfgAD8D+AfgAD8H+AfgAD8P8AfgAD8P4AfgAD8f4AfgAD8/wAfgAD8/gAfgAD+/gA/gAD+/AA/gAB/eAB/AAB/sAD/AAB/wAH/AAA////+AAAf///8AAAP///4AAAH///wAAAD///gAAAA//+AAAAAP/4AAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//gAAAAH//gAAAAP//gAD8Af//gAD8A///gAD8B///gAD8B///gAD8B/AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+D+AfgAD//+AfgAD//+AfgAB//8AfgAA//4AfgAAf/wAfgAAP/gAfgAAB8AAfgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+B+A/gAD/////gAB/////AAB/////AAA////+AAAf///8AAAP///4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//4AAAAD//8AAAAD//+AAAAD//+AAAAD//+AAAAD//+AAAAD//+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAAAB+AAAAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAD/////gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD//AAfgAD//wAfgAD//4AfgAD//8AfgAD//8AfgAD//+AfgAD8D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B/A/gAD8B///gAD8B///gAD8A///AAD8A///AAAAAf/+AAAAAP/4AAAAAD/gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB///AAAAH///wAAAf///8AAAf///8AAA////+AAB/////AAB/h+H/AAD/B+B/gAD+B+A/gAD+B+A/gAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B/A/gAD8B///gAD8B///gAD8A///AAAAAf//AAAAAf/+AAAAAH/4AAAAAB/gAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAAAD8AAAAgAD8AAABgAD8AAAHgAD8AAAfgAD8AAA/gAD8AAD/gAD8AAP/gAD8AA//gAD8AB//AAD8AH/8AAD8Af/wAAD8A//AAAD8D/+AAAD8P/4AAAD8f/gAAAD9//AAAAD//8AAAAD//wAAAAD//gAAAAD/+AAAAAD/4AAAAAD/wAAAAAD/AAAAAAD8AAAAAAA4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAH/4AAAAAP/8AAAH+f/+AAAf////AAA/////gAB/////gAB///A/gAD//+AfgAD//+AfgAD+D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+D+AfgAD//+AfgAD//+AfgAB///A/gAB/////gAA/////AAAP////AAAD+f/+AAAAAP/8AAAAAH/4AAAAAA+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAH/AAAAAAf/wAAAAA//4AAAAB//8AAAAB//8AfgAD//+AfgAD/D+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD8B+AfgAD+B+A/gAD+B+A/gAD/B+B/gAB/////AAB/////AAA////+AAAf///8AAAP///4AAAH///wAAAB///AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAeAAPAAAA/AAfgAAA/AAfgAAA/AAfgAAA/AAfgAAAeAAPAAAAAAAAAAAAAAAAAAAAAAAAAA");
|
||||
var scale = 1; // size multiplier for this font
|
||||
g.setFontCustom(font, 46, widths, 48+(scale<<8)+(1<<16));
|
||||
};
|
||||
|
||||
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 "?";
|
||||
}
|
||||
|
||||
return Math.round(steps);
|
||||
}
|
||||
|
||||
{ // must be inside our own scope here so that when we are unloaded everything disappears
|
||||
// we also define functions using 'let fn = function() {..}' for the same reason. function decls are global
|
||||
let drawTimeout;
|
||||
|
||||
|
||||
// Actually draw the watch face
|
||||
let draw = function() {
|
||||
var x = g.getWidth() / 2;
|
||||
var y = g.getHeight() / 2;
|
||||
g.reset().clearRect(Bangle.appRect); // clear whole background (w/o widgets)
|
||||
var date = new Date();
|
||||
var timeStr = require("locale").time(date, 1); // Hour and minute
|
||||
g.setFontAlign(0, 0).setFont("Audiowide").drawString(timeStr, x, y);
|
||||
var dateStr = require("locale").date(date, 1).toUpperCase();
|
||||
g.setFontAlign(0, 0).setFont("6x8", 2).drawString(dateStr, x, y+28);
|
||||
g.setFontAlign(0, 0).setFont("6x8", 2);
|
||||
g.drawString(getSteps(), 50, y+70);
|
||||
g.drawString(Math.round(Bangle.getHealthStatus("last").bpm), g.getWidth() -37, y + 70);
|
||||
|
||||
// queue next draw
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, 60000 - (Date.now() % 60000));
|
||||
};
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
Bangle.setUI({
|
||||
mode : "clock",
|
||||
remove : function() {
|
||||
// Called to unload all of the clock app
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
delete Graphics.prototype.setFontAnton;
|
||||
}});
|
||||
// Load widgets
|
||||
Bangle.loadWidgets();
|
||||
draw();
|
||||
setTimeout(Bangle.drawWidgets,0);
|
||||
}
|
After Width: | Height: | Size: 905 B |
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"id": "entonclk",
|
||||
"name": "Enton Clock",
|
||||
"version": "0.1",
|
||||
"description": "A simple clock using the Audiowide font. ",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot.png"}],
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": ["BANGLEJS2"],
|
||||
"allow_emulator": true,
|
||||
"readme":"README.md",
|
||||
"storage": [
|
||||
{"name":"entonclk.app.js","url":"app.js"},
|
||||
{"name":"entonclk.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 2.0 KiB |
|
@ -0,0 +1,2 @@
|
|||
0.01: New app!
|
||||
0.02: Submitted to app loader
|
|
@ -0,0 +1,18 @@
|
|||
# Gallery
|
||||
|
||||
A simple gallery app
|
||||
|
||||
## Usage
|
||||
|
||||
Upon opening the gallery app, you will be presented with a list of images that you can display. Tap the image to show it. Brightness will be set to full, and the screen timeout will be disabled. When you are done viewing the image, you can tap the screen to go back to the list of images. Press BTN1 to flip the image upside down.
|
||||
|
||||
## Adding images
|
||||
|
||||
1. The gallery app does not perform any scaling, and does not support panning. Therefore, you should use your favorite image editor to produce an image of the appropriate size for your watch. (240x240 for Bangle 1 or 176x176 for Bangle 2.) How you achieve this is up to you. If on a Bangle 2, I recommend adjusting the colors here to comply with the color restrictions.
|
||||
|
||||
2. Upload your image to the [Espruino image converter](https://www.espruino.com/Image+Converter). I recommend enabling compression and choosing one of the following color settings:
|
||||
* 16 bit RGB565 for Bangle 1
|
||||
* 3 bit RGB for Bangle 2
|
||||
* 1 bit black/white for monochrome images that you want to respond to your system theme. (White will be rendered as your foreground color and black will be rendered as your background color.)
|
||||
|
||||
3. Set the output format to an image string, copy it into the [IDE](https://www.espruino.com/ide/), and set the destination to a file in storage. The file name should begin with "gal-" (without the quotes) and end with ".img" (without the quotes) to appear in the gallery. Note that the gal- prefix and .img extension will be removed in the UI. Upload the file.
|
|
@ -0,0 +1,52 @@
|
|||
const storage = require('Storage');
|
||||
|
||||
let imageFiles = storage.list(/^gal-.*\.img/).sort();
|
||||
|
||||
let imageMenu = { '': { 'title': 'Gallery' } };
|
||||
|
||||
for (let fileName of imageFiles) {
|
||||
let displayName = fileName.substr(4, fileName.length - 8); // Trim off the 'gal-' and '.img' for a friendly display name
|
||||
imageMenu[displayName] = eval(`() => { drawImage("${fileName}"); }`); // Unfortunately, eval is the only reasonable way to do this
|
||||
}
|
||||
|
||||
let cachedOptions = Bangle.getOptions(); // We will change the backlight and timeouts later, and need to restore them when displaying the menu
|
||||
let backlightSetting = storage.readJSON('setting.json').brightness; // LCD brightness is not included in there for some reason
|
||||
|
||||
let angle = 0; // Store the angle of rotation
|
||||
let image; // Cache the image here because we access it in multiple places
|
||||
|
||||
function drawMenu() {
|
||||
Bangle.removeListener('touch', drawMenu); // We no longer want touching to reload the menu
|
||||
Bangle.setOptions(cachedOptions); // The drawImage function set no timeout, undo that
|
||||
Bangle.setLCDBrightness(backlightSetting); // Restore backlight
|
||||
image = undefined; // Delete the image from memory
|
||||
|
||||
E.showMenu(imageMenu);
|
||||
}
|
||||
|
||||
function drawImage(fileName) {
|
||||
E.showMenu(); // Remove the menu to prevent it from breaking things
|
||||
setTimeout(() => { Bangle.on('touch', drawMenu); }, 300); // Touch the screen to go back to the image menu (300ms timeout to allow user to lift finger)
|
||||
Bangle.setOptions({ // Disable display power saving while showing the image
|
||||
lockTimeout: 0,
|
||||
lcdPowerTimeout: 0,
|
||||
backlightTimeout: 0
|
||||
});
|
||||
Bangle.setLCDBrightness(1); // Full brightness
|
||||
|
||||
image = eval(storage.read(fileName)); // Sadly, the only reasonable way to do this
|
||||
g.clear().reset().drawImage(image, 88, 88, { rotate: angle });
|
||||
}
|
||||
|
||||
setWatch(info => {
|
||||
if (image) {
|
||||
if (angle == 0) angle = Math.PI;
|
||||
else angle = 0;
|
||||
Bangle.buzz();
|
||||
|
||||
g.clear().reset().drawImage(image, 88, 88, { rotate: angle })
|
||||
}
|
||||
}, BTN1, { repeat: true });
|
||||
|
||||
// We don't load the widgets because there is no reasonable way to unload them
|
||||
drawMenu();
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwgIOLgf/AAX8Av4FBJgkMAos/CIfMAv4Fe4AF/Apq5EAAw"))
|
After Width: | Height: | Size: 249 B |
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"id": "gallery",
|
||||
"name": "Gallery",
|
||||
"version": "0.02",
|
||||
"description": "A gallery that lets you view images uploaded with the IDE (see README)",
|
||||
"readme": "README.md",
|
||||
"icon": "icon.png",
|
||||
"type": "app",
|
||||
"tags": "tools",
|
||||
"supports": [
|
||||
"BANGLEJS2",
|
||||
"BANGLEJS"
|
||||
],
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{
|
||||
"name": "gallery.app.js",
|
||||
"url": "app.js"
|
||||
},
|
||||
{
|
||||
"name": "gallery.img",
|
||||
"url": "icon.js",
|
||||
"evaluate": true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -6,3 +6,5 @@
|
|||
0.05: Added adjustment for Bangle.js magnetometer heading fix
|
||||
0.06: Fix waypoint menu always selecting last waypoint
|
||||
Fix widget adding listeners more than once
|
||||
0.07: Show checkered flag for target markers
|
||||
Single waypoints are now shown in the compass view
|
||||
|
|
|
@ -10,7 +10,7 @@ Tapping or button to switch to the next information display, swipe right for the
|
|||
|
||||
Choose either a route or a waypoint as basis for the display.
|
||||
|
||||
After this selection and availability of a GPS fix the compass will show a blue dot for your destination and a green one for possibly available waypoints on the way.
|
||||
After this selection and availability of a GPS fix the compass will show a checkered flag for your destination and a green dot for possibly available waypoints on the way.
|
||||
Waypoints are shown with name if available and distance to waypoint.
|
||||
|
||||
As long as no GPS signal is available the compass shows the heading from the build in magnetometer. When a GPS fix becomes available, the compass display shows the GPS course. This can be differentiated by the display of bubble levels on top and sides of the compass.
|
||||
|
|
|
@ -239,8 +239,14 @@ function getCompassSlice(compassDataSource){
|
|||
} else {
|
||||
bpos=Math.round(bpos*increment);
|
||||
}
|
||||
graphics.setColor(p.color);
|
||||
graphics.fillCircle(bpos,y+height-12,Math.floor(width*0.03));
|
||||
if (p.color){
|
||||
graphics.setColor(p.color);
|
||||
}
|
||||
if (p.icon){
|
||||
graphics.drawImage(p.icon, bpos,y+height-12, {rotate:0,scale:2});
|
||||
} else {
|
||||
graphics.fillCircle(bpos,y+height-12,Math.floor(width*0.03));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (compassDataSource.getMarkers){
|
||||
|
@ -595,8 +601,8 @@ function showBackgroundMenu(){
|
|||
"title" : "Background",
|
||||
back : showMenu,
|
||||
},
|
||||
"Start" : ()=>{ E.showPrompt("Start?").then((v)=>{ if (v) {WIDGETS.gpstrek.start(true); removeMenu();} else {E.showMenu(mainmenu);}});},
|
||||
"Stop" : ()=>{ E.showPrompt("Stop?").then((v)=>{ if (v) {WIDGETS.gpstrek.stop(true); removeMenu();} else {E.showMenu(mainmenu);}});},
|
||||
"Start" : ()=>{ E.showPrompt("Start?").then((v)=>{ if (v) {WIDGETS.gpstrek.start(true); removeMenu();} else {showMenu();}}).catch(()=>{E.showMenu(mainmenu);});},
|
||||
"Stop" : ()=>{ E.showPrompt("Stop?").then((v)=>{ if (v) {WIDGETS.gpstrek.stop(true); removeMenu();} else {showMenu();}}).catch(()=>{E.showMenu(mainmenu);});},
|
||||
};
|
||||
E.showMenu(menu);
|
||||
}
|
||||
|
@ -677,13 +683,15 @@ function setClosestWaypoint(route, startindex, progress){
|
|||
|
||||
let screen = 1;
|
||||
|
||||
const finishIcon = atob("CggB//meZmeZ+Z5n/w==");
|
||||
|
||||
const compassSliceData = {
|
||||
getCourseType: function(){
|
||||
return (state.currentPos && state.currentPos.course) ? "GPS" : "MAG";
|
||||
},
|
||||
getCourse: function (){
|
||||
if(compassSliceData.getCourseType() == "GPS") return state.currentPos.course;
|
||||
return state.compassHeading?360-state.compassHeading:undefined;
|
||||
return state.compassHeading?state.compassHeading:undefined;
|
||||
},
|
||||
getPoints: function (){
|
||||
let points = [];
|
||||
|
@ -691,7 +699,10 @@ const compassSliceData = {
|
|||
points.push({bearing:bearing(state.currentPos, state.route.currentWaypoint), color:"#0f0"});
|
||||
}
|
||||
if (state.currentPos && state.currentPos.lon && state.route){
|
||||
points.push({bearing:bearing(state.currentPos, getLast(state.route)), color:"#00f"});
|
||||
points.push({bearing:bearing(state.currentPos, getLast(state.route)), icon: finishIcon});
|
||||
}
|
||||
if (state.currentPos && state.currentPos.lon && state.waypoint){
|
||||
points.push({bearing:bearing(state.currentPos, state.waypoint), icon: finishIcon});
|
||||
}
|
||||
return points;
|
||||
},
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "gpstrek",
|
||||
"name": "GPS Trekking",
|
||||
"version": "0.06",
|
||||
"version": "0.07",
|
||||
"description": "Helper for tracking the status/progress during hiking. Do NOT depend on this for navigation!",
|
||||
"icon": "icon.png",
|
||||
"screenshots": [{"url":"screen1.png"},{"url":"screen2.png"},{"url":"screen3.png"},{"url":"screen4.png"}],
|
||||
|
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.8 KiB |
|
@ -24,7 +24,7 @@ function onGPS(fix) {
|
|||
}
|
||||
|
||||
function onMag(e) {
|
||||
if (!state.compassHeading) state.compassHeading = 360-e.heading;
|
||||
if (!state.compassHeading) state.compassHeading = e.heading;
|
||||
|
||||
//if (a+180)mod 360 == b then
|
||||
//return (a+b)/2 mod 360 and ((a+b)/2 mod 360) + 180 (they are both the solution, so you may choose one depending if you prefer counterclockwise or clockwise direction)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
0.01: New App!
|
|
@ -0,0 +1,7 @@
|
|||
# Henkinen
|
||||
|
||||
By Jukio Kallio
|
||||
|
||||
A tiny app helping you to breath and relax.
|
||||
|
||||

|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwkEogA0/4AKCpNPCxYAB+gtTGJQuOGBAWPGAwuQGAwXH+cykc/C6UhgMSkMQiQXKBQsgiYFDmMCMBIIEmAWEDAUDC5nzBwogDMYgXHBoohJC4wuJEQwXG+ALDmUQgMjEYcPC5MhAYXxgAACj4ICVYYXGIwXzCwYABHAUwC5HyEwXwC4pEC+MvC4/xEoUQC4sBHIQlCC4vwIxBIEGYQXFmJKCC45ECfQQXIRoiRGC5EiOxB4EBwQXdI653XU67XX+QJCPAwrC+JKCC4v/gZIIHIUwCAQXGkIDCSIg4C/8SC5PwEwX/mUQgMjAwXzJQQXH+ZICAA8wEYYXGBgoAEEQoXHGBIhFC44OBcgQADmIgFC5H/kAYEmMCBooXDp4KFkMBiUhiCjDAAX0C5RjBmUjPo4XMABQXEMAwALCwgwRFwowRCwwwPFw4xOCpIArA"))
|
|
@ -0,0 +1,127 @@
|
|||
// Henkinen
|
||||
//
|
||||
// Bangle.js 2 breathing helper
|
||||
// by Jukio Kallio
|
||||
// www.jukiokallio.com
|
||||
|
||||
require("FontHaxorNarrow7x17").add(Graphics);
|
||||
|
||||
// settings
|
||||
const breath = {
|
||||
theme: "default",
|
||||
x:0, y:0, w:0, h:0,
|
||||
size: 60,
|
||||
|
||||
bgcolor: g.theme.bg,
|
||||
incolor: g.theme.fg,
|
||||
keepcolor: g.theme.fg,
|
||||
outcolor: g.theme.fg,
|
||||
|
||||
font: "HaxorNarrow7x17", fontsize: 1,
|
||||
textcolor: g.theme.fg,
|
||||
texty: 18,
|
||||
|
||||
in: 4000,
|
||||
keep: 7000,
|
||||
out: 8000
|
||||
};
|
||||
|
||||
// set some additional settings
|
||||
breath.w = g.getWidth(); // size of the background
|
||||
breath.h = g.getHeight();
|
||||
breath.x = breath.w * 0.5; // position of the circles
|
||||
breath.y = breath.h * 0.45;
|
||||
breath.texty = breath.y + breath.size + breath.texty; // text position
|
||||
|
||||
var wait = 100; // wait time, normally a minute
|
||||
var time = 0; // for time keeping
|
||||
|
||||
|
||||
// timeout used to update every minute
|
||||
var drawTimeout;
|
||||
|
||||
// schedule a draw for the next minute
|
||||
function queueDraw() {
|
||||
if (drawTimeout) clearTimeout(drawTimeout);
|
||||
drawTimeout = setTimeout(function() {
|
||||
drawTimeout = undefined;
|
||||
draw();
|
||||
}, wait - (Date.now() % wait));
|
||||
}
|
||||
|
||||
|
||||
// main function
|
||||
function draw() {
|
||||
// make date object
|
||||
var date = new Date();
|
||||
|
||||
// update current time
|
||||
time += wait - (Date.now() % wait);
|
||||
if (time > breath.in + breath.keep + breath.out) time = 0; // reset time
|
||||
|
||||
// Reset the state of the graphics library
|
||||
g.reset();
|
||||
|
||||
// Clear the area where we want to draw the time
|
||||
g.setColor(breath.bgcolor);
|
||||
g.fillRect(0, 0, breath.w, breath.h);
|
||||
|
||||
// calculate circle size
|
||||
var circle = 0;
|
||||
if (time < breath.in) {
|
||||
// breath in
|
||||
circle = time / breath.in;
|
||||
g.setColor(breath.incolor);
|
||||
|
||||
} else if (time < breath.in + breath.keep) {
|
||||
// keep breath
|
||||
circle = 1;
|
||||
g.setColor(breath.keepcolor);
|
||||
|
||||
} else if (time < breath.in + breath.keep + breath.out) {
|
||||
// breath out
|
||||
circle = ((breath.in + breath.keep + breath.out) - time) / breath.out;
|
||||
g.setColor(breath.outcolor);
|
||||
|
||||
}
|
||||
|
||||
// draw breath circle
|
||||
g.fillCircle(breath.x, breath.y, breath.size * circle);
|
||||
|
||||
// breath area
|
||||
g.setColor(breath.textcolor);
|
||||
g.drawCircle(breath.x, breath.y, breath.size);
|
||||
|
||||
// draw text
|
||||
g.setFontAlign(0,0).setFont(breath.font, breath.fontsize).setColor(breath.textcolor);
|
||||
|
||||
if (time < breath.in) {
|
||||
// breath in
|
||||
g.drawString("Breath in", breath.x, breath.texty);
|
||||
|
||||
} else if (time < breath.in + breath.keep) {
|
||||
// keep breath
|
||||
g.drawString("Keep it in", breath.x, breath.texty);
|
||||
|
||||
} else if (time < breath.in + breath.keep + breath.out) {
|
||||
// breath out
|
||||
g.drawString("Breath out", breath.x, breath.texty);
|
||||
|
||||
}
|
||||
|
||||
// queue draw
|
||||
queueDraw();
|
||||
}
|
||||
|
||||
|
||||
// Clear the screen once, at startup
|
||||
g.clear();
|
||||
// draw immediately at first
|
||||
draw();
|
||||
|
||||
|
||||
// keep LCD on
|
||||
Bangle.setLCDPower(1);
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
Bangle.setUI("clock");
|
After Width: | Height: | Size: 2.9 KiB |
|
@ -0,0 +1,15 @@
|
|||
{ "id": "henkinen",
|
||||
"name": "Henkinen - Tiny Breathing Helper",
|
||||
"shortName":"Henkinen",
|
||||
"version":"0.01",
|
||||
"description": "A tiny app helping you to breath and relax.",
|
||||
"icon": "app.png",
|
||||
"screenshots": [{"url":"screenshot1.png"}],
|
||||
"tags": "outdoors",
|
||||
"supports" : ["BANGLEJS","BANGLEJS2"],
|
||||
"readme": "README.md",
|
||||
"storage": [
|
||||
{"name":"henkinen.app.js","url":"app.js"},
|
||||
{"name":"henkinen.img","url":"app-icon.js","evaluate":true}
|
||||
]
|
||||
}
|
After Width: | Height: | Size: 13 KiB |
|
@ -8,3 +8,9 @@
|
|||
Add swipe-to-exit
|
||||
0.08: Only use fast loading for switching to clock to prevent problems in full screen apps
|
||||
0.09: Remove fast load option since clocks containing Bangle.loadWidgets are now always normally loaded
|
||||
0.10: changed the launch.json file name in iconlaunch.json ( launch.cache.json -> iconlaunch.cache.json)
|
||||
used Object.assing for the settings
|
||||
fix cache not deleted when "showClocks" options is changed
|
||||
added timeOut to return to the clock
|
||||
0.11: Cleanup timeout when changing to clock
|
||||
Reset timeout on swipe and drag
|
||||
|
|
|
@ -1,12 +1,20 @@
|
|||
{
|
||||
const s = require("Storage");
|
||||
const settings = s.readJSON("launch.json", true) || { showClocks: true, fullscreen: false,direct:false,swipeExit:false,oneClickExit:false};
|
||||
const settings = Object.assign({
|
||||
showClocks: true,
|
||||
fullscreen: false,
|
||||
direct: false,
|
||||
oneClickExit: false,
|
||||
swipeExit: false,
|
||||
timeOut:"Off"
|
||||
}, s.readJSON("iconlaunch.json", true) || {});
|
||||
|
||||
if (!settings.fullscreen) {
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
}
|
||||
let launchCache = s.readJSON("launch.cache.json", true)||{};
|
||||
let launchHash = require("Storage").hash(/\.info/);
|
||||
let launchCache = s.readJSON("iconlaunch.cache.json", true)||{};
|
||||
let launchHash = s.hash(/\.info/);
|
||||
if (launchCache.hash!=launchHash) {
|
||||
launchCache = {
|
||||
hash : launchHash,
|
||||
|
@ -20,7 +28,7 @@
|
|||
if (a.name>b.name) return 1;
|
||||
return 0;
|
||||
}) };
|
||||
s.writeJSON("launch.cache.json", launchCache);
|
||||
s.writeJSON("iconlaunch.cache.json", launchCache);
|
||||
}
|
||||
let scroll = 0;
|
||||
let selectedItem = -1;
|
||||
|
@ -124,6 +132,7 @@
|
|||
g.flip();
|
||||
const itemsN = Math.ceil(launchCache.apps.length / appsN);
|
||||
let onDrag = function(e) {
|
||||
updateTimeout();
|
||||
g.setColor(g.theme.fg);
|
||||
g.setBgColor(g.theme.bg);
|
||||
let dy = e.dy;
|
||||
|
@ -173,6 +182,7 @@
|
|||
drag: onDrag,
|
||||
touch: (_, e) => {
|
||||
if (e.y < R.y - 4) return;
|
||||
updateTimeout();
|
||||
let i = YtoIdx(e.y);
|
||||
selectItem(i, e);
|
||||
},
|
||||
|
@ -193,11 +203,23 @@
|
|||
delete idxToY;
|
||||
delete YtoIdx;
|
||||
delete settings;
|
||||
if (timeout) clearTimeout(timeout);
|
||||
setTimeout(eval, 0, s.read(".bootcde"));
|
||||
};
|
||||
|
||||
|
||||
if (settings.oneClickExit) mode.btn = returnToClock;
|
||||
|
||||
let timeout;
|
||||
const updateTimeout = function(){
|
||||
if (settings.timeOut!="Off"){
|
||||
let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt
|
||||
if (timeout) clearTimeout(timeout);
|
||||
timeout = setTimeout(returnToClock,time*1000);
|
||||
}
|
||||
}
|
||||
|
||||
updateTimeout();
|
||||
|
||||
Bangle.setUI(mode);
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "iconlaunch",
|
||||
"name": "Icon Launcher",
|
||||
"shortName" : "Icon launcher",
|
||||
"version": "0.09",
|
||||
"version": "0.11",
|
||||
"icon": "app.png",
|
||||
"description": "A launcher inspired by smartphones, with an icon-only scrollable menu.",
|
||||
"tags": "tool,system,launcher",
|
||||
|
@ -12,6 +12,7 @@
|
|||
{ "name": "iconlaunch.app.js", "url": "app.js" },
|
||||
{ "name": "iconlaunch.settings.js", "url": "settings.js" }
|
||||
],
|
||||
"data": [{"name":"iconlaunch.json"},{"name":"iconlaunch.cache.json"}],
|
||||
"screenshots": [{ "url": "screenshot1.png" }, { "url": "screenshot2.png" }],
|
||||
"readme": "README.md"
|
||||
}
|
||||
|
|
|
@ -1,24 +1,29 @@
|
|||
// make sure to enclose the function in parentheses
|
||||
(function(back) {
|
||||
const s = require("Storage");
|
||||
let settings = Object.assign({
|
||||
showClocks: true,
|
||||
fullscreen: false,
|
||||
direct: false,
|
||||
oneClickExit: false,
|
||||
swipeExit: false
|
||||
}, require("Storage").readJSON("launch.json", true) || {});
|
||||
swipeExit: false,
|
||||
timeOut:"Off"
|
||||
}, s.readJSON("iconlaunch.json", true) || {});
|
||||
|
||||
let fonts = g.getFonts();
|
||||
function save(key, value) {
|
||||
settings[key] = value;
|
||||
require("Storage").write("launch.json",settings);
|
||||
s.write("iconlaunch.json",settings);
|
||||
}
|
||||
const timeOutChoices = [/*LANG*/"Off", "10s", "15s", "20s", "30s"];
|
||||
const appMenu = {
|
||||
"": { "title": /*LANG*/"Launcher" },
|
||||
/*LANG*/"< Back": back,
|
||||
/*LANG*/"Show Clocks": {
|
||||
value: settings.showClocks == true,
|
||||
onchange: (m) => { save("showClocks", m) }
|
||||
onchange: (m) => {
|
||||
save("showClocks", m);
|
||||
s.erase("iconlaunch.cache.json"); //delete the cache app list
|
||||
}
|
||||
},
|
||||
/*LANG*/"Fullscreen": {
|
||||
value: settings.fullscreen == true,
|
||||
|
@ -35,7 +40,15 @@
|
|||
/*LANG*/"Swipe exit": {
|
||||
value: settings.swipeExit == true,
|
||||
onchange: m => { save("swipeExit", m) }
|
||||
}
|
||||
},
|
||||
/*LANG*/'Time Out': {
|
||||
value: timeOutChoices.indexOf(settings.timeOut),
|
||||
min: 0, max: timeOutChoices.length-1,
|
||||
format: v => timeOutChoices[v],
|
||||
onchange: m => {
|
||||
save("timeOut", timeOutChoices[m]);
|
||||
}
|
||||
},
|
||||
};
|
||||
E.showMenu(appMenu);
|
||||
});
|
||||
|
|
|
@ -12,3 +12,7 @@
|
|||
0.10: Fix clock not correctly refreshing when drawing in timeouts option is not on
|
||||
0.11: Additional option in customizer to force drawing directly
|
||||
Fix some problems in handling timeouts
|
||||
0.12: Use widget_utils module
|
||||
Fix colorsetting in promises in generated code
|
||||
Some performance improvements by caching lookups
|
||||
Activate UI after first draw is complete to prevent drawing over launcher
|
||||
|
|
|
@ -202,27 +202,39 @@ let firstDraw = true;
|
|||
let firstDigitY = element.Y;
|
||||
let imageIndex = element.ImageIndex ? element.ImageIndex : 0;
|
||||
|
||||
let firstImage;
|
||||
if (imageIndex){
|
||||
firstImage = getByPath(resources, [], "" + (0 + imageIndex));
|
||||
} else {
|
||||
firstImage = getByPath(resources, element.ImagePath, 0);
|
||||
let firstImage = element.cachedFirstImage;
|
||||
if (!firstImage && !element.cachedFirstImageMissing){
|
||||
if (imageIndex){
|
||||
firstImage = getByPath(resources, [], "" + (0 + imageIndex));
|
||||
} else {
|
||||
firstImage = getByPath(resources, element.ImagePath, 0);
|
||||
}
|
||||
element.cachedFirstImage = firstImage;
|
||||
if (!firstImage) element.cachedFirstImageMissing = true;
|
||||
}
|
||||
|
||||
let minusImage;
|
||||
if (imageIndexMinus){
|
||||
minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus));
|
||||
} else {
|
||||
minusImage = getByPath(resources, element.ImagePath, "minus");
|
||||
let minusImage = element.cachedMinusImage;
|
||||
if (!minusImage && !element.cachedMinusImageMissing){
|
||||
if (imageIndexMinus){
|
||||
minusImage = getByPath(resources, [], "" + (0 + imageIndexMinus));
|
||||
} else {
|
||||
minusImage = getByPath(resources, element.ImagePath, "minus");
|
||||
}
|
||||
element.cachedMinusImage = minusImage;
|
||||
if (!minusImage) element.cachedMinusImageMissing = true;
|
||||
}
|
||||
|
||||
let unitImage;
|
||||
let unitImage = element.cachedUnitImage;
|
||||
//print("Get image for unit", imageIndexUnit);
|
||||
if (imageIndexUnit !== undefined){
|
||||
unitImage = getByPath(resources, [], "" + (0 + imageIndexUnit));
|
||||
//print("Unit image is", unitImage);
|
||||
} else if (element.Unit){
|
||||
unitImage = getByPath(resources, element.ImagePath, getMultistate(element.Unit, "unknown"));
|
||||
if (!unitImage && !element.cachedUnitImageMissing){
|
||||
if (imageIndexUnit !== undefined){
|
||||
unitImage = getByPath(resources, [], "" + (0 + imageIndexUnit));
|
||||
//print("Unit image is", unitImage);
|
||||
} else if (element.Unit){
|
||||
unitImage = getByPath(resources, element.ImagePath, getMultistate(element.Unit, "unknown"));
|
||||
}
|
||||
unitImage = element.cachedUnitImage;
|
||||
if (!unitImage) element.cachedUnitImageMissing = true;
|
||||
}
|
||||
|
||||
let numberWidth = (numberOfDigits * firstImage.width) + (Math.max((numberOfDigits - 1),0) * spacing);
|
||||
|
@ -292,14 +304,7 @@ let firstDraw = true;
|
|||
if (resource){
|
||||
prepareImg(resource);
|
||||
//print("lastElem", typeof resource)
|
||||
if (resource) {
|
||||
element.cachedImage[cacheKey] = resource;
|
||||
//print("cache res ",typeof element.cachedImage[cacheKey]);
|
||||
} else {
|
||||
element.cachedImage[cacheKey] = null;
|
||||
//print("cache null",typeof element.cachedImage[cacheKey]);
|
||||
//print("Could not create image from", resource);
|
||||
}
|
||||
element.cachedImage[cacheKey] = resource;
|
||||
} else {
|
||||
//print("Could not get resource from", element, lastElem);
|
||||
}
|
||||
|
@ -604,18 +609,15 @@ let firstDraw = true;
|
|||
|
||||
promise.then(()=>{
|
||||
let currentDrawingTime = Date.now();
|
||||
if (showWidgets && global.WIDGETS){
|
||||
//print("Draw widgets");
|
||||
if (showWidgets){
|
||||
restoreWidgetDraw();
|
||||
Bangle.drawWidgets();
|
||||
g.setColor(g.theme.fg);
|
||||
g.drawLine(0,24,g.getWidth(),24);
|
||||
}
|
||||
lastDrawTime = Date.now() - start;
|
||||
isDrawing=false;
|
||||
firstDraw=false;
|
||||
requestRefresh = false;
|
||||
endPerfLog("initialDraw");
|
||||
if (!Bangle.uiRemove) setUi();
|
||||
}).catch((e)=>{
|
||||
print("Error during drawing", e);
|
||||
});
|
||||
|
@ -751,30 +753,19 @@ let firstDraw = true;
|
|||
|
||||
|
||||
let showWidgetsChanged = false;
|
||||
let currentDragDistance = 0;
|
||||
|
||||
let restoreWidgetDraw = function(){
|
||||
if (global.WIDGETS) {
|
||||
for (let w in global.WIDGETS) {
|
||||
let wd = global.WIDGETS[w];
|
||||
wd.draw = originalWidgetDraw[w];
|
||||
wd.area = originalWidgetArea[w];
|
||||
}
|
||||
}
|
||||
require("widget_utils").show();
|
||||
Bangle.drawWidgets();
|
||||
};
|
||||
|
||||
let handleDrag = function(e){
|
||||
//print("handleDrag");
|
||||
currentDragDistance += e.dy;
|
||||
if (Math.abs(currentDragDistance) < 10) return;
|
||||
dragDown = currentDragDistance > 0;
|
||||
currentDragDistance = 0;
|
||||
if (!showWidgets && dragDown){
|
||||
|
||||
let handleSwipe = function(lr, ud){
|
||||
if (!showWidgets && ud == 1){
|
||||
//print("Enable widgets");
|
||||
restoreWidgetDraw();
|
||||
showWidgetsChanged = true;
|
||||
}
|
||||
if (showWidgets && !dragDown){
|
||||
if (showWidgets && ud == -1){
|
||||
//print("Disable widgets");
|
||||
clearWidgetsDraw();
|
||||
firstDraw = true;
|
||||
|
@ -783,12 +774,12 @@ let firstDraw = true;
|
|||
if (showWidgetsChanged){
|
||||
showWidgetsChanged = false;
|
||||
//print("Draw after widget change");
|
||||
showWidgets = dragDown;
|
||||
showWidgets = ud == 1;
|
||||
initialDraw();
|
||||
}
|
||||
};
|
||||
|
||||
Bangle.on('drag', handleDrag);
|
||||
Bangle.on('swipe', handleSwipe);
|
||||
|
||||
if (!events || events.includes("pressure")){
|
||||
Bangle.on('pressure', handlePressure);
|
||||
|
@ -814,62 +805,54 @@ let firstDraw = true;
|
|||
|
||||
let clearWidgetsDraw = function(){
|
||||
//print("Clear widget draw calls");
|
||||
if (global.WIDGETS) {
|
||||
originalWidgetDraw = {};
|
||||
originalWidgetArea = {};
|
||||
for (let w in global.WIDGETS) {
|
||||
let wd = global.WIDGETS[w];
|
||||
originalWidgetDraw[w] = wd.draw;
|
||||
originalWidgetArea[w] = wd.area;
|
||||
wd.draw = () => {};
|
||||
wd.area = "";
|
||||
}
|
||||
}
|
||||
require("widget_utils").hide();
|
||||
}
|
||||
|
||||
handleLock(Bangle.isLocked(), true);
|
||||
|
||||
Bangle.setUI({
|
||||
mode : "clock",
|
||||
remove : function() {
|
||||
//print("remove calls");
|
||||
// Called to unload all of the clock app
|
||||
Bangle.setHRMPower(0, "imageclock");
|
||||
Bangle.setBarometerPower(0, 'imageclock');
|
||||
let setUi = function(){
|
||||
Bangle.setUI({
|
||||
mode : "clock",
|
||||
remove : function() {
|
||||
//print("remove calls");
|
||||
// Called to unload all of the clock app
|
||||
Bangle.setHRMPower(0, "imageclock");
|
||||
Bangle.setBarometerPower(0, 'imageclock');
|
||||
|
||||
Bangle.removeListener('drag', handleDrag);
|
||||
Bangle.removeListener('lock', handleLock);
|
||||
Bangle.removeListener('charging', handleCharging);
|
||||
Bangle.removeListener('HRM', handleHrm);
|
||||
Bangle.removeListener('pressure', handlePressure);
|
||||
Bangle.removeListener('swipe', handleSwipe);
|
||||
Bangle.removeListener('lock', handleLock);
|
||||
Bangle.removeListener('charging', handleCharging);
|
||||
Bangle.removeListener('HRM', handleHrm);
|
||||
Bangle.removeListener('pressure', handlePressure);
|
||||
|
||||
if (deferredTimout) clearTimeout(deferredTimout);
|
||||
if (initialDrawTimeoutUnlocked) clearTimeout(initialDrawTimeoutUnlocked);
|
||||
if (initialDrawTimeoutLocked) clearTimeout(initialDrawTimeoutLocked);
|
||||
if (deferredTimout) clearTimeout(deferredTimout);
|
||||
if (initialDrawTimeoutUnlocked) clearTimeout(initialDrawTimeoutUnlocked);
|
||||
if (initialDrawTimeoutLocked) clearTimeout(initialDrawTimeoutLocked);
|
||||
|
||||
for (let i of unlockedDrawInterval){
|
||||
//print("Clearing unlocked", i);
|
||||
clearInterval(i);
|
||||
for (let i of global.unlockedDrawInterval){
|
||||
//print("Clearing unlocked", i);
|
||||
clearInterval(i);
|
||||
}
|
||||
delete global.unlockedDrawInterval;
|
||||
for (let i of global.lockedDrawInterval){
|
||||
//print("Clearing locked", i);
|
||||
clearInterval(i);
|
||||
}
|
||||
delete global.lockedDrawInterval;
|
||||
delete global.showWidgets;
|
||||
delete global.firstDraw;
|
||||
|
||||
delete Bangle.printPerfLog;
|
||||
if (settings.perflog){
|
||||
delete Bangle.resetPerfLog;
|
||||
delete performanceLog;
|
||||
}
|
||||
|
||||
cleanupDelays();
|
||||
restoreWidgetDraw();
|
||||
}
|
||||
delete unlockedDrawInterval;
|
||||
for (let i of lockedDrawInterval){
|
||||
//print("Clearing locked", i);
|
||||
clearInterval(i);
|
||||
}
|
||||
delete lockedDrawInterval;
|
||||
delete showWidgets;
|
||||
delete firstDraw;
|
||||
|
||||
delete Bangle.printPerfLog;
|
||||
if (settings.perflog){
|
||||
delete Bangle.resetPerfLog;
|
||||
delete performanceLog;
|
||||
}
|
||||
|
||||
cleanupDelays();
|
||||
restoreWidgetDraw();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Bangle.loadWidgets();
|
||||
clearWidgetsDraw();
|
||||
|
|
|
@ -714,13 +714,13 @@
|
|||
}
|
||||
|
||||
if (addDebug()) code += 'print("Element condition is ' + condition + '");' + "\n";
|
||||
code += "" + colorsetting;
|
||||
code += (condition.length > 0 ? "if (" + condition + "){\n" : "");
|
||||
if (wrapInTimeouts && (plane != 0 || forceUseOrigPlane)){
|
||||
code += "p = p.then(()=>delay(0)).then(()=>{\n";
|
||||
} else {
|
||||
code += "p = p.then(()=>{\n";
|
||||
}
|
||||
code += "" + colorsetting;
|
||||
if (addDebug()) code += 'print("Drawing element ' + elementIndex + ' with type ' + c.type + ' on plane ' + planeName + '");' + "\n";
|
||||
code += "draw" + c.type + "(" + planeName + ", wr, wf.Collapsed[" + elementIndex + "].value);\n";
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
"id": "imageclock",
|
||||
"name": "Imageclock",
|
||||
"shortName": "Imageclock",
|
||||
"version": "0.11",
|
||||
"version": "0.12",
|
||||
"type": "clock",
|
||||
"description": "BETA!!! File formats still subject to change --- This app is a highly customizable watchface. To use it, you need to select a watchface. You can build the watchfaces yourself without programming anything. All you need to do is write some json and create image files.",
|
||||
"icon": "app.png",
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
0.01: New app!
|
||||
0.02-0.07: Bug fixes
|
||||
0.08: Submitted to the app loader
|
|
@ -0,0 +1,33 @@
|
|||
# Informational clock
|
||||
|
||||
A configurable clock with extra info and shortcuts when unlocked, but large time when locked
|
||||
|
||||
## Information
|
||||
|
||||
The clock has two different screen arrangements, depending on whether the watch is locked or unlocked. The most commonly viewed piece of information is the time, so when the watch is locked it optimizes for the time being visible at a glance without the backlight. The hours and minutes take up nearly the entire top half of the display, with the date and seconds taking up nearly the entire bottom half. The day progress bar is between them if enabled, unless configured to be on the bottom row. The bottom row can be configured to display a weather summary, step count, step count and heart rate, the daily progress bar, or nothing.
|
||||
|
||||
When the watch is unlocked, it can be assumed that the backlight is on and the user is actively looking at the watch, so instead we can optimize for information density. The bottom half of the display becomes shortcuts, and the top half of the display becomes 4 rows of information (date and time, step count and heart rate, 2 line weather summary) + an optional daily progress bar. (The daily progress bar can be independently enabled when locked and unlocked.)
|
||||
|
||||
Most things are self-explanatory, but the day progress bar might not be. The day progress bar is intended to show approximately how far through the day you are, in the form of a progress bar. You might want to configure it to show how far you are through your waking hours, or you might want to use it to show how far you are through your work or school day.
|
||||
|
||||
## Shortcuts
|
||||
|
||||
There are generally a few apps that the user uses far more frequently than the others. For example, they might use a timer, alarm clock, and calculator every day, while everything else (such as the settings app) gets used only occasionally. This clock has space for 8 apps in the bottom half of the screen only one tap away, avoiding the need to wait for the launcher to open and then scroll through it. Tapping the top of the watch opens the launcher, eliminating the need for the button (which still opens the launcher due to bangle.js conventions). There is also handling for left, right, and vertical swipes. A vertical swipe by default opens the messages app, mimicking mobile operating systems which use a swipe down to view the notification shade.
|
||||
|
||||
## Configurability
|
||||
|
||||
Displaying the seconds allows for more precise timing, but waking up the CPU to refresh the display more often consumes battery. The user can enable or disable them completely, but can also configure them to be enabled or disabled automatically based on some hueristics:
|
||||
|
||||
* They can be hidden while the display is locked, if the user expects to unlock their watch when they need the seconds.
|
||||
* They can be hidden when the battery is too low, to make the last portion of the battery last a little bit longer.
|
||||
* They can be hidden during a period of time such as when the user is asleep and therefore unlikely to need very much precision.
|
||||
|
||||
The date format can be changed.
|
||||
|
||||
As described earlier, the contents of the bottom row when locked can be changed.
|
||||
|
||||
The 8 tap-based shortcuts on the bottom and the 3 swipe-based shortcuts can be changed to nothing, the launcher, or any app on the watch.
|
||||
|
||||
The start and end time of the day progress bar can be changed. It can be enabled or disabled separately when the watch is locked and unlocked. The color can be changed. The time when it resets from full to empty can be changed.
|
||||
|
||||
When the battery is below a defined point, the watch's color can change to another chosen color to help the user notice that the battery is low.
|
|
@ -0,0 +1,405 @@
|
|||
const SETTINGS_FILE = "infoclk.json";
|
||||
const FONT = require('infoclk-font.js');
|
||||
|
||||
const storage = require("Storage");
|
||||
const locale = require("locale");
|
||||
const weather = require('weather');
|
||||
|
||||
let config = Object.assign({
|
||||
seconds: {
|
||||
// Displaying the seconds can reduce battery life because the CPU must wake up more often to update the display.
|
||||
// The seconds will be shown unless one of these conditions is enabled here, and currently true.
|
||||
hideLocked: false, // Hide the seconds when the display is locked.
|
||||
hideBattery: 20, // Hide the seconds when the battery is at or below a defined percentage.
|
||||
hideTime: true, // Hide the seconds when between a certain period of time. Useful for when you are sleeping and don't need the seconds
|
||||
hideStart: 2200, // The time when the seconds are hidden: first 2 digits are hours on a 24 hour clock, last 2 are minutes
|
||||
hideEnd: 700, // The time when the seconds are shown again
|
||||
hideAlways: false, // Always hide (never show) the seconds
|
||||
},
|
||||
|
||||
date: {
|
||||
// Settings related to the display of the date
|
||||
mmdd: true, // If true, display the month first. If false, display the date first.
|
||||
separator: '-', // The character that goes between the month and date
|
||||
monthName: false, // If false, display the month as a number. If true, display the name.
|
||||
monthFullName: false, // If displaying the name: If false, display an abbreviation. If true, display a full name.
|
||||
dayFullName: false, // If false, display the day of the week's abbreviation. If true, display the full name.
|
||||
},
|
||||
|
||||
bottomLocked: {
|
||||
display: 'weather' // What to display in the bottom row when locked:
|
||||
// 'weather': The current temperature and weather description
|
||||
// 'steps': Step count
|
||||
// 'health': Step count and bpm
|
||||
// 'progress': Day progress bar
|
||||
// false: Nothing
|
||||
},
|
||||
|
||||
shortcuts: [
|
||||
//8 shortcuts, displayed in the bottom half of the screen (2 rows of 4 shortcuts) when unlocked
|
||||
// false = no shortcut
|
||||
// '#LAUNCHER' = open the launcher
|
||||
// any other string = name of app to open
|
||||
'stlap', 'keytimer', 'pomoplus', 'alarm',
|
||||
'rpnsci', 'calendar', 'torch', 'weather'
|
||||
],
|
||||
|
||||
swipe: {
|
||||
// 3 shortcuts to launch upon swiping:
|
||||
// false = no shortcut
|
||||
// '#LAUNCHER' = open the launcher
|
||||
// any other string = name of app to open
|
||||
up: 'messages', // Swipe up or swipe down, due to limitation of event handler
|
||||
left: '#LAUNCHER',
|
||||
right: '#LAUNCHER',
|
||||
},
|
||||
|
||||
dayProgress: {
|
||||
// A progress bar representing how far through the day you are
|
||||
enabledLocked: true, // Whether this bar is enabled when the watch is locked
|
||||
enabledUnlocked: false, // Whether the bar is enabled when the watch is unlocked
|
||||
color: [0, 0, 1], // The color of the bar
|
||||
start: 700, // The time of day that the bar starts filling
|
||||
end: 2200, // The time of day that the bar becomes full
|
||||
reset: 300 // The time of day when the progress bar resets from full to empty
|
||||
},
|
||||
|
||||
lowBattColor: {
|
||||
// The text can change color to indicate that the battery is low
|
||||
level: 20, // The percentage where this happens
|
||||
color: [1, 0, 0] // The color that the text changes to
|
||||
}
|
||||
}, storage.readJSON(SETTINGS_FILE));
|
||||
|
||||
// Return whether the given time (as a date object) is between start and end (as a number where the first 2 digits are hours on a 24 hour clock and the last 2 are minutes), with end time wrapping to next day if necessary
|
||||
function timeInRange(start, time, end) {
|
||||
|
||||
// Convert the given date object to a time number
|
||||
let timeNumber = time.getHours() * 100 + time.getMinutes();
|
||||
|
||||
// Normalize to prevent the numbers from wrapping around at midnight
|
||||
if (end <= start) {
|
||||
end += 2400;
|
||||
if (timeNumber < start) timeNumber += 2400;
|
||||
}
|
||||
|
||||
return start <= timeNumber && timeNumber <= end;
|
||||
}
|
||||
|
||||
// Return whether settings should be displayed based on the user's configuration
|
||||
function shouldDisplaySeconds(now) {
|
||||
return !(
|
||||
(config.seconds.hideAlways) ||
|
||||
(config.seconds.hideLocked && Bangle.isLocked()) ||
|
||||
(E.getBattery() <= config.seconds.hideBattery) ||
|
||||
(config.seconds.hideTime && timeInRange(config.seconds.hideStart, now, config.seconds.hideEnd))
|
||||
);
|
||||
}
|
||||
|
||||
// Determine the font size needed to fit a string of the given length widthin maxWidth number of pixels, clamped between minSize and maxSize
|
||||
function getFontSize(length, maxWidth, minSize, maxSize) {
|
||||
let size = Math.floor(maxWidth / length); //Number of pixels of width available to character
|
||||
size *= (20 / 12); //Convert to height, assuming 20 pixels of height for every 12 of width
|
||||
|
||||
// Clamp to within range
|
||||
if (size < minSize) return minSize;
|
||||
else if (size > maxSize) return maxSize;
|
||||
else return Math.floor(size);
|
||||
}
|
||||
|
||||
// Get the current day of the week according to user settings
|
||||
function getDayString(now) {
|
||||
if (config.date.dayFullName) return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'][now.getDay()];
|
||||
else return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'][now.getDay()];
|
||||
}
|
||||
|
||||
// Pad a number with zeros to be the given number of digits
|
||||
function pad(number, digits) {
|
||||
let result = '' + number;
|
||||
while (result.length < digits) result = '0' + result;
|
||||
return result;
|
||||
}
|
||||
|
||||
// Get the current date formatted according to the user settings
|
||||
function getDateString(now) {
|
||||
let month;
|
||||
if (!config.date.monthName) month = pad(now.getMonth() + 1, 2);
|
||||
else if (config.date.monthFullName) month = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'][now.getMonth()];
|
||||
else month = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now.getMonth()];
|
||||
|
||||
if (config.date.mmdd) return `${month}${config.date.separator}${pad(now.getDate(), 2)}`;
|
||||
else return `${pad(now.getDate(), 2)}${config.date.separator}${month}`;
|
||||
}
|
||||
|
||||
// Get a floating point number from 0 to 1 representing how far between the user-defined start and end points we are
|
||||
function getDayProgress(now) {
|
||||
let start = config.dayProgress.start;
|
||||
let current = now.getHours() * 100 + now.getMinutes();
|
||||
let end = config.dayProgress.end;
|
||||
let reset = config.dayProgress.reset;
|
||||
|
||||
// Normalize
|
||||
if (end <= start) end += 2400;
|
||||
if (current < start) current += 2400;
|
||||
if (reset < start) reset += 2400;
|
||||
|
||||
// Convert an hhmm number into a floating-point hours
|
||||
function toDecimalHours(time) {
|
||||
let hours = Math.floor(time / 100);
|
||||
let minutes = time % 100;
|
||||
|
||||
return hours + (minutes / 60);
|
||||
}
|
||||
|
||||
start = toDecimalHours(start);
|
||||
current = toDecimalHours(current);
|
||||
end = toDecimalHours(end);
|
||||
reset = toDecimalHours(reset);
|
||||
|
||||
let progress = (current - start) / (end - start);
|
||||
|
||||
if (progress < 0 || progress > 1) {
|
||||
if (current < reset) return 1;
|
||||
else return 0;
|
||||
} else {
|
||||
return progress;
|
||||
}
|
||||
}
|
||||
|
||||
// Get a Gadgetbridge weather string
|
||||
function getWeatherString() {
|
||||
let current = weather.get();
|
||||
if (current) return locale.temp(current.temp - 273.15) + ', ' + current.txt;
|
||||
else return 'Weather unknown!';
|
||||
}
|
||||
|
||||
// Get a second weather row showing humidity, wind speed, and wind direction
|
||||
function getWeatherRow2() {
|
||||
let current = weather.get();
|
||||
if (current) return `${current.hum}%, ${locale.speed(current.wind)} ${current.wrose}`;
|
||||
else return 'Check Gadgetbridge';
|
||||
}
|
||||
|
||||
// Get a step string
|
||||
function getStepsString() {
|
||||
return '' + Bangle.getHealthStatus('day').steps + ' steps';
|
||||
}
|
||||
|
||||
// Get a health string including daily steps and recent bpm
|
||||
function getHealthString() {
|
||||
return `${Bangle.getHealthStatus('day').steps} steps ${Bangle.getHealthStatus('last').bpm} bpm`;
|
||||
}
|
||||
|
||||
// Set the next timeout to draw the screen
|
||||
let drawTimeout;
|
||||
function setNextDrawTimeout() {
|
||||
if (drawTimeout) {
|
||||
clearTimeout(drawTimeout);
|
||||
drawTimeout = undefined;
|
||||
}
|
||||
|
||||
let time;
|
||||
let now = new Date();
|
||||
if (shouldDisplaySeconds(now)) time = 1000 - (now.getTime() % 1000);
|
||||
else time = 60000 - (now.getTime() % 60000);
|
||||
|
||||
drawTimeout = setTimeout(draw, time);
|
||||
}
|
||||
|
||||
|
||||
const DIGIT_WIDTH = 40; // How much width is allocated for each digit, 37 pixels + 3 pixels of space (which will go off of the screen on the right edge)
|
||||
const COLON_WIDTH = 19; // How much width is allocated for the colon, 16 pixels + 3 pixels of space
|
||||
const HHMM_TOP = 27; // 24 pixels for widgets + 3 pixels of space
|
||||
const DIGIT_HEIGHT = 64; // How tall the digits are
|
||||
|
||||
const SECONDS_TOP = HHMM_TOP + DIGIT_HEIGHT + 3; // The top edge of the seconds, top of hours and minutes + digit height + space
|
||||
const SECONDS_LEFT = 2 * DIGIT_WIDTH + COLON_WIDTH; // The left edge of the seconds: displayed after 2 digits and the colon
|
||||
const DATE_LETTER_HEIGHT = DIGIT_HEIGHT / 2; // Each letter of the day of week and date will be half the height of the time digits
|
||||
|
||||
const DATE_CENTER_X = SECONDS_LEFT / 2; // Day of week and date will be centered between left edge of screen and where seconds start
|
||||
const DOW_CENTER_Y = SECONDS_TOP + (DATE_LETTER_HEIGHT / 2); // Day of week will be the top row
|
||||
const DATE_CENTER_Y = DOW_CENTER_Y + DATE_LETTER_HEIGHT; // Date will be the bottom row
|
||||
const DOW_DATE_CENTER_Y = SECONDS_TOP + (DIGIT_HEIGHT / 2); // When displaying both on one row, center it
|
||||
const BOTTOM_CENTER_Y = ((SECONDS_TOP + DIGIT_HEIGHT + 3) + g.getHeight()) / 2;
|
||||
|
||||
// Draw the clock
|
||||
function draw() {
|
||||
//Prepare to draw
|
||||
g.reset()
|
||||
.setFontAlign(0, 0);
|
||||
|
||||
if (E.getBattery() <= config.lowBattColor.level) {
|
||||
let color = config.lowBattColor.color;
|
||||
g.setColor(color[0], color[1], color[2]);
|
||||
}
|
||||
now = new Date();
|
||||
|
||||
if (Bangle.isLocked()) { //When the watch is locked
|
||||
g.clearRect(0, 24, g.getWidth(), g.getHeight());
|
||||
|
||||
//Draw the hours and minutes
|
||||
let x = 0;
|
||||
|
||||
for (let digit of locale.time(now, 1)) { //apparently this is how you get an hh:mm time string adjusting for the user's 12/24 hour preference
|
||||
if (digit != ' ') g.drawImage(FONT[digit], x, HHMM_TOP);
|
||||
if (digit == ':') x += COLON_WIDTH;
|
||||
else x += DIGIT_WIDTH;
|
||||
}
|
||||
if (storage.readJSON('setting.json')['12hour']) g.drawImage(FONT[(now.getHours() < 12) ? 'am' : 'pm'], 0, HHMM_TOP);
|
||||
|
||||
//Draw the seconds if necessary
|
||||
if (shouldDisplaySeconds(now)) {
|
||||
let tens = Math.floor(now.getSeconds() / 10);
|
||||
let ones = now.getSeconds() % 10;
|
||||
g.drawImage(FONT[tens], SECONDS_LEFT, SECONDS_TOP)
|
||||
.drawImage(FONT[ones], SECONDS_LEFT + DIGIT_WIDTH, SECONDS_TOP);
|
||||
|
||||
// Draw the day of week and date assuming the seconds are displayed
|
||||
|
||||
g.setFont('Vector', getFontSize(getDayString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
|
||||
.drawString(getDayString(now), DATE_CENTER_X, DOW_CENTER_Y)
|
||||
.setFont('Vector', getFontSize(getDateString(now).length, SECONDS_LEFT, 6, DATE_LETTER_HEIGHT))
|
||||
.drawString(getDateString(now), DATE_CENTER_X, DATE_CENTER_Y);
|
||||
|
||||
} else {
|
||||
//Draw the day of week and date without the seconds
|
||||
|
||||
let string = getDayString(now) + ' ' + getDateString(now);
|
||||
g.setFont('Vector', getFontSize(string.length, g.getWidth(), 6, DATE_LETTER_HEIGHT))
|
||||
.drawString(string, g.getWidth() / 2, DOW_DATE_CENTER_Y);
|
||||
}
|
||||
|
||||
// Draw the bottom area
|
||||
if (config.bottomLocked.display == 'progress') {
|
||||
let color = config.dayProgress.color;
|
||||
g.setColor(color[0], color[1], color[2])
|
||||
.fillRect(0, SECONDS_TOP + DIGIT_HEIGHT + 3, g.getWidth() * getDayProgress(now), g.getHeight());
|
||||
} else {
|
||||
let bottomString;
|
||||
|
||||
if (config.bottomLocked.display == 'weather') bottomString = getWeatherString();
|
||||
else if (config.bottomLocked.display == 'steps') bottomString = getStepsString();
|
||||
else if (config.bottomLocked.display == 'health') bottomString = getHealthString();
|
||||
else bottomString = ' ';
|
||||
|
||||
g.setFont('Vector', getFontSize(bottomString.length, 176, 6, g.getHeight() - (SECONDS_TOP + DIGIT_HEIGHT + 3)))
|
||||
.drawString(bottomString, g.getWidth() / 2, BOTTOM_CENTER_Y);
|
||||
}
|
||||
|
||||
// Draw the day progress bar between the rows if necessary
|
||||
if (config.dayProgress.enabledLocked && config.bottomLocked.display != 'progress') {
|
||||
let color = config.dayProgress.color;
|
||||
g.setColor(color[0], color[1], color[2])
|
||||
.fillRect(0, HHMM_TOP + DIGIT_HEIGHT, g.getWidth() * getDayProgress(now), SECONDS_TOP);
|
||||
}
|
||||
} else {
|
||||
|
||||
//If the watch is unlocked
|
||||
g.clearRect(0, 24, g.getWidth(), g.getHeight() / 2);
|
||||
rows = [
|
||||
`${getDayString(now)} ${getDateString(now)} ${locale.time(now, 1)}`,
|
||||
getHealthString(),
|
||||
getWeatherString(),
|
||||
getWeatherRow2()
|
||||
];
|
||||
if (shouldDisplaySeconds(now)) rows[0] += ':' + pad(now.getSeconds(), 2);
|
||||
if (storage.readJSON('setting.json')['12hour']) rows[0] += ((now.getHours() < 12) ? ' AM' : ' PM');
|
||||
|
||||
let maxHeight = ((g.getHeight() / 2) - HHMM_TOP) / (config.dayProgress.enabledUnlocked ? (rows.length + 1) : rows.length);
|
||||
|
||||
let y = HHMM_TOP + maxHeight / 2;
|
||||
for (let row of rows) {
|
||||
let size = getFontSize(row.length, g.getWidth(), 6, maxHeight);
|
||||
g.setFont('Vector', size)
|
||||
.drawString(row, g.getWidth() / 2, y);
|
||||
y += maxHeight;
|
||||
}
|
||||
|
||||
if (config.dayProgress.enabledUnlocked) {
|
||||
let color = config.dayProgress.color;
|
||||
g.setColor(color[0], color[1], color[2])
|
||||
.fillRect(0, y - maxHeight / 2, 176 * getDayProgress(now), y + maxHeight / 2);
|
||||
}
|
||||
}
|
||||
|
||||
setNextDrawTimeout();
|
||||
}
|
||||
|
||||
// Draw the icons. This is done separately from the main draw routine to avoid having to scale and draw a bunch of images repeatedly.
|
||||
function drawIcons() {
|
||||
g.reset().clearRect(0, 24, g.getWidth(), g.getHeight());
|
||||
for (let i = 0; i < 8; i++) {
|
||||
let x = [0, 44, 88, 132, 0, 44, 88, 132][i];
|
||||
let y = [88, 88, 88, 88, 132, 132, 132, 132][i];
|
||||
let appId = config.shortcuts[i];
|
||||
let appInfo = storage.readJSON(appId + '.info', 1);
|
||||
if (!appInfo) continue;
|
||||
icon = storage.read(appInfo.icon);
|
||||
g.drawImage(icon, x, y, {
|
||||
scale: 0.916666666667
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
weather.on("update", draw);
|
||||
Bangle.on("step", draw);
|
||||
Bangle.on('lock', locked => {
|
||||
//If the watch is unlocked, draw the icons
|
||||
if (!locked) drawIcons();
|
||||
draw();
|
||||
});
|
||||
|
||||
// Show launcher when middle button pressed
|
||||
Bangle.setUI("clock");
|
||||
// Load widgets
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
// Launch an app given the current ID. Handles special cases:
|
||||
// false: Do nothing
|
||||
// '#LAUNCHER': Open the launcher
|
||||
// nonexistent app: Do nothing
|
||||
function launch(appId) {
|
||||
if (appId == false) return;
|
||||
else if (appId == '#LAUNCHER') {
|
||||
Bangle.buzz();
|
||||
Bangle.showLauncher();
|
||||
} else {
|
||||
let appInfo = storage.readJSON(appId + '.info', 1);
|
||||
if (appInfo) {
|
||||
Bangle.buzz();
|
||||
load(appInfo.src);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Set up touch to launch the selected app
|
||||
Bangle.on('touch', function (button, xy) {
|
||||
let x = Math.floor(xy.x / 44);
|
||||
if (x < 0) x = 0;
|
||||
else if (x > 3) x = 3;
|
||||
|
||||
let y = Math.floor(xy.y / 44);
|
||||
if (y < 0) y = -1;
|
||||
else if (y > 3) y = 1;
|
||||
else y -= 2;
|
||||
|
||||
if (y < 0) {
|
||||
Bangle.buzz();
|
||||
Bangle.showLauncher();
|
||||
} else {
|
||||
let i = 4 * y + x;
|
||||
launch(config.shortcuts[i]);
|
||||
}
|
||||
});
|
||||
|
||||
//Set up swipe handler
|
||||
Bangle.on('swipe', function (direction) {
|
||||
if (direction == -1) launch(config.swipe.left);
|
||||
else if (direction == 0) launch(config.swipe.up);
|
||||
else launch(config.swipe.right);
|
||||
});
|
||||
|
||||
if (!Bangle.isLocked()) drawIcons();
|
||||
|
||||
draw();
|
|
@ -0,0 +1,23 @@
|
|||
const heatshrink = require("heatshrink")
|
||||
|
||||
function decompress(string) {
|
||||
return heatshrink.decompress(atob(string))
|
||||
}
|
||||
|
||||
exports = {
|
||||
'0': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYk/En4kmAA4qBAAP7BAePAYX4BofBAYX8F4Q+BEwRHBIQI5BA"),
|
||||
'1': decompress("ktAwIGDj/4AgX/4ADBg/+BAU/+ADBgP/wAEBh/8BoV/8ADBgf/En4k/En4k/EgQ="),
|
||||
'2': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECElQdBPA2HAYX8OYfHBAYRD8Z3Dj6TG/kPPYZm4EiwAHO4f7BAfPfI/xBoaTEPAfgQwY"),
|
||||
'3': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECElJWIAEpu/EhpgS34DC/IID54DC/l/AgXDAYX4j57DA"),
|
||||
'4': decompress("ktAwMA//AgEf//+BYP///wgEHAgOAgE///8gEBBAPggEPAgIWBv///EAgYIBEn4kXABf9AgfnAgY4BAAP4BAfDAYX+EwfwIQRRCJIJRBJIRRBJIQICj5RBJIRRBJIJRCNwJRBNwQk/Ei4A=="),
|
||||
'5': decompress("ktAwIEB/4AB/EfAgXDAYX+n4EC+YDC/+fAYX9BAfvAgYAJ+AwBgP/wAEBh/8H4V/8ADBgf/BAUf/AEC//AAYMH/wICn4kpPYUPAgXgv4EC4JfDg4DC/iFD8ANDwaTDCQfwEoZ2/EhrXNAAm/AYX5BAfPQoaTD4ahDj57DA=="),
|
||||
'6': decompress("ktAwIEB/4AB/EfAgXDAYX+n4EC+YDC/+fAYX9BAfvAgYAJ+AwBgP/wAEBh/8H4V/8ADBgf/BAUf/AEC//AAYMH/wICn4kpPYUPAgXgv6AG/6JD/gID84ED358NJIIsCKIQ0BKIRJCFgJJCSYcHAgJuBXYJuBKIQkpAA58D/YIDx6PDBofBQoYvCHwImCI4KUCwA="),
|
||||
'7': decompress("ktAwMA/4AB/EHAgXwn4EC8IDC/+PAYX+v4EC+YND74NDBAYAE4A0Bg/+HIU/+ADBgP/wAEBh/8BoV/8ADBgf/BAUf/AECEn4k/En4kVA"),
|
||||
'8': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYkpAFpu/EhwAHFQIAB/YIDx4DC/AND4IDC/ieD4AmCI4JCBHIIA=="),
|
||||
'9': decompress("ktAwIEB////EAj4EB+EDAYP/8E/AgWDAYX+CIX/+IDC//PBoYIDAAvwgEHAgOAgAnB/kAgIvCgEPAgJCBv5CCHwXAI4X+KAYkpABf9AgfnAgaFD/AID4Z8DEwfwIQRRCJIJRBJIRRBJIQICj5RBJIRRBJIJRCNwJRBNwQkoPhoAE34DC/L0H/iwBQAv4WAJ7CA=="),
|
||||
|
||||
':': decompress("iFAwITQg/gj/4n/8v/+AIP/ABQPDCoIZBDoJTfH94A=="),
|
||||
|
||||
'am': decompress("jFAwIEBngCEvwCH/4CFwEBAQkD//AgfnAQcH4fgAQsPwPwAQf/+Ef//4AQn8n0AvgCCHQN+vkAnwCC/EAj4CF+EAh4CCNIoLFC4v8gE/AQv+gF/AQpwB/4CDwICG+/D94CD8/v+fn54CC+P/x4CF+H/IgICFvwCEngCD"),
|
||||
'pm': decompress("jFAwMAn///l///+/4AE+EAh4CaEYoABFgX8BwMAAUwAFIIv4gEfAQX8OYICF/0Av4CF/8AKQICCwICG+/D94CD8/v+fn54CC+P/x4CF+H/IgICFvwCEngCDA")
|
||||
}
|
After Width: | Height: | Size: 360 B |
After Width: | Height: | Size: 216 B |
After Width: | Height: | Size: 290 B |
After Width: | Height: | Size: 183 B |
After Width: | Height: | Size: 305 B |
After Width: | Height: | Size: 270 B |
After Width: | Height: | Size: 247 B |
After Width: | Height: | Size: 302 B |
After Width: | Height: | Size: 309 B |
After Width: | Height: | Size: 227 B |
After Width: | Height: | Size: 309 B |
After Width: | Height: | Size: 319 B |
After Width: | Height: | Size: 327 B |
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwgOAA4YFS/4AKEf5BlABcAjAgBjAfBAuhH/Apo"))
|
After Width: | Height: | Size: 249 B |
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"id": "infoclk",
|
||||
"name": "Informational clock",
|
||||
"version": "0.08",
|
||||
"description": "A configurable clock with extra info and shortcuts when unlocked, but large time when locked",
|
||||
"readme": "README.md",
|
||||
"icon": "icon.png",
|
||||
"type": "clock",
|
||||
"tags": "clock",
|
||||
"supports": [
|
||||
"BANGLEJS2"
|
||||
],
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{
|
||||
"name": "infoclk.app.js",
|
||||
"url": "app.js"
|
||||
},
|
||||
{
|
||||
"name": "infoclk.settings.js",
|
||||
"url": "settings.js"
|
||||
},
|
||||
{
|
||||
"name": "infoclk-font.js",
|
||||
"url": "font.js"
|
||||
},
|
||||
{
|
||||
"name": "infoclk.img",
|
||||
"url": "icon.js",
|
||||
"evaluate": true
|
||||
}
|
||||
],
|
||||
"data": [
|
||||
{
|
||||
"name": "infoclk.json"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,571 @@
|
|||
(function (back) {
|
||||
const SETTINGS_FILE = "infoclk.json";
|
||||
const storage = require('Storage');
|
||||
|
||||
let config = Object.assign({
|
||||
seconds: {
|
||||
// Displaying the seconds can reduce battery life because the CPU must wake up more often to update the display.
|
||||
// The seconds will be shown unless one of these conditions is enabled here, and currently true.
|
||||
hideLocked: false, // Hide the seconds when the display is locked.
|
||||
hideBattery: 20, // Hide the seconds when the battery is at or below a defined percentage.
|
||||
hideTime: true, // Hide the seconds when between a certain period of time. Useful for when you are sleeping and don't need the seconds
|
||||
hideStart: 2200, // The time when the seconds are hidden: first 2 digits are hours on a 24 hour clock, last 2 are minutes
|
||||
hideEnd: 700, // The time when the seconds are shown again
|
||||
hideAlways: false, // Always hide (never show) the seconds
|
||||
},
|
||||
|
||||
date: {
|
||||
// Settings related to the display of the date
|
||||
mmdd: true, // If true, display the month first. If false, display the date first.
|
||||
separator: '-', // The character that goes between the month and date
|
||||
monthName: false, // If false, display the month as a number. If true, display the name.
|
||||
monthFullName: false, // If displaying the name: If false, display an abbreviation. If true, display a full name.
|
||||
dayFullName: false, // If false, display the day of the week's abbreviation. If true, display the full name.
|
||||
},
|
||||
|
||||
bottomLocked: {
|
||||
display: 'weather' // What to display in the bottom row when locked:
|
||||
// 'weather': The current temperature and weather description
|
||||
// 'steps': Step count
|
||||
// 'health': Step count and bpm
|
||||
// 'progress': Day progress bar
|
||||
// false: Nothing
|
||||
},
|
||||
|
||||
shortcuts: [
|
||||
//8 shortcuts, displayed in the bottom half of the screen (2 rows of 4 shortcuts) when unlocked
|
||||
// false = no shortcut
|
||||
// '#LAUNCHER' = open the launcher
|
||||
// any other string = name of app to open
|
||||
'stlap', 'keytimer', 'pomoplus', 'alarm',
|
||||
'rpnsci', 'calendar', 'torch', 'weather'
|
||||
],
|
||||
|
||||
swipe: {
|
||||
// 3 shortcuts to launch upon swiping:
|
||||
// false = no shortcut
|
||||
// '#LAUNCHER' = open the launcher
|
||||
// any other string = name of app to open
|
||||
up: 'messages', // Swipe up or swipe down, due to limitation of event handler
|
||||
left: '#LAUNCHER',
|
||||
right: '#LAUNCHER',
|
||||
},
|
||||
|
||||
dayProgress: {
|
||||
// A progress bar representing how far through the day you are
|
||||
enabledLocked: true, // Whether this bar is enabled when the watch is locked
|
||||
enabledUnlocked: false, // Whether the bar is enabled when the watch is unlocked
|
||||
color: [0, 0, 1], // The color of the bar
|
||||
start: 700, // The time of day that the bar starts filling
|
||||
end: 2200, // The time of day that the bar becomes full
|
||||
reset: 300 // The time of day when the progress bar resets from full to empty
|
||||
},
|
||||
|
||||
lowBattColor: {
|
||||
// The text can change color to indicate that the battery is low
|
||||
level: 20, // The percentage where this happens
|
||||
color: [1, 0, 0] // The color that the text changes to
|
||||
}
|
||||
}, storage.readJSON(SETTINGS_FILE));
|
||||
|
||||
function saveSettings() {
|
||||
storage.writeJSON(SETTINGS_FILE, config);
|
||||
}
|
||||
|
||||
function hourToString(hour) {
|
||||
if (storage.readJSON('setting.json')['12hour']) {
|
||||
if (hour == 0) return '12 AM';
|
||||
else if (hour < 12) return `${hour} AM`;
|
||||
else if (hour == 12) return '12 PM';
|
||||
else return `${hour - 12} PM`;
|
||||
} else return '' + hour;
|
||||
}
|
||||
|
||||
// The menu for configuring when the seconds are shown
|
||||
function showSecondsMenu() {
|
||||
E.showMenu({
|
||||
'': {
|
||||
'title': 'Seconds display',
|
||||
'back': showMainMenu
|
||||
},
|
||||
'Show seconds': {
|
||||
value: !config.seconds.hideAlways,
|
||||
onchange: value => {
|
||||
config.seconds.hideAlways = !value;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'...unless locked': {
|
||||
value: config.seconds.hideLocked,
|
||||
onchange: value => {
|
||||
config.seconds.hideLocked = value;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'...unless battery below': {
|
||||
value: config.seconds.hideBattery,
|
||||
min: 0,
|
||||
max: 100,
|
||||
format: value => `${value}%`,
|
||||
onchange: value => {
|
||||
config.seconds.hideBattery = value;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'...unless between these 2 times...': () => {
|
||||
E.showMenu({
|
||||
'': {
|
||||
'title': 'Hide seconds between',
|
||||
'back': showSecondsMenu
|
||||
},
|
||||
'Enabled': {
|
||||
value: config.seconds.hideTime,
|
||||
onchange: value => {
|
||||
config.seconds.hideTime = value;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Start hour': {
|
||||
value: Math.floor(config.seconds.hideStart / 100),
|
||||
format: hourToString,
|
||||
min: 0,
|
||||
max: 23,
|
||||
wrap: true,
|
||||
onchange: hour => {
|
||||
minute = config.seconds.hideStart % 100;
|
||||
config.seconds.hideStart = (100 * hour) + minute;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Start minute': {
|
||||
value: config.seconds.hideStart % 100,
|
||||
min: 0,
|
||||
max: 59,
|
||||
wrap: true,
|
||||
onchange: minute => {
|
||||
hour = Math.floor(config.seconds.hideStart / 100);
|
||||
config.seconds.hideStart = (100 * hour) + minute;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'End hour': {
|
||||
value: Math.floor(config.seconds.hideEnd / 100),
|
||||
format: hourToString,
|
||||
min: 0,
|
||||
max: 23,
|
||||
wrap: true,
|
||||
onchange: hour => {
|
||||
minute = config.seconds.hideEnd % 100;
|
||||
config.seconds.hideEnd = (100 * hour) + minute;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'End minute': {
|
||||
value: config.seconds.hideEnd % 100,
|
||||
min: 0,
|
||||
max: 59,
|
||||
wrap: true,
|
||||
onchange: minute => {
|
||||
hour = Math.floor(config.seconds.hideEnd / 100);
|
||||
config.seconds.hideEnd = (100 * hour) + minute;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Available month/date separators
|
||||
const SEPARATORS = [
|
||||
{ name: 'Slash', char: '/' },
|
||||
{ name: 'Dash', char: '-' },
|
||||
{ name: 'Space', char: ' ' },
|
||||
{ name: 'Comma', char: ',' },
|
||||
{ name: 'None', char: '' }
|
||||
];
|
||||
|
||||
// Available bottom row display options
|
||||
const BOTTOM_ROW_OPTIONS = [
|
||||
{ name: 'Weather', val: 'weather' },
|
||||
{ name: 'Step count', val: 'steps' },
|
||||
{ name: 'Steps + BPM', val: 'health' },
|
||||
{ name: 'Day progresss bar', val: 'progress' },
|
||||
{ name: 'Nothing', val: false }
|
||||
];
|
||||
|
||||
// The menu for configuring which apps have shortcut icons
|
||||
function showShortcutMenu() {
|
||||
//Builds the shortcut options
|
||||
let shortcutOptions = [
|
||||
{ name: 'Nothing', val: false },
|
||||
{ name: 'Launcher', val: '#LAUNCHER' },
|
||||
];
|
||||
|
||||
let infoFiles = storage.list(/\.info$/).sort((a, b) => {
|
||||
if (a.name < b.name) return -1;
|
||||
else if (a.name > b.name) return 1;
|
||||
else return 0;
|
||||
});
|
||||
for (let infoFile of infoFiles) {
|
||||
let appInfo = storage.readJSON(infoFile);
|
||||
if (appInfo.src) shortcutOptions.push({
|
||||
name: appInfo.name,
|
||||
val: appInfo.id
|
||||
});
|
||||
}
|
||||
|
||||
E.showMenu({
|
||||
'': {
|
||||
'title': 'Shortcuts',
|
||||
'back': showMainMenu
|
||||
},
|
||||
'Top first': {
|
||||
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[0]),
|
||||
format: value => shortcutOptions[value].name,
|
||||
min: 0,
|
||||
max: shortcutOptions.length - 1,
|
||||
wrap: false,
|
||||
onchange: value => {
|
||||
config.shortcuts[0] = shortcutOptions[value].val;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Top second': {
|
||||
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[1]),
|
||||
format: value => shortcutOptions[value].name,
|
||||
min: 0,
|
||||
max: shortcutOptions.length - 1,
|
||||
wrap: false,
|
||||
onchange: value => {
|
||||
config.shortcuts[1] = shortcutOptions[value].val;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Top third': {
|
||||
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[2]),
|
||||
format: value => shortcutOptions[value].name,
|
||||
min: 0,
|
||||
max: shortcutOptions.length - 1,
|
||||
wrap: false,
|
||||
onchange: value => {
|
||||
config.shortcuts[2] = shortcutOptions[value].val;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Top fourth': {
|
||||
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[3]),
|
||||
format: value => shortcutOptions[value].name,
|
||||
min: 0,
|
||||
max: shortcutOptions.length - 1,
|
||||
wrap: false,
|
||||
onchange: value => {
|
||||
config.shortcuts[3] = shortcutOptions[value].val;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Bottom first': {
|
||||
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[4]),
|
||||
format: value => shortcutOptions[value].name,
|
||||
min: 0,
|
||||
max: shortcutOptions.length - 1,
|
||||
wrap: false,
|
||||
onchange: value => {
|
||||
config.shortcuts[4] = shortcutOptions[value].val;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Bottom second': {
|
||||
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[5]),
|
||||
format: value => shortcutOptions[value].name,
|
||||
min: 0,
|
||||
max: shortcutOptions.length - 1,
|
||||
wrap: false,
|
||||
onchange: value => {
|
||||
config.shortcuts[5] = shortcutOptions[value].val;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Bottom third': {
|
||||
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[6]),
|
||||
format: value => shortcutOptions[value].name,
|
||||
min: 0,
|
||||
max: shortcutOptions.length - 1,
|
||||
wrap: false,
|
||||
onchange: value => {
|
||||
config.shortcuts[6] = shortcutOptions[value].val;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Bottom fourth': {
|
||||
value: shortcutOptions.map(item => item.val).indexOf(config.shortcuts[7]),
|
||||
format: value => shortcutOptions[value].name,
|
||||
min: 0,
|
||||
max: shortcutOptions.length - 1,
|
||||
wrap: false,
|
||||
onchange: value => {
|
||||
config.shortcuts[7] = shortcutOptions[value].val;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Swipe up': {
|
||||
value: shortcutOptions.map(item => item.val).indexOf(config.swipe.up),
|
||||
format: value => shortcutOptions[value].name,
|
||||
min: 0,
|
||||
max: shortcutOptions.length - 1,
|
||||
wrap: false,
|
||||
onchange: value => {
|
||||
config.swipe.up = shortcutOptions[value].val;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Swipe left': {
|
||||
value: shortcutOptions.map(item => item.val).indexOf(config.swipe.left),
|
||||
format: value => shortcutOptions[value].name,
|
||||
min: 0,
|
||||
max: shortcutOptions.length - 1,
|
||||
wrap: false,
|
||||
onchange: value => {
|
||||
config.swipe.left = shortcutOptions[value].val;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Swipe right': {
|
||||
value: shortcutOptions.map(item => item.val).indexOf(config.swipe.right),
|
||||
format: value => shortcutOptions[value].name,
|
||||
min: 0,
|
||||
max: shortcutOptions.length - 1,
|
||||
wrap: false,
|
||||
onchange: value => {
|
||||
config.swipe.right = shortcutOptions[value].val;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const COLOR_OPTIONS = [
|
||||
{ name: 'Black', val: [0, 0, 0] },
|
||||
{ name: 'Blue', val: [0, 0, 1] },
|
||||
{ name: 'Green', val: [0, 1, 0] },
|
||||
{ name: 'Cyan', val: [0, 1, 1] },
|
||||
{ name: 'Red', val: [1, 0, 0] },
|
||||
{ name: 'Magenta', val: [1, 0, 1] },
|
||||
{ name: 'Yellow', val: [1, 1, 0] },
|
||||
{ name: 'White', val: [1, 1, 1] }
|
||||
];
|
||||
|
||||
// Workaround for being unable to use == on arrays: convert them into strings
|
||||
function colorString(color) {
|
||||
return `${color[0]} ${color[1]} ${color[2]}`;
|
||||
}
|
||||
|
||||
//Shows the top level menu
|
||||
function showMainMenu() {
|
||||
E.showMenu({
|
||||
'': {
|
||||
'title': 'Informational Clock',
|
||||
'back': back
|
||||
},
|
||||
'Seconds display': showSecondsMenu,
|
||||
'Day of week format': {
|
||||
value: config.date.dayFullName,
|
||||
format: value => value ? 'Full name' : 'Abbreviation',
|
||||
onchange: value => {
|
||||
config.date.dayFullName = value;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Date format': () => {
|
||||
E.showMenu({
|
||||
'': {
|
||||
'title': 'Date format',
|
||||
'back': showMainMenu,
|
||||
},
|
||||
'Order': {
|
||||
value: config.date.mmdd,
|
||||
format: value => value ? 'Month first' : 'Date first',
|
||||
onchange: value => {
|
||||
config.date.mmdd = value;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Separator': {
|
||||
value: SEPARATORS.map(item => item.char).indexOf(config.date.separator),
|
||||
format: value => SEPARATORS[value].name,
|
||||
min: 0,
|
||||
max: SEPARATORS.length - 1,
|
||||
wrap: true,
|
||||
onchange: value => {
|
||||
config.date.separator = SEPARATORS[value].char;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Month format': {
|
||||
// 0 = number only
|
||||
// 1 = abbreviation
|
||||
// 2 = full name
|
||||
value: config.date.monthName ? (config.date.monthFullName ? 2 : 1) : 0,
|
||||
format: value => ['Number', 'Abbreviation', 'Full name'][value],
|
||||
min: 0,
|
||||
max: 2,
|
||||
wrap: true,
|
||||
onchange: value => {
|
||||
if (value == 0) config.date.monthName = false;
|
||||
else {
|
||||
config.date.monthName = true;
|
||||
config.date.monthFullName = (value == 2);
|
||||
}
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
'Bottom row': {
|
||||
value: BOTTOM_ROW_OPTIONS.map(item => item.val).indexOf(config.bottomLocked.display),
|
||||
format: value => BOTTOM_ROW_OPTIONS[value].name,
|
||||
min: 0,
|
||||
max: BOTTOM_ROW_OPTIONS.length - 1,
|
||||
wrap: true,
|
||||
onchange: value => {
|
||||
config.bottomLocked.display = BOTTOM_ROW_OPTIONS[value].val;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Shortcuts': showShortcutMenu,
|
||||
'Day progress': () => {
|
||||
E.showMenu({
|
||||
'': {
|
||||
'title': 'Day progress',
|
||||
'back': showMainMenu
|
||||
},
|
||||
'Enable while locked': {
|
||||
value: config.dayProgress.enabledLocked,
|
||||
onchange: value => {
|
||||
config.dayProgress.enableLocked = value;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Enable while unlocked': {
|
||||
value: config.dayProgress.enabledUnlocked,
|
||||
onchange: value => {
|
||||
config.dayProgress.enabledUnlocked = value;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Color': {
|
||||
value: COLOR_OPTIONS.map(item => colorString(item.val)).indexOf(colorString(config.dayProgress.color)),
|
||||
format: value => COLOR_OPTIONS[value].name,
|
||||
min: 0,
|
||||
max: COLOR_OPTIONS.length - 1,
|
||||
wrap: false,
|
||||
onchange: value => {
|
||||
config.dayProgress.color = COLOR_OPTIONS[value].val;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Start hour': {
|
||||
value: Math.floor(config.dayProgress.start / 100),
|
||||
format: hourToString,
|
||||
min: 0,
|
||||
max: 23,
|
||||
wrap: true,
|
||||
onchange: hour => {
|
||||
minute = config.dayProgress.start % 100;
|
||||
config.dayProgress.start = (100 * hour) + minute;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Start minute': {
|
||||
value: config.dayProgress.start % 100,
|
||||
min: 0,
|
||||
max: 59,
|
||||
wrap: true,
|
||||
onchange: minute => {
|
||||
hour = Math.floor(config.dayProgress.start / 100);
|
||||
config.dayProgress.start = (100 * hour) + minute;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'End hour': {
|
||||
value: Math.floor(config.dayProgress.end / 100),
|
||||
format: hourToString,
|
||||
min: 0,
|
||||
max: 23,
|
||||
wrap: true,
|
||||
onchange: hour => {
|
||||
minute = config.dayProgress.end % 100;
|
||||
config.dayProgress.end = (100 * hour) + minute;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'End minute': {
|
||||
value: config.dayProgress.end % 100,
|
||||
min: 0,
|
||||
max: 59,
|
||||
wrap: true,
|
||||
onchange: minute => {
|
||||
hour = Math.floor(config.dayProgress.end / 100);
|
||||
config.dayProgress.end = (100 * hour) + minute;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Reset hour': {
|
||||
value: Math.floor(config.dayProgress.reset / 100),
|
||||
format: hourToString,
|
||||
min: 0,
|
||||
max: 23,
|
||||
wrap: true,
|
||||
onchange: hour => {
|
||||
minute = config.dayProgress.reset % 100;
|
||||
config.dayProgress.reset = (100 * hour) + minute;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Reset minute': {
|
||||
value: config.dayProgress.reset % 100,
|
||||
min: 0,
|
||||
max: 59,
|
||||
wrap: true,
|
||||
onchange: minute => {
|
||||
hour = Math.floor(config.dayProgress.reset / 100);
|
||||
config.dayProgress.reset = (100 * hour) + minute;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
'Low battery color': () => {
|
||||
E.showMenu({
|
||||
'': {
|
||||
'title': 'Low battery color',
|
||||
back: showMainMenu
|
||||
},
|
||||
'Low battery threshold': {
|
||||
value: config.lowBattColor.level,
|
||||
min: 0,
|
||||
max: 100,
|
||||
format: value => `${value}%`,
|
||||
onchange: value => {
|
||||
config.lowBattColor.level = value;
|
||||
saveSettings();
|
||||
}
|
||||
},
|
||||
'Color': {
|
||||
value: COLOR_OPTIONS.map(item => colorString(item.val)).indexOf(colorString(config.lowBattColor.color)),
|
||||
format: value => COLOR_OPTIONS[value].name,
|
||||
min: 0,
|
||||
max: COLOR_OPTIONS.length - 1,
|
||||
wrap: false,
|
||||
onchange: value => {
|
||||
config.lowBattColor.color = COLOR_OPTIONS[value].val;
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
showMainMenu();
|
||||
});
|
|
@ -2,3 +2,4 @@
|
|||
0.02: Now keeps user input trace intact by changing how the screen is updated.
|
||||
0.03: Positioning of marker now takes the height of the widget field into account.
|
||||
0.04: Fix issue if going back without typing.
|
||||
0.05: Keep drag-function in ram, hopefully improving performance and input reliability somewhat.
|
||||
|
|
|
@ -139,6 +139,7 @@ exports.getStrokes( (id,s) => Bangle.strokes[id] = Unistroke.new(s) );
|
|||
return new Promise((resolve,reject) => {
|
||||
var l;//last event
|
||||
Bangle.setUI({mode:"custom", drag:e=>{
|
||||
"ram";
|
||||
if (l) g.reset().setColor("#f00").drawLine(l.x,l.y,e.x,e.y);
|
||||
l = e.b ? e : 0;
|
||||
},touch:() => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{ "id": "kbswipe",
|
||||
"name": "Swipe keyboard",
|
||||
"version":"0.04",
|
||||
"version":"0.05",
|
||||
"description": "A library for text input via PalmOS style swipe gestures (beta!)",
|
||||
"icon": "app.png",
|
||||
"type":"textinput",
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
0.01: New app!
|
||||
0.02: Submitted to the app loader
|
|
@ -0,0 +1,27 @@
|
|||
Bangle.keytimer_ACTIVE = true;
|
||||
const common = require("keytimer-com.js");
|
||||
const storage = require("Storage");
|
||||
|
||||
const keypad = require("keytimer-keys.js");
|
||||
const timerView = require("keytimer-tview.js");
|
||||
|
||||
Bangle.loadWidgets();
|
||||
Bangle.drawWidgets();
|
||||
|
||||
//Save our state when the app is closed
|
||||
E.on('kill', () => {
|
||||
storage.writeJSON(common.STATE_PATH, common.state);
|
||||
});
|
||||
|
||||
//Handle touch here. I would implement these separately in each view, but I can't figure out how to clear the event listeners.
|
||||
Bangle.on('touch', (button, xy) => {
|
||||
if (common.state.wasRunning) timerView.touch(button, xy);
|
||||
else keypad.touch(button, xy);
|
||||
});
|
||||
|
||||
Bangle.on('swipe', dir => {
|
||||
if (!common.state.wasRunning) keypad.swipe(dir);
|
||||
});
|
||||
|
||||
if (common.state.wasRunning) timerView.show(common);
|
||||
else keypad.show(common);
|
|
@ -0,0 +1,11 @@
|
|||
const keytimer_common = require("keytimer-com.js");
|
||||
|
||||
//Only start the timeout if the timer is running
|
||||
if (keytimer_common.state.running) {
|
||||
setTimeout(() => {
|
||||
//Check now to avoid race condition
|
||||
if (Bangle.keytimer_ACTIVE === undefined) {
|
||||
load('keytimer-ring.js');
|
||||
}
|
||||
}, keytimer_common.getTimeLeft());
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
const storage = require("Storage");
|
||||
const heatshrink = require("heatshrink");
|
||||
|
||||
exports.STATE_PATH = "keytimer.state.json";
|
||||
|
||||
exports.BUTTON_ICONS = {
|
||||
play: heatshrink.decompress(atob("jEYwMAkAGBnACBnwCBn+AAQPgAQPwAQP8AQP/AQXAAQPwAQP8AQP+AQgICBwQUCEAn4FggyBHAQ+CIgQ")),
|
||||
pause: heatshrink.decompress(atob("jEYwMA/4BBAX4CEA")),
|
||||
reset: heatshrink.decompress(atob("jEYwMA/4BB/+BAQPDAQPnAQIAKv///0///8j///EP//wAQQICBwQUCEhgyCHAQ+CIgI="))
|
||||
};
|
||||
|
||||
//Store the minimal amount of information to be able to reconstruct the state of the timer at any given time.
|
||||
//This is necessary because it is necessary to write to flash to let the timer run in the background, so minimizing the writes is necessary.
|
||||
exports.STATE_DEFAULT = {
|
||||
wasRunning: false, //If the timer ever was running. Used to determine whether to display a reset button
|
||||
running: false, //Whether the timer is currently running
|
||||
startTime: 0, //When the timer was last started. Difference between this and now is how long timer has run continuously.
|
||||
pausedTime: 0, //When the timer was last paused. Used for expiration and displaying timer while paused.
|
||||
elapsedTime: 0, //How much time the timer had spent running before the current start time. Update on pause or user skipping stages.
|
||||
setTime: 0, //How long the user wants the timer to run for
|
||||
inputString: '0' //The string of numbers the user typed in.
|
||||
};
|
||||
exports.state = storage.readJSON(exports.STATE_PATH);
|
||||
if (!exports.state) {
|
||||
exports.state = exports.STATE_DEFAULT;
|
||||
}
|
||||
|
||||
//Get the number of milliseconds until the timer expires
|
||||
exports.getTimeLeft = function () {
|
||||
if (!exports.state.wasRunning) {
|
||||
//If the timer never ran, the time left is just the set time
|
||||
return exports.setTime
|
||||
} else if (exports.state.running) {
|
||||
//If the timer is running, the time left is current time - start time + preexisting time
|
||||
var runningTime = (new Date()).getTime() - exports.state.startTime + exports.state.elapsedTime;
|
||||
} else {
|
||||
//If the timer is not running, the same as above but use when the timer was paused instead of now.
|
||||
var runningTime = exports.state.pausedTime - exports.state.startTime + exports.state.elapsedTime;
|
||||
}
|
||||
|
||||
return exports.state.setTime - runningTime;
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
require("heatshrink").decompress(atob("mEwwcAkmSpICOggRPpEACJ9AgESCJxMBhu27dtARVgCIMBCJpxDmwRL7ARDgwRL4CWECJaoFjYRJ2ARFgYRJwDNGCJFsb46SIRgQAFSRAQHSRCMEAAqSGRgoAFRhaSKRgySKRg6SIRhCSIRhCSICBqSCRhSSGRhY2FkARPhMkCJ9JkiONgECCIOQCJsSCIOSCJuSCIVACBcECIdICJYOBCIVJRhYRFSRSMBCIiSKBwgCCSRCMCCIqSIRgYCFRhYCFSQyMEAQqSGBw6SIRgySKRgtO4iSJBAmT23bOIqSCRgvtCINsSQ4aEndtCINt2KSGIggOBCIW2JQlARgZECCIhKEpBEGCIpKEA=="))
|
After Width: | Height: | Size: 414 B |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 2.8 KiB |
|
@ -0,0 +1,136 @@
|
|||
let common;
|
||||
|
||||
function inputStringToTime(inputString) {
|
||||
let number = parseInt(inputString);
|
||||
let hours = Math.floor(number / 10000);
|
||||
let minutes = Math.floor((number % 10000) / 100);
|
||||
let seconds = number % 100;
|
||||
|
||||
return 3600000 * hours +
|
||||
60000 * minutes +
|
||||
1000 * seconds;
|
||||
}
|
||||
|
||||
function pad(number) {
|
||||
return ('00' + parseInt(number)).slice(-2);
|
||||
}
|
||||
|
||||
function inputStringToDisplayString(inputString) {
|
||||
let number = parseInt(inputString);
|
||||
let hours = Math.floor(number / 10000);
|
||||
let minutes = Math.floor((number % 10000) / 100);
|
||||
let seconds = number % 100;
|
||||
|
||||
if (hours == 0 && minutes == 0) return '' + seconds;
|
||||
else if (hours == 0) return `${pad(minutes)}:${pad(seconds)}`;
|
||||
else return `${hours}:${pad(minutes)}:${pad(seconds)}`;
|
||||
}
|
||||
|
||||
class NumberButton {
|
||||
constructor(number) {
|
||||
this.label = '' + number;
|
||||
}
|
||||
|
||||
onclick() {
|
||||
if (common.state.inputString == '0') common.state.inputString = this.label;
|
||||
else common.state.inputString += this.label;
|
||||
common.state.setTime = inputStringToTime(common.state.inputString);
|
||||
feedback(true);
|
||||
updateDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
let ClearButton = {
|
||||
label: 'Clr',
|
||||
onclick: () => {
|
||||
common.state.inputString = '0';
|
||||
common.state.setTime = 0;
|
||||
updateDisplay();
|
||||
feedback(true);
|
||||
}
|
||||
};
|
||||
|
||||
let StartButton = {
|
||||
label: 'Go',
|
||||
onclick: () => {
|
||||
common.state.startTime = (new Date()).getTime();
|
||||
common.state.elapsedTime = 0;
|
||||
common.state.wasRunning = true;
|
||||
common.state.running = true;
|
||||
feedback(true);
|
||||
require('keytimer-tview.js').show(common);
|
||||
}
|
||||
};
|
||||
|
||||
const BUTTONS = [
|
||||
[new NumberButton(7), new NumberButton(8), new NumberButton(9), ClearButton],
|
||||
[new NumberButton(4), new NumberButton(5), new NumberButton(6), new NumberButton(0)],
|
||||
[new NumberButton(1), new NumberButton(2), new NumberButton(3), StartButton]
|
||||
];
|
||||
|
||||
function feedback(acceptable) {
|
||||
if (acceptable) Bangle.buzz(50, 0.5);
|
||||
else Bangle.buzz(200, 1);
|
||||
}
|
||||
|
||||
function drawButtons() {
|
||||
g.reset().clearRect(0, 44, 175, 175).setFont("Vector", 15).setFontAlign(0, 0);
|
||||
//Draw lines
|
||||
for (let x = 44; x <= 176; x += 44) {
|
||||
g.drawLine(x, 44, x, 175);
|
||||
}
|
||||
for (let y = 44; y <= 176; y += 44) {
|
||||
g.drawLine(0, y, 175, y);
|
||||
}
|
||||
for (let row = 0; row < 3; row++) {
|
||||
for (let col = 0; col < 4; col++) {
|
||||
g.drawString(BUTTONS[row][col].label, 22 + 44 * col, 66 + 44 * row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getFontSize(length) {
|
||||
let size = Math.floor(176 / length); //Characters of width needed per pixel
|
||||
size *= (20 / 12); //Convert to height
|
||||
// Clamp to between 6 and 20
|
||||
if (size < 6) return 6;
|
||||
else if (size > 20) return 20;
|
||||
else return Math.floor(size);
|
||||
}
|
||||
|
||||
function updateDisplay() {
|
||||
let displayString = inputStringToDisplayString(common.state.inputString);
|
||||
g.clearRect(0, 24, 175, 43).setColor(storage.readJSON('setting.json').theme.fg2).setFontAlign(1, -1).setFont("Vector", getFontSize(displayString.length)).drawString(displayString, 176, 24);
|
||||
}
|
||||
|
||||
exports.show = function (callerCommon) {
|
||||
common = callerCommon;
|
||||
g.reset();
|
||||
drawButtons();
|
||||
updateDisplay();
|
||||
};
|
||||
|
||||
exports.touch = function (button, xy) {
|
||||
let row = Math.floor((xy.y - 44) / 44);
|
||||
let col = Math.floor(xy.x / 44);
|
||||
if (row < 0) return;
|
||||
if (row > 2) row = 2;
|
||||
if (col < 0) col = 0;
|
||||
if (col > 3) col = 3;
|
||||
|
||||
BUTTONS[row][col].onclick();
|
||||
};
|
||||
|
||||
exports.swipe = function (dir) {
|
||||
if (dir == -1) {
|
||||
if (common.state.inputString.length == 1) common.state.inputString = '0';
|
||||
else common.state.inputString = common.state.inputString.substring(0, common.state.inputString.length - 1);
|
||||
|
||||
common.state.setTime = inputStringToTime(common.state.inputString);
|
||||
|
||||
feedback(true);
|
||||
updateDisplay();
|
||||
} else if (dir == 0) {
|
||||
EnterButton.onclick();
|
||||
}
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
{
|
||||
"id": "keytimer",
|
||||
"name": "Keypad Timer",
|
||||
"version": "0.02",
|
||||
"description": "A timer with a keypad that runs in the background",
|
||||
"icon": "icon.png",
|
||||
"type": "app",
|
||||
"tags": "tools",
|
||||
"supports": [
|
||||
"BANGLEJS2"
|
||||
],
|
||||
"allow_emulator": true,
|
||||
"storage": [
|
||||
{
|
||||
"name": "keytimer.app.js",
|
||||
"url": "app.js"
|
||||
},
|
||||
{
|
||||
"name": "keytimer.img",
|
||||
"url": "icon.js",
|
||||
"evaluate": true
|
||||
},
|
||||
{
|
||||
"name": "keytimer.boot.js",
|
||||
"url": "boot.js"
|
||||
},
|
||||
{
|
||||
"name": "keytimer-com.js",
|
||||
"url": "common.js"
|
||||
},
|
||||
{
|
||||
"name": "keytimer-ring.js",
|
||||
"url": "ring.js"
|
||||
},
|
||||
{
|
||||
"name": "keytimer-keys.js",
|
||||
"url": "keypad.js"
|
||||
},
|
||||
{
|
||||
"name": "keytimer-tview.js",
|
||||
"url": "timerview.js"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
const common = require('keytimer-com.js');
|
||||
|
||||
Bangle.loadWidgets()
|
||||
Bangle.drawWidgets()
|
||||
|
||||
Bangle.setLocked(false);
|
||||
Bangle.setLCDPower(true);
|
||||
|
||||
let brightness = 0;
|
||||
|
||||
setInterval(() => {
|
||||
Bangle.buzz(200);
|
||||
Bangle.setLCDBrightness(1 - brightness);
|
||||
brightness = 1 - brightness;
|
||||
}, 400);
|
||||
Bangle.buzz(200);
|
||||
|
||||
function stopTimer() {
|
||||
common.state.wasRunning = false;
|
||||
common.state.running = false;
|
||||
require("Storage").writeJSON(common.STATE_PATH, common.state);
|
||||
}
|
||||
|
||||
E.showAlert("Timer expired!").then(() => {
|
||||
stopTimer();
|
||||
load();
|
||||
});
|
||||
E.on('kill', stopTimer);
|
|
@ -0,0 +1,107 @@
|
|||
let common;
|
||||
|
||||
function drawButtons() {
|
||||
//Draw the backdrop
|
||||
const BAR_TOP = g.getHeight() - 24;
|
||||
g.setColor(0, 0, 1).setFontAlign(0, -1)
|
||||
.clearRect(0, BAR_TOP, g.getWidth(), g.getHeight())
|
||||
.fillRect(0, BAR_TOP, g.getWidth(), g.getHeight())
|
||||
.setColor(1, 1, 1)
|
||||
.drawLine(g.getWidth() / 2, BAR_TOP, g.getWidth() / 2, g.getHeight())
|
||||
|
||||
//Draw the buttons
|
||||
.drawImage(common.BUTTON_ICONS.reset, g.getWidth() / 4, BAR_TOP);
|
||||
if (common.state.running) {
|
||||
g.drawImage(common.BUTTON_ICONS.pause, g.getWidth() * 3 / 4, BAR_TOP);
|
||||
} else {
|
||||
g.drawImage(common.BUTTON_ICONS.play, g.getWidth() * 3 / 4, BAR_TOP);
|
||||
}
|
||||
}
|
||||
|
||||
function drawTimer() {
|
||||
let timeLeft = common.getTimeLeft();
|
||||
g.reset()
|
||||
.setFontAlign(0, 0)
|
||||
.setFont("Vector", 36)
|
||||
.clearRect(0, 24, 176, 152)
|
||||
|
||||
//Draw the timer
|
||||
.drawString((() => {
|
||||
let hours = timeLeft / 3600000;
|
||||
let minutes = (timeLeft % 3600000) / 60000;
|
||||
let seconds = (timeLeft % 60000) / 1000;
|
||||
|
||||
function pad(number) {
|
||||
return ('00' + parseInt(number)).slice(-2);
|
||||
}
|
||||
|
||||
if (hours >= 1) return `${parseInt(hours)}:${pad(minutes)}:${pad(seconds)}`;
|
||||
else return `${parseInt(minutes)}:${pad(seconds)}`;
|
||||
})(), g.getWidth() / 2, g.getHeight() / 2)
|
||||
|
||||
if (timeLeft <= 0) load('keytimer-ring.js');
|
||||
}
|
||||
|
||||
let timerInterval;
|
||||
|
||||
function setupTimerInterval() {
|
||||
if (timerInterval !== undefined) {
|
||||
clearInterval(timerInterval);
|
||||
}
|
||||
setTimeout(() => {
|
||||
timerInterval = setInterval(drawTimer, 1000);
|
||||
drawTimer();
|
||||
}, common.timeLeft % 1000);
|
||||
}
|
||||
|
||||
exports.show = function (callerCommon) {
|
||||
common = callerCommon;
|
||||
drawButtons();
|
||||
drawTimer();
|
||||
if (common.state.running) {
|
||||
setupTimerInterval();
|
||||
}
|
||||
}
|
||||
|
||||
function clearTimerInterval() {
|
||||
if (timerInterval !== undefined) {
|
||||
clearInterval(timerInterval);
|
||||
timerInterval = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
exports.touch = (button, xy) => {
|
||||
if (xy.y < 152) return;
|
||||
|
||||
if (button == 1) {
|
||||
//Reset the timer
|
||||
let setTime = common.state.setTime;
|
||||
let inputString = common.state.inputString;
|
||||
common.state = common.STATE_DEFAULT;
|
||||
common.state.setTime = setTime;
|
||||
common.state.inputString = inputString;
|
||||
clearTimerInterval();
|
||||
require('keytimer-keys.js').show(common);
|
||||
} else {
|
||||
if (common.state.running) {
|
||||
//Record the exact moment that we paused
|
||||
let now = (new Date()).getTime();
|
||||
common.state.pausedTime = now;
|
||||
|
||||
//Stop the timer
|
||||
common.state.running = false;
|
||||
clearTimerInterval();
|
||||
drawTimer();
|
||||
drawButtons();
|
||||
} else {
|
||||
//Start the timer and record when we started
|
||||
let now = (new Date()).getTime();
|
||||
common.state.elapsedTime += common.state.pausedTime - common.state.startTime;
|
||||
common.state.startTime = now;
|
||||
common.state.running = true;
|
||||
drawTimer();
|
||||
setupTimerInterval();
|
||||
drawButtons();
|
||||
}
|
||||
}
|
||||
};
|
|
@ -17,3 +17,5 @@
|
|||
0.15: Support for unload and quick return to the clock on 2v16
|
||||
0.16: Use a cache of app.info files to speed up loading the launcher
|
||||
0.17: Don't display 'Loading...' now the watch has its own loading screen
|
||||
0.18: Add 'back' icon in top-left to go back to clock
|
||||
0.19: Fix regression after back button added (returnToClock was called twice!)
|
||||
|
|
|
@ -41,6 +41,17 @@ let apps = launchCache.apps;
|
|||
// Now apps list is loaded - render
|
||||
if (!settings.fullscreen)
|
||||
Bangle.loadWidgets();
|
||||
|
||||
let returnToClock = function() {
|
||||
// unload everything manually
|
||||
// ... or we could just call `load();` but it will be slower
|
||||
Bangle.setUI(); // remove scroller's handling
|
||||
if (lockTimeout) clearTimeout(lockTimeout);
|
||||
Bangle.removeListener("lock", lockHandler);
|
||||
// now load the default clock - just call .bootcde as this has the code already
|
||||
setTimeout(eval,0,s.read(".bootcde"));
|
||||
}
|
||||
|
||||
E.showScroller({
|
||||
h : 64*scaleval, c : apps.length,
|
||||
draw : (i, r) => {
|
||||
|
@ -62,26 +73,11 @@ E.showScroller({
|
|||
} else {
|
||||
load(app.src);
|
||||
}
|
||||
}
|
||||
},
|
||||
back : returnToClock // button press or tap in top left calls returnToClock now
|
||||
});
|
||||
g.flip(); // force a render before widgets have finished drawing
|
||||
|
||||
let returnToClock = function() {
|
||||
// unload everything manually
|
||||
// ... or we could just call `load();` but it will be slower
|
||||
Bangle.setUI(); // remove scroller's handling
|
||||
if (lockTimeout) clearTimeout(lockTimeout);
|
||||
Bangle.removeListener("lock", lockHandler);
|
||||
// now load the default clock - just call .bootcde as this has the code already
|
||||
setTimeout(eval,0,s.read(".bootcde"));
|
||||
}
|
||||
|
||||
// on bangle.js 2, the screen is used for navigating, so the single button goes back
|
||||
// on bangle.js 1, the buttons are used for navigating
|
||||
if (process.env.HWVERSION==2) {
|
||||
setWatch(returnToClock, BTN1, {edge:"falling"});
|
||||
}
|
||||
|
||||
// 10s of inactivity goes back to clock
|
||||
Bangle.setLocked(false); // unlock initially
|
||||
let lockTimeout;
|
||||
|
|