Merge branch 'bttfclock' of https://github.com/NoobEjby/BangleApps into bttfclock

pull/3702/head
noobejby 2024-12-23 23:20:11 +01:00
commit 754820fcdc
152 changed files with 5543 additions and 1599 deletions

View File

@ -5,3 +5,5 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "daily"
reviewers:
- "gfwilliams"

View File

@ -289,6 +289,7 @@ and which gives information about the app for the Launcher.
"dependencies" : { "message":"widget" } // optional, depend on a specific type of widget - see provides_widgets "dependencies" : { "message":"widget" } // optional, depend on a specific type of widget - see provides_widgets
"provides_modules" : ["messageicons"] // optional, this app provides a module that can be used with 'require' "provides_modules" : ["messageicons"] // optional, this app provides a module that can be used with 'require'
"provides_widgets" : ["battery"] // optional, this app provides a type of widget - 'alarm/battery/bluetooth/pedometer/message' "provides_widgets" : ["battery"] // optional, this app provides a type of widget - 'alarm/battery/bluetooth/pedometer/message'
"provides_features" : ["welcome"] // optional, this app provides some feature, used to ensure two aren't installed at once. Currently just 'welcome'
"default" : true, // set if an app is the default implementer of something (a widget/module/etc) "default" : true, // set if an app is the default implementer of something (a widget/module/etc)
"readme": "README.md", // if supplied, a link to a markdown-style text file "readme": "README.md", // if supplied, a link to a markdown-style text file
// that contains more information about this app (usage, etc) // that contains more information about this app (usage, etc)

View File

@ -15,3 +15,4 @@
0.13: Show day of the week in date 0.13: Show day of the week in date
0.14: Fixed "Today" and "Yesterday" wrongly displayed for allDay events on some time zones 0.14: Fixed "Today" and "Yesterday" wrongly displayed for allDay events on some time zones
0.15: Minor code improvements 0.15: Minor code improvements
0.16: Correct date for all day events in negative timezones, improve locale display

View File

@ -30,11 +30,16 @@ var settings = require("Storage").readJSON("agenda.settings.json",true)||{};
CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp); CALENDAR=CALENDAR.sort((a,b)=>a.timestamp - b.timestamp);
function getDate(timestamp) { function getDate(timestamp, allDay) {
return new Date(timestamp*1000); // All day events are always in UTC and always start at 00:00:00, so we
// need to "undo" the timezone offsetting to make sure that the day is
// correct.
var offset = allDay ? new Date().getTimezoneOffset() * 60 : 0
return new Date((timestamp+offset)*1000);
} }
function formatDay(date) { function formatDay(date) {
let formattedDate = Locale.dow(date,1) + " " + Locale.date(date).replace(/\d\d\d\d/,""); let formattedDate = Locale.dow(date,1) + " " + Locale.date(date).replace(/,*\s*\d\d\d\d/,"");
if (!settings.useToday) { if (!settings.useToday) {
return formattedDate; return formattedDate;
} }
@ -57,8 +62,9 @@ function formatDateLong(date, includeDay, allDay) {
} }
return shortTime; return shortTime;
} }
function formatDateShort(date, allDay) { function formatDateShort(date, allDay) {
return formatDay(date)+(allDay?"":Locale.time(date,1)+Locale.meridian(date)); return formatDay(date)+(allDay?"":" "+Locale.time(date,1)+Locale.meridian(date));
} }
var lines = []; var lines = [];
@ -69,16 +75,19 @@ function showEvent(ev) {
//var lines = []; //var lines = [];
if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10); if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10);
var titleCnt = lines.length; var titleCnt = lines.length;
var start = getDate(ev.timestamp); var start = getDate(ev.timestamp, ev.allDay);
var end = getDate((+ev.timestamp) + (+ev.durationInSeconds)); // All day events end at the midnight boundary of the following day. Here, we
// subtract one second for all day events so the days display correctly.
const allDayEndCorrection = ev.allDay ? 1 : 0;
var end = getDate((+ev.timestamp) + (+ev.durationInSeconds) - allDayEndCorrection, ev.allDay);
var includeDay = true; var includeDay = true;
if (titleCnt) lines.push(""); // add blank line after title if (titleCnt) lines.push(""); // add blank line after title
if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth()) if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth())
includeDay = false; includeDay = false;
if(includeDay && ev.allDay) { if(!includeDay && ev.allDay) {
//single day all day (average to avoid getting previous day) //single day all day
lines = lines.concat( lines = lines.concat(
g.wrapString(formatDateLong(new Date((start+end)/2), includeDay, ev.allDay), g.getWidth()-10)); g.wrapString(formatDateLong(start, includeDay, ev.allDay), g.getWidth()-10));
} else if(includeDay || ev.allDay) { } else if(includeDay || ev.allDay) {
lines = lines.concat( lines = lines.concat(
/*LANG*/"Start"+":", /*LANG*/"Start"+":",
@ -137,7 +146,7 @@ function showList() {
if (!ev) return; if (!ev) return;
var isPast = false; var isPast = false;
var x = r.x+2, title = ev.title; var x = r.x+2, title = ev.title;
var body = formatDateShort(getDate(ev.timestamp),ev.allDay)+"\n"+(ev.location?ev.location:/*LANG*/"No location"); var body = formatDateShort(getDate(ev.timestamp, ev.allDay),ev.allDay)+"\n"+(ev.location?ev.location:/*LANG*/"No location");
if(settings.pastEvents) isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000; if(settings.pastEvents) isPast = ev.timestamp + ev.durationInSeconds < (new Date())/1000;
if (title) g.setFontAlign(-1,-1).setFont(fontBig) if (title) g.setFontAlign(-1,-1).setFont(fontBig)
.setColor(isPast ? "#888" : g.theme.fg).drawString(title, x+4,r.y+2); .setColor(isPast ? "#888" : g.theme.fg).drawString(title, x+4,r.y+2);

View File

@ -1,7 +1,7 @@
{ {
"id": "agenda", "id": "agenda",
"name": "Agenda", "name": "Agenda",
"version": "0.15", "version": "0.16",
"description": "Simple agenda", "description": "Simple agenda",
"icon": "agenda.png", "icon": "agenda.png",
"screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}], "screenshots": [{"url":"screenshot_agenda_overview.png"}, {"url":"screenshot_agenda_event1.png"}, {"url":"screenshot_agenda_event2.png"}],

View File

@ -2,13 +2,26 @@ Alpine Navigator
================ ================
App that performs GPS monitoring to track and display position relative to a given origin in realtime. App that performs GPS monitoring to track and display position relative to a given origin in realtime.
![screenshot](./sample.png) ![screenshot](./sample.png)
[compass 5]
altitude
[start 1] [current 2]
distance
[from start 3] [track 4]
[btn1 -- screen lock]
[btn2 -- remove points]
[btn3 -- pause]
Functions Functions
--------- ---------
Note if you've not used GPS yet I suggest using one of the GPS apps to get your first fix and confirm as I've found that helps initially. Note if you've not used GPS yet, I suggest using one of the GPS apps to get your first fix and confirm, as I've found that helps initially.
The GPS and magnetometer will be turned on and after a few moments, when the watch buzzes and the dot turns from red to pink, that means the GPS is fixed. all your movements now will be displayed with a line drawn back to show your position relative to the start. New waypoints will be added based on checking every 10 seconds for at least 5 meters of movement. The map will scale to your distance travelled so the route will always remain within the window, the accelerometer/pedometer is not used - this is a purely GPS and compass solution so can be used for driving/cycling etc. A log file will be recorded that tracks upto 1000 waypoints, this isn't a big file and you could remove the limit but I've kept it fairly conservative here as it's not intended as a main feature, there's already good GPS recorders for the Bangle. The following other items are displayed: The GPS and magnetometer will be turned on and after a few moments, when the watch buzzes and the dot turns from red to pink, that means the GPS is fixed. All your movements now will be displayed with a line drawn back to show your position relative to the start. New waypoints will be added based on checking every 10 seconds for at least 5 meters of movement. The map will scale to your distance travelled so the route will always remain within the window, the accelerometer/pedometer is not used - this is a purely GPS and compass solution so can be used for driving/cycling etc. A log file will be recorded that tracks upto 1000 waypoints, this isn't a big file and you could remove the limit, but I've kept it fairly conservative here, as it's not intended as a main feature, there's already good GPS recorders for the Bangle. The following other items are displayed:
1. altitude at origin, this is displayed left of the centre. 1. altitude at origin, this is displayed left of the centre.
2. current altitude, displayed centre right 2. current altitude, displayed centre right
@ -16,12 +29,12 @@ The GPS and magnetometer will be turned on and after a few moments, when the wat
4. distance travelled, bottom right (meters) 4. distance travelled, bottom right (meters)
5. compass heading, at the top 5. compass heading, at the top
For the display, the route is kept at a set resolution, so there's no risk of running into memory problems if you run this for long periods or any length of time because the waypoints will be reduced when it reaches a set threshold so you may see the path smooth out slightly at intervals. For the display, the route is kept at a set resolution, so there's no risk of running into memory problems if you run this for long periods or any length of time, because the waypoints will be reduced when it reaches a set threshold, so you may see the path smooth out slightly at intervals.
If you get strange values or dashes for the compass, it just needs calibration so you need to move the watch around briefly for this each time, ideally 360 degrees around itself, which involves taking the watch off. If you don't want to do that you can also just wave your hand around for a few seconds like you're at a rave or Dr Strange making a Sling Ring but often just moving your wrist a bit is enough. If you get strange values or dashes for the compass, it just needs calibration so you need to move the watch around briefly for this each time, ideally 360 degrees around itself, which involves taking the watch off. If you don't want to do that you can also just wave your hand around for a few seconds like you're at a rave or Dr Strange making a Sling Ring, but often just moving your wrist a bit is enough.
The buttons do the following: The buttons do the following:
BTN1: this will display an 'X' in the bottom of the screen and lock all the buttons, this is to prevent you accidentally pressing either of the below. Remember to press this again to unlock it! soft and hard reset will both still work. BTN1: this will display an 'X' in the bottom of the screen and lock all the buttons, this is to prevent you accidentally pressing either of the below. Remember to press this again to unlock it! Soft and hard reset will both still work.
BTN2: this removes all waypoints aside from the origin and your current location; sometimes during smaller journeys and walks, the GPS can give sporadic differences in locations because of the error margins of GPS and this can add noise to your route. BTN2: this removes all waypoints aside from the origin and your current location; sometimes during smaller journeys and walks, the GPS can give sporadic differences in locations because of the error margins of GPS and this can add noise to your route.
BTN3: this will pause the GPS and magnetometer, useful for saving power for situations where you don't necessarily need to track parts of your route e.g. you're going indoors/shelter for some time. You'll know it's paused because the compass won't update it's reading and all the metrics will be blacked out on the screen. BTN3: this will pause the GPS and magnetometer, useful for saving power for situations where you don't necessarily need to track parts of your route e.g. you're going indoors/shelter for some time. You'll know it's paused because the compass won't update it's reading and all the metrics will be blacked out on the screen.

View File

@ -5,3 +5,5 @@
0.05: Fix support for dark theme + support widgets + 0.05: Fix support for dark theme + support widgets +
add settings for widgets, order of drawing and hour hand length add settings for widgets, order of drawing and hour hand length
0.06: Fix issue showing widgets when app is fast-loaded into from launcher with widgets disabled 0.06: Fix issue showing widgets when app is fast-loaded into from launcher with widgets disabled
0.07: Enable fast loading and queue updates to the second
0.08: Restore redraw on charging event + fixup for safer fast-loading

View File

@ -1,3 +1,4 @@
{
const defaultSettings = { const defaultSettings = {
loadWidgets : false, loadWidgets : false,
textAboveHands : false, textAboveHands : false,
@ -11,9 +12,9 @@ const zahlpos=(function() {
let z=[]; let z=[];
let sk=1; let sk=1;
for(let i=-10;i<50;i+=5){ for(let i=-10;i<50;i+=5){
let win=i*2*Math.PI/60; let win=i*2*Math.PI/60;
let xsk =c.x+2+Math.cos(win)*(c.x-10), let xsk =c.x+2+Math.cos(win)*(c.x-10),
ysk =c.y+2+Math.sin(win)*(c.x-10); ysk =c.y+2+Math.sin(win)*(c.x-10);
if(sk==3){xsk-=10;} if(sk==3){xsk-=10;}
if(sk==6){ysk-=10;} if(sk==6){ysk-=10;}
if(sk==9){xsk+=10;} if(sk==9){xsk+=10;}
@ -25,18 +26,15 @@ const zahlpos=(function() {
return z; return z;
})(); })();
let unlock = false; const zeiger = function(len,dia,tim) {
function zeiger(len,dia,tim){
const x=c.x+ Math.cos(tim)*len/2, const x=c.x+ Math.cos(tim)*len/2,
y=c.y + Math.sin(tim)*len/2, y=c.y + Math.sin(tim)*len/2,
d={"d":3,"x":dia/2*Math.cos(tim+Math.PI/2),"y":dia/2*Math.sin(tim+Math.PI/2)}, d={"d":3,"x":dia/2*Math.cos(tim+Math.PI/2),"y":dia/2*Math.sin(tim+Math.PI/2)},
pol=[c.x-d.x,c.y-d.y,c.x+d.x,c.y+d.y,x+d.x,y+d.y,x-d.x,y-d.y]; pol=[c.x-d.x,c.y-d.y,c.x+d.x,c.y+d.y,x+d.x,y+d.y,x-d.x,y-d.y];
return pol; return pol;
};
} const drawHands = function(d) {
function drawHands(d) {
let m=d.getMinutes(), h=d.getHours(), s=d.getSeconds(); let m=d.getMinutes(), h=d.getHours(), s=d.getSeconds();
g.setColor(1,1,1); g.setColor(1,1,1);
@ -61,32 +59,60 @@ function drawHands(d) {
g.fillPoly(sekz,true); g.fillPoly(sekz,true);
} }
g.fillCircle(c.x,c.y,4); g.fillCircle(c.x,c.y,4);
} };
function drawText(d) { const drawText = function(d) {
g.setFont("Vector",10); g.setFont("Vector",10);
g.setBgColor(0,0,0); g.setBgColor(0,0,0);
g.setColor(1,1,1); g.setColor(1,1,1);
let dateStr = require("locale").date(d); const dateStr = require("locale").date(d);
g.drawString(dateStr, c.x, c.y+20, true); g.drawString(dateStr, c.x, c.y+20, true);
let batStr = Math.round(E.getBattery()/5)*5+"%"; const batStr = Math.round(E.getBattery()/5)*5+"%";
if (Bangle.isCharging()) { if (Bangle.isCharging()) {
g.setBgColor(1,0,0); g.setBgColor(1,0,0);
} }
g.drawString(batStr, c.x, c.y+40, true); g.drawString(batStr, c.x, c.y+40, true);
} };
function drawNumbers() { const drawNumbers = function() {
//draws the numbers on the screen //draws the numbers on the screen
g.setFont("Vector",20); g.setFont("Vector",20);
g.setColor(1,1,1); g.setColor(1,1,1);
g.setBgColor(0,0,0); g.setBgColor(0,0,0);
for(let i = 0;i<12;i++){ for(let i = 0;i<12;i++){
g.drawString(zahlpos[i][0],zahlpos[i][1],zahlpos[i][2],true); g.drawString(zahlpos[i][0],zahlpos[i][1],zahlpos[i][2],true);
} }
} };
function draw(){ let drawTimeout;
let queueMillis = 1000;
let unlock = true;
const updateState = function() {
if (Bangle.isLCDOn()) {
if (!Bangle.isLocked()) {
queueMillis = 1000;
unlock = true;
} else {
queueMillis = 60000;
unlock = false;
}
draw();
} else {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
}
};
const queueDraw = function() {
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, queueMillis - (Date.now() % queueMillis));
};
const draw = function() {
// draw black rectangle in the middle to clear screen from scale and hands // draw black rectangle in the middle to clear screen from scale and hands
g.setColor(0,0,0); g.setColor(0,0,0);
g.fillRect(10,10,2*c.x-10,2*c.x-10); g.fillRect(10,10,2*c.x-10,2*c.x-10);
@ -100,10 +126,11 @@ function draw(){
} else { } else {
drawText(d); drawHands(d); drawText(d); drawHands(d);
} }
} queueDraw();
};
//draws the scale once the app is startet //draws the scale once the app is startet
function drawScale(){ const drawScale = function() {
// clear the screen // clear the screen
g.setBgColor(0,0,0); g.setBgColor(0,0,0);
g.clear(); g.clear();
@ -117,36 +144,35 @@ function drawScale(){
g.fillRect(10,10,2*c.x-10,2*c.x-10); g.fillRect(10,10,2*c.x-10,2*c.x-10);
g.setColor(1,1,1); g.setColor(1,1,1);
} }
} };
//// main running sequence //// //// main running sequence ////
// Show launcher when middle button pressed, and widgets that we're clock // Show launcher when middle button pressed, and widgets that we're clock
Bangle.setUI("clock"); Bangle.setUI({
mode: "clock",
remove: function() {
Bangle.removeListener('lcdPower', updateState);
Bangle.removeListener('lock', updateState);
Bangle.removeListener('charging', draw);
// We clear drawTimout after removing all listeners, because they can add one again
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
require("widget_utils").show();
}
});
// Load widgets if needed, and make them show swipeable // Load widgets if needed, and make them show swipeable
if (settings.loadWidgets) { if (settings.loadWidgets) {
Bangle.loadWidgets(); Bangle.loadWidgets();
require("widget_utils").swipeOn(); require("widget_utils").swipeOn();
} else if (global.WIDGETS) require("widget_utils").hide(); } else if (global.WIDGETS) require("widget_utils").hide();
// Clear the screen once, at startup
drawScale();
draw();
let secondInterval = setInterval(draw, 1000);
// Stop updates when LCD is off, restart when on // Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{ Bangle.on('lcdPower', updateState);
if (secondInterval) clearInterval(secondInterval); Bangle.on('lock', updateState);
secondInterval = undefined; Bangle.on('charging', draw); // Immediately redraw when charger (dis)connected
if (on) {
secondInterval = setInterval(draw, 1000); updateState();
draw(); // draw immediately drawScale();
} draw();
}); }
Bangle.on('lock',on=>{
unlock = !on;
if (secondInterval) clearInterval(secondInterval);
secondInterval = setInterval(draw, unlock ? 1000 : 60000);
draw(); // draw immediately
});
Bangle.on('charging',on=>{draw();});

View File

@ -1,7 +1,7 @@
{ "id": "andark", { "id": "andark",
"name": "Analog Dark", "name": "Analog Dark",
"shortName":"AnDark", "shortName":"AnDark",
"version":"0.06", "version":"0.08",
"description": "analog clock face without disturbing widgets", "description": "analog clock face without disturbing widgets",
"icon": "andark_icon.png", "icon": "andark_icon.png",
"type": "clock", "type": "clock",

View File

@ -49,7 +49,7 @@
<input type="radio" name="gnss_select" value="6"><i class="form-icon"></i> BDS+GLONASS <input type="radio" name="gnss_select" value="6"><i class="form-icon"></i> BDS+GLONASS
</label> </label>
<label class="form-radio"> <label class="form-radio">
<input type="radio" name="gnss_select" value="6"><i class="form-icon"></i> GPS+BDS+GLONASS <input type="radio" name="gnss_select" value="7"><i class="form-icon"></i> GPS+BDS+GLONASS
</label> </label>
</div> </div>
</div> </div>

2
apps/ateatimer/ChangeLog Normal file
View File

@ -0,0 +1,2 @@
0.01: First release
0.02: Fix icon, utilize sched, show running timer on app relaunch

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("mEwgIKHgwFKo0gAofmsALEGR0H/+f//+gEP/4ACAoXAn4FDAQn8g0DAoX4g0BAoXx4E4AoXhAoN/8EP4AzBn/4h/IC4M//kPzgjBz/+h+MAoMfj0PNYUfh4FDh8HAo0wg/454RBmBDBAoRnBCIIjCAAMPF4IFDHYOIgEBj5HBzkAIIPAIIIFBn4hBLIU+AoPgwEQvwFBOIX8CgP5w0RAoSJC/AsB/0EJwIgB/+Aj/wAoN/VgPgQwQFBwBKCXAQWBAAfgAoocCAoQcCAAPAj7XEcYIABcYLIBAAJBBA=="))

156
apps/ateatimer/app.js Normal file
View File

@ -0,0 +1,156 @@
// Tea Timer Application for Bangle.js 2 using sched library
let timerDuration = (() => {
let file = require("Storage").open("ateatimer.data", "r");
let data = file.read(4); // Assuming 4 bytes for storage
return data ? parseInt(data, 10) : 4 * 60; // Default to 4 minutes
})();
let timeRemaining = timerDuration;
let timerRunning = false;
function saveDefaultDuration() {
let file = require("Storage").open("ateatimer.data", "w");
file.write(timerDuration.toString());
}
function drawTime() {
g.clear();
g.setFont("Vector", 40);
g.setFontAlign(0, 0); // Center align
const minutes = Math.floor(Math.abs(timeRemaining) / 60);
const seconds = Math.abs(timeRemaining) % 60;
const sign = timeRemaining < 0 ? "-" : "";
const timeStr = `${sign}${minutes}:${seconds.toString().padStart(2, '0')}`;
g.drawString(timeStr, g.getWidth() / 2, g.getHeight() / 2);
// Draw Increase button (triangle pointing up)
g.fillPoly([
g.getWidth() / 2, g.getHeight() / 2 - 80, // Top vertex
g.getWidth() / 2 - 20, g.getHeight() / 2 - 60, // Bottom-left vertex
g.getWidth() / 2 + 20, g.getHeight() / 2 - 60 // Bottom-right vertex
]);
// Draw Decrease button (triangle pointing down)
g.fillPoly([
g.getWidth() / 2, g.getHeight() / 2 + 80, // Bottom vertex
g.getWidth() / 2 - 20, g.getHeight() / 2 + 60, // Top-left vertex
g.getWidth() / 2 + 20, g.getHeight() / 2 + 60 // Top-right vertex
]);
g.flip();
}
function startTimer() {
if (timerRunning) return;
if (timeRemaining == 0) return;
timerRunning = true;
// Save the default duration on timer start
timerDuration = timeRemaining;
saveDefaultDuration();
scheduleTimer();
// Start the secondary timer to update the display
setInterval(updateDisplay, 1000);
}
function scheduleTimer() {
// Schedule a new timer using the sched library
require("sched").setAlarm("ateatimer", {
msg: "Tea is ready!",
timer: timeRemaining * 1000, // Convert to milliseconds
vibrate: ".." // Default vibration pattern
});
// Ensure the scheduler updates
require("sched").reload();
}
function resetTimer() {
// Cancel the existing timer
require("sched").setAlarm("ateatimer", undefined);
require("sched").reload();
timerRunning = false;
timeRemaining = timerDuration;
drawTime();
}
function adjustTime(amount) {
if (-amount > timeRemaining) {
// Return if result will be negative
return;
}
timeRemaining += amount;
timeRemaining = Math.max(0, timeRemaining); // Ensure time doesn't go negative
if (timerRunning) {
// Update the existing timer with the new remaining time
let alarm = require("sched").getAlarm("ateatimer");
if (alarm) {
// Cancel the current alarm
require("sched").setAlarm("ateatimer", undefined);
// Set a new alarm with the updated time
scheduleTimer();
}
}
drawTime();
}
function handleTouch(x, y) {
const centerY = g.getHeight() / 2;
if (y < centerY - 40) {
// Increase button area
adjustTime(60);
} else if (y > centerY + 40) {
// Decrease button area
adjustTime(-60);
} else {
// Center area
if (!timerRunning) {
startTimer();
}
}
}
// Function to update the display every second
function updateDisplay() {
if (timerRunning) {
let alarm = require("sched").getAlarm("ateatimer");
timeRemaining = Math.floor(require("sched").getTimeToAlarm(alarm) / 1000);
drawTime();
if (timeRemaining <= 0) {
timeRemaining = 0;
clearInterval(updateDisplay);
timerRunning = false;
}
}
}
// Handle physical button press for resetting timer
setWatch(() => {
if (timerRunning) {
resetTimer();
} else {
startTimer();
}
}, BTN1, { repeat: true, edge: "falling" });
// Handle touch
Bangle.on("touch", (zone, xy) => {
handleTouch(xy.x, xy.y, false);
});
let isRunning = require("sched").getAlarm("ateatimer");
if (isRunning) {
timerRunning = true;
// Start the timer to update the display
setInterval(updateDisplay, 1000);
} else {
// Draw the initial timer display
drawTime();
}

1
apps/ateatimer/app.json Normal file
View File

@ -0,0 +1 @@
{ "duration": 240 }

BIN
apps/ateatimer/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@ -0,0 +1,14 @@
{ "id": "ateatimer",
"name": "A Tea Timer",
"shortName":"A Tea Timer",
"icon": "app.png",
"version":"0.02",
"description": "Simple app for setting timers for tea. Touch up and down to change time, and time or button to start counting. When timer is running, button will stop timer and reset counter to last used value.",
"tags": "timer",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"ateatimer.app.js","url":"app.js"},
{"name":"ateatimer.img","url":"app-icon.js","evaluate":true}
],
"dependencies": {"scheduler":"type"}
}

View File

@ -1,6 +1,5 @@
0.01: New App! 0.01: New App!
0.02: Don't fire if the app uses swipes already. 0.02: Don't fire if the app uses swipes already.
0.03: Only count defined handlers in the handler array. 0.03: Only count defined handlers in the handler array.
0.04: Fix messages auto opened by `messagegui` could not be blacklisted. Needs 0.04: Fix messages auto opened by `messagegui` could not be blacklisted. Needs a refresh by deselecting and reselecting the "Messages" app throught Back Swipe settings.
a refresh by deselecting and reselecting the "Messages" app throught Back Swipe 0.05: React on swipes before the active app (for the most part) by using `prependListener`.
settings.

View File

@ -38,6 +38,7 @@
// if we're in an app that has a back button, run the callback for it // if we're in an app that has a back button, run the callback for it
if (global.BACK && countHandlers("swipe")<=settings.standardNumSwipeHandlers && countHandlers("drag")<=settings.standardNumDragHandlers) { if (global.BACK && countHandlers("swipe")<=settings.standardNumSwipeHandlers && countHandlers("drag")<=settings.standardNumDragHandlers) {
global.BACK(); global.BACK();
E.stopEventPropagation();
} }
} }
} }
@ -56,5 +57,5 @@
} }
// Listen to left to right swipe // Listen to left to right swipe
Bangle.on("swipe", goBack); Bangle.prependListener("swipe", goBack);
})(); })();

View File

@ -1,7 +1,7 @@
{ "id": "backswipe", { "id": "backswipe",
"name": "Back Swipe", "name": "Back Swipe",
"shortName":"BackSwipe", "shortName":"BackSwipe",
"version":"0.04", "version":"0.05",
"description": "Service that allows you to use an app's back button using left to right swipe gesture", "description": "Service that allows you to use an app's back button using left to right swipe gesture",
"icon": "app.png", "icon": "app.png",
"tags": "back,gesture,swipe", "tags": "back,gesture,swipe",

View File

@ -8,3 +8,4 @@
0.08: Fixed typo in settings.js for DRAGDOWN to make option work 0.08: Fixed typo in settings.js for DRAGDOWN to make option work
0.09: You can now back out of the calendar using the button 0.09: You can now back out of the calendar using the button
0.10: Fix linter warnings 0.10: Fix linter warnings
0.11: Added option to show prior weeks on clock calendar

View File

@ -2,7 +2,8 @@ Bangle.setUI("clock");
Bangle.loadWidgets(); Bangle.loadWidgets();
var s = Object.assign({ var s = Object.assign({
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. CAL_ROWS: 4, //total number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
CAL_ROWS_PRIOR: 0, //number of calendar rows.(weeks) that show above the current week
BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually
MODE24: true, //24h mode vs 12h mode MODE24: true, //24h mode vs 12h mode
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
@ -178,7 +179,7 @@ function drawWatch() {
const dow = (s.FIRSTDAY + d.getDay()) % 7; //MO=0, SU=6 const dow = (s.FIRSTDAY + d.getDay()) % 7; //MO=0, SU=6
const today = d.getDate(); const today = d.getDate();
var rD = new Date(d.getTime()); var rD = new Date(d.getTime());
rD.setDate(rD.getDate() - dow); rD.setDate(rD.getDate() - dow - s.CAL_ROWS_PRIOR * 7);
var rDate = rD.getDate(); var rDate = rD.getDate();
g.setFontAlign(1, 1); g.setFontAlign(1, 1);
for (var y = 1; y <= s.CAL_ROWS; y++) { for (var y = 1; y <= s.CAL_ROWS; y++) {
@ -187,7 +188,7 @@ function drawWatch() {
bottomrightY = y * CELL_H + CAL_Y; bottomrightY = y * CELL_H + CAL_Y;
g.setFont("Vector", 16); g.setFont("Vector", 16);
var fg = ((s.REDSUN && rD.getDay() == 0) || (s.REDSAT && rD.getDay() == 6)) ? '#f00' : '#fff'; var fg = ((s.REDSUN && rD.getDay() == 0) || (s.REDSAT && rD.getDay() == 6)) ? '#f00' : '#fff';
if (y == 1 && today == rDate) { if (y == s.CAL_ROWS_PRIOR + 1 && today == rDate) {
g.setColor('#0f0'); g.setColor('#0f0');
g.fillRect(bottomrightX - CELL_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2); g.fillRect(bottomrightX - CELL_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2);
g.setColor('#000'); g.setColor('#000');

View File

@ -1,7 +1,7 @@
{ {
"id": "clockcal", "id": "clockcal",
"name": "Clock & Calendar", "name": "Clock & Calendar",
"version": "0.10", "version": "0.11",
"description": "Clock with Calendar", "description": "Clock with Calendar",
"readme":"README.md", "readme":"README.md",
"icon": "app.png", "icon": "app.png",

View File

@ -1,7 +1,8 @@
(function (back) { (function (back) {
var FILE = "clockcal.json"; var FILE = "clockcal.json";
const defaults={ const defaults={
CAL_ROWS: 4, //number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets. CAL_ROWS: 4, //total number of calendar rows.(weeks) Shouldn't exceed 5 when using widgets.
CAL_ROWS_PRIOR: 0, //number of calendar rows.(weeks) that show above the current week
BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually BUZZ_ON_BT: true, //2x slow buzz on disconnect, 2x fast buzz on connect. Will be extra widget eventually
MODE24: true, //24h mode vs 12h mode MODE24: true, //24h mode vs 12h mode
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
@ -39,6 +40,14 @@
writeSettings(); writeSettings();
} }
}, },
'#Cal Rows Prior': {
value: settings.CAL_ROWS_PRIOR,
min: 0, max: 4,
onchange: v => {
settings.CAL_ROWS_PRIOR = v;
writeSettings();
}
},
'Clock mode': { 'Clock mode': {
value: settings.MODE24, value: settings.MODE24,
format: v => v ? "24h" : "12h", format: v => v ? "24h" : "12h",

View File

@ -58,6 +58,7 @@ var draw = function () {
}, 60000 - (date.getTime() % 60000)); }, 60000 - (date.getTime() % 60000));
}; };
var reload = function () { var reload = function () {
var scroller;
var showMenu = function () { var showMenu = function () {
var menu = { var menu = {
"Restore to full power": drainedRestore, "Restore to full power": drainedRestore,
@ -69,9 +70,12 @@ var reload = function () {
menu["Settings"] = function () { return load("setting.app.js"); }; menu["Settings"] = function () { return load("setting.app.js"); };
menu["Recovery"] = function () { return Bangle.showRecoveryMenu(); }; menu["Recovery"] = function () { return Bangle.showRecoveryMenu(); };
menu["Exit menu"] = reload; menu["Exit menu"] = reload;
if (scroller) {
menu[""] = { selected: scroller.scroll };
}
if (nextDraw) if (nextDraw)
clearTimeout(nextDraw); clearTimeout(nextDraw);
E.showMenu(menu); (scroller = E.showMenu(menu).scroller);
}; };
Bangle.setUI({ Bangle.setUI({
mode: "custom", mode: "custom",

View File

@ -78,8 +78,9 @@ const draw = () => {
}; };
const reload = () => { const reload = () => {
let scroller: MenuInstance["scroller"] | undefined;
const showMenu = () => { const showMenu = () => {
const menu: { [k: string]: () => void } = { const menu: Menu = {
"Restore to full power": drainedRestore, "Restore to full power": drainedRestore,
}; };
@ -92,8 +93,12 @@ const reload = () => {
menu["Recovery"] = () => Bangle.showRecoveryMenu(); menu["Recovery"] = () => Bangle.showRecoveryMenu();
menu["Exit menu"] = reload; menu["Exit menu"] = reload;
if(scroller){
menu[""] = { selected: scroller.scroll };
}
if(nextDraw) clearTimeout(nextDraw); if(nextDraw) clearTimeout(nextDraw);
E.showMenu(menu); ({ scroller } = E.showMenu(menu));
}; };
Bangle.setUI({ Bangle.setUI({

View File

@ -31,3 +31,4 @@ when moving pages. Add caching for faster startups.
0.24: Add buzz-on-interaction setting 0.24: Add buzz-on-interaction setting
0.25: Minor code improvements 0.25: Minor code improvements
0.26: Bangle 2: Postpone loading icons that are not needed initially. 0.26: Bangle 2: Postpone loading icons that are not needed initially.
0.27: Bangle 2: Add setting to remember and present the last open page between instances of dtlaunch.

View File

@ -17,7 +17,9 @@ Bangle 2:
![shot3](https://user-images.githubusercontent.com/89286474/146471760-5497fd1b-8e82-4fd5-a4e3-4734701a7dbd.png) ![shot3](https://user-images.githubusercontent.com/89286474/146471760-5497fd1b-8e82-4fd5-a4e3-4734701a7dbd.png)
## Controls- Bangle ## Controls
### Bangle 1
**BTN1** - move backward through app icons on a page **BTN1** - move backward through app icons on a page
@ -35,10 +37,28 @@ Bangle 2:
**Touch Middle(1+2) area** - run the selected app **Touch Middle(1+2) area** - run the selected app
## Controls- Bangle 2 ### Bangle 2
**Touch** - icon to select, second touch launches app **Touch** - icon to select, second touch launches app
**Swipe Left/Up** - move to next page of app icons **Swipe Left/Up** - move to next page of app icons
**Swipe Right/Down** - move to previous page of app icons **Swipe Right/Down** - move to previous page of app icons
## Settings
**Show clocks**
**Show launchers**
### Only Bangle 2
**Direct launch** - launch on first touch.
**Swipe Exit** - Swipe left to exit.
**Time Out** - Return to clock after a short while.
**Interaction buzz**
**Remember Page** - Remember page when leaving and coming back to the launcher.

View File

@ -11,6 +11,7 @@
swipeExit: false, swipeExit: false,
timeOut: "Off", timeOut: "Off",
interactionBuzz: false, interactionBuzz: false,
rememberPage: false,
}, require('Storage').readJSON("dtlaunch.json", true) || {}); }, require('Storage').readJSON("dtlaunch.json", true) || {});
let s = require("Storage"); let s = require("Storage");
@ -33,7 +34,17 @@
s.writeJSON("launch.cache.json", launchCache); s.writeJSON("launch.cache.json", launchCache);
} }
let apps = launchCache.apps; let apps = launchCache.apps;
for (let i = 0; i < 4; i++) { // Initially only load icons for the current page. let page = 0;
let initPageAppZeroth = 0;
let initPageAppLast = 3;
if (settings.rememberPage) {
page = (global.dtlaunch&&global.dtlaunch.handlePagePersist()) ??
(parseInt(s.read("dtlaunch.page")) ?? 0);
initPageAppZeroth = page*4;
initPageAppLast = Math.min(page*4+3, apps.length-1);
}
for (let i = initPageAppZeroth; i <= initPageAppLast; i++) { // Initially only load icons for the current page.
if (apps[i].icon) if (apps[i].icon)
apps[i].icon = s.read(apps[i].icon); // should just be a link to a memory area apps[i].icon = s.read(apps[i].icon); // should just be a link to a memory area
} }
@ -43,12 +54,11 @@
let maxPage = Npages-1; let maxPage = Npages-1;
let selected = -1; let selected = -1;
//let oldselected = -1; //let oldselected = -1;
let page = 0;
const XOFF = 24; const XOFF = 24;
const YOFF = 30; const YOFF = 30;
let drawIcon= function(p,n,selected) { let drawIcon= function(p,n,selected) {
let x = (n%2)*72+XOFF; let x = (n%2)*72+XOFF;
let y = n>1?72+YOFF:YOFF; 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); (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.clearRect(x+12,y+4,x+59,y+51);
@ -99,13 +109,32 @@
Bangle.drawWidgets(); // To immediately update widget field to follow current theme - remove leftovers if previous app set custom theme. Bangle.drawWidgets(); // To immediately update widget field to follow current theme - remove leftovers if previous app set custom theme.
Bangle.loadWidgets(); Bangle.loadWidgets();
drawPage(0); drawPage(page);
for (let i = 4; i < apps.length; i++) { // Load the rest of the app icons that were not initially. for (let i = 0; i < apps.length; i++) { // Load the rest of the app icons that were not initially.
if (i >= initPageAppZeroth && i <= initPageAppLast) continue;
if (apps[i].icon) if (apps[i].icon)
apps[i].icon = s.read(apps[i].icon); // should just be a link to a memory area apps[i].icon = s.read(apps[i].icon); // should just be a link to a memory area
} }
if (!global.dtlaunch) {
global.dtlaunch = {};
global.dtlaunch.handlePagePersist = function(page) {
// Function for persisting the active page when leaving dtlaunch.
if (page===undefined) {return this.page||0;}
if (!this.killHandler) { // Only register kill listener once.
this.killHandler = () => {
s.write("dtlaunch.page", this.page.toString());
};
E.on("kill", this.killHandler); // This is intentionally left around after fastloading into other apps. I.e. not removed in uiRemove.
}
this.page = page;
};
global.dtlaunch.handlePagePersist(page);
}
let swipeListenerDt = function(dirLeftRight, dirUpDown){ let swipeListenerDt = function(dirLeftRight, dirUpDown){
updateTimeoutToClock(); updateTimeoutToClock();
selected = -1; selected = -1;
@ -142,6 +171,7 @@
drawIcon(page,selected,false); drawIcon(page,selected,false);
} else { } else {
buzzLong(); buzzLong();
global.dtlaunch.handlePagePersist(page);
load(apps[page*4+i].src); load(apps[page*4+i].src);
} }
} }
@ -162,7 +192,10 @@
back : Bangle.showClock, back : Bangle.showClock,
swipe : swipeListenerDt, swipe : swipeListenerDt,
touch : touchListenerDt, touch : touchListenerDt,
remove : ()=>{if (timeoutToClock) clearTimeout(timeoutToClock);} remove : ()=>{
if (timeoutToClock) {clearTimeout(timeoutToClock);}
global.dtlaunch.handlePagePersist(page);
}
}); });
// taken from Icon Launcher with minor alterations // taken from Icon Launcher with minor alterations
@ -171,10 +204,9 @@
if (settings.timeOut!="Off"){ if (settings.timeOut!="Off"){
let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt let time=parseInt(settings.timeOut); //the "s" will be trimmed by the parseInt
if (timeoutToClock) clearTimeout(timeoutToClock); if (timeoutToClock) clearTimeout(timeoutToClock);
timeoutToClock = setTimeout(Bangle.showClock,time*1000); timeoutToClock = setTimeout(Bangle.showClock,time*1000);
} }
}; };
updateTimeoutToClock(); updateTimeoutToClock();
} // end of app scope } // end of app scope

View File

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

View File

@ -8,6 +8,7 @@
swipeExit: false, swipeExit: false,
timeOut: "Off", timeOut: "Off",
interactionBuzz: false, interactionBuzz: false,
rememberPage: false,
}, require('Storage').readJSON(FILE, true) || {}); }, require('Storage').readJSON(FILE, true) || {});
function writeSettings() { function writeSettings() {
@ -64,5 +65,12 @@
writeSettings(); writeSettings();
} }
}, },
/*LANG*/'Remember Page': {
value: settings.rememberPage,
onchange: v => {
settings.rememberPage = v;
writeSettings();
}
},
}); });
}) })

File diff suppressed because it is too large Load Diff

View File

@ -16,8 +16,9 @@
The DFU (bootloader) rarely changes, so it does not have to be the same version as your main firmware.</p> The DFU (bootloader) rarely changes, so it does not have to be the same version as your main firmware.</p>
</ul> </ul>
<div id="fw-ok" style="display:none"> <div id="fw-ok" style="display:none">
<p id="fw-old-bootloader-msg">If you have an early (KickStarter or developer) Bangle.js device and still have the old 2v10.x DFU, the Firmware Update <p id="fw-old-bootloader-msg">If you have an early (KickStarter or developer) Bangle.js device and still have the old 2v10.x DFU, the Firmware Update
will fail with a message about the DFU version. If so, please <a href="bootloader_espruino_2v20_banglejs2.hex" class="fw-link">click here to update to DFU 2v20</a> and then click the 'Upload' button that appears.</p> will fail with a message about the DFU version. If so, please <a href="bootloader_espruino_2v25_banglejs2.hex" class="fw-link">click here to update to DFU 2v25</a> and then click the 'Upload' button that appears.</p>
<p><button id="upload" class="btn btn-primary" style="display:none">Upload</button></p>
<div id="latest-firmware" style="display:none"> <div id="latest-firmware" style="display:none">
<p>The currently available Espruino firmware releases are:</p> <p>The currently available Espruino firmware releases are:</p>
<ul id="latest-firmware-list"> <ul id="latest-firmware-list">
@ -25,6 +26,7 @@
<p>To update, click a link above and then click the 'Upload' button that appears.</p> <p>To update, click a link above and then click the 'Upload' button that appears.</p>
</div> </div>
<p><a href="#" id="info-btn">What is DFU? ▼</a></p> <p><a href="#" id="info-btn">What is DFU? ▼</a></p>
<div id="info-div" style="display:none"> <div id="info-div" style="display:none">
<p><b>What is DFU?</b></p> <p><b>What is DFU?</b></p>
@ -33,7 +35,7 @@
Bangle.js firmware. Normally you would update firmware via this Firmware Bangle.js firmware. Normally you would update firmware via this Firmware
Updater app, but if for some reason Bangle.js will not boot, you can Updater app, but if for some reason Bangle.js will not boot, you can
<a href="https://www.espruino.com/Bangle.js2#firmware-updates" target="_blank">always use DFU to do the update manually</a>. <a href="https://www.espruino.com/Bangle.js2#firmware-updates" target="_blank">always use DFU to do the update manually</a>.
On DFU 2v19 and earlier, iOS devices could have issues updating firmware - 2v20 fixes this.</p> On DFU 2v19 and earlier, iOS devices could have issues updating firmware - 2v20 at later fixes this.</p>
<p>DFU is itself a bootloader, but here we're calling it DFU to avoid confusion <p>DFU is itself a bootloader, but here we're calling it DFU to avoid confusion
with the Bootloader app in the app loader (which prepares Bangle.js for running apps).</p> with the Bootloader app in the app loader (which prepares Bangle.js for running apps).</p>
</div> </div>
@ -51,7 +53,7 @@
potentially overwrite your DFU with the wrong binary and brick your Bangle.</p> potentially overwrite your DFU with the wrong binary and brick your Bangle.</p>
<input class="form-input" type="file" id="fileLoader" accept=".hex,.app_hex,.zip"/><br> <input class="form-input" type="file" id="fileLoader" accept=".hex,.app_hex,.zip"/><br>
</div> </div>
<p><button id="upload" class="btn btn-primary" style="display:none">Upload</button></p>
</div> </div>
@ -106,7 +108,8 @@ function onInit(device) {
else if (crcs[0] == 3816337552) version = "2v21"; else if (crcs[0] == 3816337552) version = "2v21";
else if (crcs[0] == 3329616485) version = "2v22"; else if (crcs[0] == 3329616485) version = "2v22";
else if (crcs[0] == 1569433504) version = "2v23"; else if (crcs[0] == 1569433504) version = "2v23";
else if (crcs[0] == 680675961) version = "2v24"; else if (crcs[0] == 680675961) version = "2v24";
else if (crcs[0] == 4148062987 || crcs[0] == 3675049818) version = "2v25";
else { // for other versions all 7 pages are used, check those else { // for other versions all 7 pages are used, check those
var crc = crcs[1]; var crc = crcs[1];
if (crc==1339551013) { version = "2v10.219"; ok = false; } if (crc==1339551013) { version = "2v10.219"; ok = false; }
@ -127,7 +130,7 @@ function onInit(device) {
} }
document.getElementById("boot-version").innerHTML = version; document.getElementById("boot-version").innerHTML = version;
var versionNumber = parseFloat(version.replace(".","").replace("v",".")); var versionNumber = parseFloat(version.replace(".","").replace("v","."));
if (versionNumber>=2.20) if (versionNumber>=2.25)
document.getElementById("fw-old-bootloader-msg").style.display = "none"; document.getElementById("fw-old-bootloader-msg").style.display = "none";
}); });
} }
@ -385,6 +388,7 @@ function createJS_bootloader(binary, startAddress, endAddress) {
for (var i=startAddress;i<endAddress;i+=4096) for (var i=startAddress;i<endAddress;i+=4096)
hexJS += 'f.erasePage(0x'+i.toString(16)+');\n'; hexJS += 'f.erasePage(0x'+i.toString(16)+');\n';
hexJS += `f.write(_fw,${startAddress});\n`; hexJS += `f.write(_fw,${startAddress});\n`;
hexJS += 'E.showMessage("Update Complete.")\n';
hexJS += `})()\n`; hexJS += `})()\n`;
log("DFU ready for upload"); log("DFU ready for upload");
} }

View File

@ -1,6 +1,6 @@
## gpsnav - navigate to waypoints ## gpsnav - navigate to waypoints
The app is aimed at small boat navigation although it can also be used to mark the location of your car, bicycle etc and then get directions back to it. Please note that it would be foolish in the extreme to rely on this as your only boat navigation aid! The app is aimed at small boat navigation, although it can also be used to mark the location of your car, bicycle etc and then get directions back to it. Please note that it would be foolish in the extreme to rely on this as your only boat navigation aid!
The app displays direction of travel (course), speed, direction to waypoint (bearing) and distance to waypoint. The screen shot below is before the app has got a GPS fix. The app displays direction of travel (course), speed, direction to waypoint (bearing) and distance to waypoint. The screen shot below is before the app has got a GPS fix.
@ -13,11 +13,11 @@ The large digits are the course and speed. The top of the display is a linear co
![](waypoint_screen.jpg) ![](waypoint_screen.jpg)
The display shows that Stone Henge is 108.75Km from the location where I made the screenshot and the direction is 255 degrees - approximately west. The display shows that I am currently moving approximately north - albeit slowly!. The position of the blue circle indicates that I need to turn left to get on course to Stone Henge. When the circle and red triangle line up you are on course and course will equal bearing. The display shows that Stone Henge is 108.75km from the location where I made the screenshot and the direction is 255 degrees - approximately west. The display shows that I am currently moving approximately north - albeit slowly! The position of the blue circle indicates that I need to turn left to get on course to Stone Henge. When the circle and red triangle line up you are on course and course will equal bearing.
### Marking Waypoints ### Marking Waypoints
The app lets you mark your current location as follows. There are vacant slots in the waypoint file which can be allocated a location. In the distributed waypoint file these are labelled WP0 to WP4. Select one of these - WP2 is shown below. The app lets you mark your current location as follows. There are vacant slots in the waypoint file which can be allocated a location. In the distributed waypoint file, these are labelled WP0 to WP4. Select one of these - WP2 is shown below.
![](select_screen.jpg) ![](select_screen.jpg)
@ -25,7 +25,7 @@ Bearing and distance are both zero as WP1 has currently no GPS location associat
![](marked_screen.jpg) ![](marked_screen.jpg)
The app indicates that WP2 is now marked by adding the prefix @ to it's name. The distance should be small as shown in the screen shot as you have just marked your current location. The app indicates that WP2 is now marked by adding the prefix @ to it's name. The distance should be small as shown in the screen shot, as you have just marked your current location.
### Waypoint JSON file ### Waypoint JSON file
@ -60,6 +60,6 @@ You add and delete entries using the Web IDE to load and then save the file from
### Waypoint Editor ### Waypoint Editor
Clicking on the download icon of gpsnav in the app loader invokes the waypoint editor. The editor downloads and displays the current `waypoints.json` file. Clicking the `Edit` button beside an entry causes the entry to be deleted from the list and displayed in the edit boxes. It can be restored - by clicking the `Add waypoint` button. A new markable entry is created by using the `Add name` button. The edited `waypoints.json` file is uploaded to the Bangle by clicking the `Upload` button. Clicking on the download icon of gpsnav in the app loader invokes the waypoint editor. The editor downloads and displays the current `waypoints.json` file. Clicking the `Edit` button beside an entry causes the entry to be deleted from the list and displayed in the edit boxes. It can be restored - by clicking the `Add waypoint` button. A new markable entry is created by using the `Add name` button. The edited `waypoints.json` file is uploaded to the Bangle by clicking the `Upload` button.
*Please report bugs etc. by raising an issue [here](https://github.com/jeffmer/JeffsBangleAppsDev). * *Please report bugs etc. by raising an issue [here](https://github.com/jeffmer/JeffsBangleAppsDev). *

View File

@ -1,2 +1,3 @@
0.01: First version of GPS Setup app 0.01: First version of GPS Setup app
0.02: Created gppsetup module 0.02: Created gppsetup module
0.03: Added support for Bangle.js2

View File

@ -48,13 +48,17 @@ used. These settings will remain for all apps that use the GPS.
the interval the more time the GPS will spend sleeping in low the interval the more time the GPS will spend sleeping in low
power mode (7mA) between obtaining fixes (35mA). For walking in power mode (7mA) between obtaining fixes (35mA). For walking in
open country an update once every 60 seconds is adequate to put open country an update once every 60 seconds is adequate to put
you within a 6 digit grid refernce sqaure. you within a 6 digit grid refernce sqaure.
**Note:** For the Bangle.js2, the GPS module does not have a PSMOO mode, and thus this is emulated using on/off timeouts specified using the update and search options.
- update - the time between two position fix attempts. - update - the time between two position fix attempts.
- search - the time between two acquisition attempts if the receiver - search - the time between two acquisition attempts if the receiver
is unable to get a position fix. is unable to get a position fix.
- fix_req (Bangle.js2 only) - the number of fixes required before the GPS turns off until next search for GPS signal. default is 1.
## Module ## Module
A module is provided that'll allow you to set GPS configuration from your own A module is provided that'll allow you to set GPS configuration from your own

View File

@ -34,6 +34,7 @@ function loadSettings() {
settings = require("Storage").readJSON(SETTINGS_FILE,1)||{}; settings = require("Storage").readJSON(SETTINGS_FILE,1)||{};
settings.update = settings.update||120; settings.update = settings.update||120;
settings.search = settings.search||5; settings.search = settings.search||5;
settings.fix_req = settings.fix_req||1;
settings.power_mode = settings.power_mode||"SuperE"; settings.power_mode = settings.power_mode||"SuperE";
log_debug(settings); log_debug(settings);
} }
@ -85,6 +86,16 @@ function showMainMenu() {
settings.search = v; settings.search = v;
updateSettings(); updateSettings();
} }
},
'Fix Req (#)': {
value: settings.fix_req,
min: 1,
max: 100,
step: 1,
onchange: v => {
settings.fix_req = v;
updateSettings();
}
} }
}; };

View File

@ -1,5 +1,5 @@
const SETTINGS_FILE = "gpssetup.settings.json"; const SETTINGS_FILE = "gpssetup.settings.json";
const BANGLE_VER = process.env.HWVERSION; //BangleJS2 support
function log_debug(o) { function log_debug(o) {
//let timestamp = new Date().getTime(); //let timestamp = new Date().getTime();
//console.log(timestamp + " : " + o); //console.log(timestamp + " : " + o);
@ -106,49 +106,97 @@ function delay(ms) {
function setupSuperE() { function setupSuperE() {
log_debug("setupGPS() Super-E"); log_debug("setupGPS() Super-E");
return Promise.resolve().then(function() { switch(BANGLE_VER){
UBX_CFG_RESET(); case(1): {
return delay(100); return Promise.resolve().then(function() {
}).then(function() { UBX_CFG_RESET();
UBX_CFG_PMS(); return delay(100);
return delay(20); }).then(function() {
}).then(function() { UBX_CFG_PMS();
UBX_CFG_SAVE(); return delay(20);
return delay(20); }).then(function() {
}).then(function() { UBX_CFG_SAVE();
log_debug("Powering GPS Off"); return delay(20);
/* }).then(function() {
* must be part of the promise chain to ensure that log_debug("Powering GPS Off");
* setup does not return and powerOff before config functions /*
* have run * must be part of the promise chain to ensure that
*/ * setup does not return and powerOff before config functions
return delay(20); * have run
}); */
return delay(20);
});
}
case(2):{
//nothing more to do.
return;
}
}
} }
function setupPSMOO(settings) { function setupPSMOO(settings) {
log_debug("setupGPS() PSMOO"); log_debug("setupGPS() PSMOO");
return Promise.resolve().then(function() { switch(BANGLE_VER){
UBX_CFG_RESET(); case(1):{
return delay(100); return Promise.resolve().then(function() {
}).then(function() { UBX_CFG_RESET();
UBX_CFG_PM2(settings.update, settings.search); return delay(100);
return delay(20); }).then(function() {
}).then(function() { UBX_CFG_PM2(settings.update, settings.search);
UBX_CFG_RXM(); return delay(20);
return delay(20); }).then(function() {
}).then(function() { UBX_CFG_RXM();
UBX_CFG_SAVE(); return delay(20);
return delay(20); }).then(function() {
}).then(function() { UBX_CFG_SAVE();
log_debug("Powering GPS Off"); return delay(20);
/* }).then(function() {
* must be part of the promise chain to ensure that log_debug("Powering GPS Off");
* setup does not return and powerOff before config functions /*
* have run * must be part of the promise chain to ensure that
*/ * setup does not return and powerOff before config functions
return delay(20); * have run
}); */
return delay(20);
});
}
case(2): {
var gpsTimeout = null;
var gpsActive = false;
var fix = 0;
function cb(f){
if(parseInt(f.fix) === 1){
fix++;
if(fix >= settings.fix_req){
fix = 0;
turnOffGPS();
}
}
}
function turnOffGPS() {
if (!gpsActive) return;
gpsActive = false;
clearTimeout(gpsTimeout);
Bangle.setGPSPower(0,settings.appName);
Bangle.removeListener('GPS', cb); // cleaning it up
gpsTimeout = setTimeout(() => {
turnOnGPS();
}, settings.update * 1000);
}
function turnOnGPS(){
if (gpsActive) return;
if(!Bangle.isGPSOn()) Bangle.setGPSPower(1,settings.appName);
Bangle.on('GPS',cb);
gpsActive = true;
gpsTimeout = setTimeout(() => {
turnOffGPS();
}, settings.search * 1000);
}
turnOnGPS();
break;
}
}
} }
/** Set GPS power mode (assumes GPS on), returns a promise. /** Set GPS power mode (assumes GPS on), returns a promise.
@ -161,16 +209,21 @@ require("gpssetup").setPowerMode({power_mode:"SuperE"}) // <-- Super E mode
See the README for more information See the README for more information
*/ */
exports.setPowerMode = function(options) { exports.setPowerMode = function(options) {
settings = require("Storage").readJSON(SETTINGS_FILE,1)||{}; var settings = require("Storage").readJSON(SETTINGS_FILE,1)||{};
if (options) { if (options) {
if (options.update) settings.update = options.update; if (options.update) settings.update = options.update;
if (options.search) settings.search = options.search; if (options.search) settings.search = options.search;
if (options.fix_req) settings.fix_req = options.fix_req;
if (options.power_mode) settings.power_mode = options.power_mode; if (options.power_mode) settings.power_mode = options.power_mode;
if (options.appName) settings.appName = options.appName;
} }
settings.update = settings.update||120; settings.update = settings.update||120;
settings.search = settings.search||5; settings.search = settings.search||5;
settings.fix_req = settings.fix_req||1; //default to just one fix and will turn off
settings.power_mode = settings.power_mode||"SuperE"; settings.power_mode = settings.power_mode||"SuperE";
settings.appName = settings.appName || "gpssetup";
if (options) require("Storage").write(SETTINGS_FILE, settings); if (options) require("Storage").write(SETTINGS_FILE, settings);
if(!Bangle.isGPSOn()) Bangle.setGPSPower(1,settings.appName); //always know its on - no point calling this otherwise!!!
if (settings.power_mode === "PSMOO") { if (settings.power_mode === "PSMOO") {
return setupPSMOO(settings); return setupPSMOO(settings);
} else { } else {

View File

@ -2,11 +2,11 @@
"id": "gpssetup", "id": "gpssetup",
"name": "GPS Setup", "name": "GPS Setup",
"shortName": "GPS Setup", "shortName": "GPS Setup",
"version": "0.02", "version": "0.03",
"description": "Configure the GPS power options and store them in the GPS nvram", "description": "Configure the GPS power options and store them in the GPS nvram",
"icon": "gpssetup.png", "icon": "gpssetup.png",
"tags": "gps,tools,outdoors", "tags": "gps,tools,outdoors",
"supports": ["BANGLEJS"], "supports": ["BANGLEJS","BANGLEJS2"],
"readme": "README.md", "readme": "README.md",
"storage": [ "storage": [
{"name":"gpssetup","url":"gpssetup.js"}, {"name":"gpssetup","url":"gpssetup.js"},

View File

@ -1,3 +1,7 @@
0.01: New app! 0.01: New app!
0.02: Add sensor icons 0.02: Add sensor icons
Customize code directly, remove config file Customize code directly, remove config file
0.03: Add HRM sensor
Add step count sensor
Add pressure and temperature sensors
Document Home Assistant `unique ID` workaround

View File

@ -21,4 +21,30 @@ You need to fill out these fields:
Currently creates these sensors: Currently creates these sensors:
* `<sensor id>_battery_level`: Your watch battery level as percentage * `<sensor id>_battery_level`: Your watch battery level as percentage
* `<sensor id>_battery_state`: `charging` or `discharging` * `<sensor id>_battery_state`: `charging` or `discharging`
* `<sensor id>_hrm`: Heart rate (only if measured: this app doesn't enable/disable the sensor)
* `<sensor id>_steps`: Step Count
* `<sensor id>_pressure`: Pressure
* `<sensor id>_temperature`: Temperature
## Home Assistant `unique ID` workaround
If you try to customize the created entities, Home Assistant will complain that
> This entity ('sensor.…') does not have a unique ID, therefore its settings
> cannot be managed from the UI.
The problem is that these sensors are created "dynamically", and there is no way
to supply a `unique ID`.
There is a workaround though:
1. Make note of the sensor name you want to customize (e.g. `banglejs_battery_state`).
2. Disconnect your Bangle.js from your phone, so it doesn't send updates.
3. Restart Home Assistant, the sensor is now gone.
4. <a href="https://my.home-assistant.io/redirect/config_flow_start?domain=template" target="_blank">Create a template sensor</a>: choose "Template a sensor".
- Use the name from step 1 (without `sensor.` prefix).
- Set the state template to `unknown`.
5. Reconnect your Bangle.js: it will now update the new template sensor, which
*does* have a `unique ID`.
**Warning:** Do not customize the `Entity ID`: the app sends values by sensor
ID, so you end up with both a non-updating template sensor and "dynamic" sensor
without `unique ID`.

View File

@ -1,6 +1,8 @@
(function () { (function () {
const sb = () => require("hasensors").sendBattery(); const su = () => require("hasensors").sendUpdate();
Bangle.on("charging", sb); Bangle.on("charging", su);
NRF.on("connect", () => setTimeout(sb, 2000)); NRF.on("connect", () => setTimeout(su, 2000));
setInterval(sb, 10 * 60 * 1000); su();
})(); setInterval(su, 10 * 60 * 1000);
Bangle.on('HRM', h=>require("hasensors").sendHRM(h));
})();

View File

@ -12,9 +12,8 @@ function post(sensor, data) {
}); });
} }
exports.sendBattery = function () { function sendBattery() {
if (!NRF.getSecurityStatus().connected) return; const b = E.getBattery(),
const b = E.getBattery(),
c = Bangle.isCharging(); c = Bangle.isCharging();
let i = "mdi:battery"; let i = "mdi:battery";
if (c) i += "-charging"; if (c) i += "-charging";
@ -40,4 +39,75 @@ exports.sendBattery = function () {
icon: i, icon: i,
} }
}); });
} }
function sendSteps() {
post("steps", {
state: Bangle.getStepCount(),
attributes: {
friendly_name: "{name} Step Count",
unit_of_measurement: "steps",
state_class: "total",
icon: "mdi:shoe-print",
}
});
}
/**
* Sends pressure *and temperature*
*/
function sendPressure() {
if (!Bangle.getPressure) return; // not a Bangle 2
const promise = Bangle.getPressure();
if (!promise) return; // emulator?
promise.then(values=>{
post("pressure", {
state: Math.round(values.pressure*10)/10,
attributes: {
friendly_name: "{name} Pressure",
unit_of_measurement: "hPa",
device_class: "atmospheric pressure",
state_class: "measurement",
icon: "mdi:gauge",
}
});
post("temperature", {
state: Math.round(values.temperature*10)/10,
attributes: {
friendly_name: "{name} Temperature",
unit_of_measurement: "°C",
device_class: "temperature",
state_class: "measurement",
icon: "mdi:thermometer",
}
});
});
}
exports.sendUpdate = function() {
if (!NRF.getSecurityStatus().connected) return;
sendBattery();
sendSteps();
sendPressure();
}
let hrm_last = 0;
const HRM_INTERVAL = 10*60*1000;
exports.sendHRM = function (hrm) {
if (!NRF.getSecurityStatus().connected) return;
const now = (new Date).getTime();
if (hrm_last > now-HRM_INTERVAL) return;
post("hrm", {
state: hrm.bpm,
attributes: {
confidence: hrm.confidence,
raw: hrm.raw,
friendly_name: "{name} Heart Rate",
icon: "mdi:heart",
unit_of_measurement: "bpm",
state_class: "measurement",
}
});
hrm_last = now;
};

View File

@ -2,7 +2,7 @@
"id": "hasensors", "id": "hasensors",
"name": "Home Assistant Sensors", "name": "Home Assistant Sensors",
"shortName": "HA sensors", "shortName": "HA sensors",
"version": "0.02", "version": "0.03",
"description": "Send sensor values to Home Assistant using Android Integration/Gadgetbridge", "description": "Send sensor values to Home Assistant using Android Integration/Gadgetbridge",
"icon": "ha.png", "icon": "ha.png",
"type": "bootloader", "type": "bootloader",

View File

@ -1,2 +1,3 @@
0.01: New App! 0.01: New App!
0.02: Accents and extended mode characters 0.02: Accents and extended mode characters
0.03: Bugfix - draw initial text after the back button has been removed

View File

@ -352,11 +352,6 @@ exports.input = function(options) {
} }
}; };
// Draw initial string
require("widget_utils").hide();
g.setBgColor(g.theme.bg);
wrapText();
draw();
return new Promise((resolve,reject) => { return new Promise((resolve,reject) => {
Bangle.setUI({ Bangle.setUI({
@ -385,6 +380,13 @@ exports.input = function(options) {
} }
} }
}); });
// Draw initial string
require("widget_utils").hide();
g.setBgColor(g.theme.bg);
wrapText();
draw();
}); });

View File

@ -1,6 +1,6 @@
{ "id": "kbedgewrite", { "id": "kbedgewrite",
"name": "EdgeWrite keyboard", "name": "EdgeWrite keyboard",
"version":"0.02", "version":"0.03",
"description": "A library for text input via EdgeWrite swipe gestures", "description": "A library for text input via EdgeWrite swipe gestures",
"icon": "app.png", "icon": "app.png",
"type":"textinput", "type":"textinput",

View File

@ -111,3 +111,5 @@
Make sure play button image is transparent Make sure play button image is transparent
Add top-right menu to music playback to allow message to be deleted Add top-right menu to music playback to allow message to be deleted
0.82: Stop buzzing when a message is removed (e.g. call answered) 0.82: Stop buzzing when a message is removed (e.g. call answered)
0.83: Add option to not open the first unread message
0.84: Fix: Assign show message entry to the settings menu and not the message itself.

View File

@ -155,7 +155,7 @@ function showMapMessage(msg) {
function back() { // mark as not new and return to menu function back() { // mark as not new and return to menu
msg.new = false; msg.new = false;
layout = undefined; layout = undefined;
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:0}); checkMessages({clockIfNoMsg:1,clockIfAllRead:1,ignoreUnread:settings.ignoreUnread,openMusic:0});
} }
Bangle.setUI({mode:"updown", back: back}, back); // any input takes us back Bangle.setUI({mode:"updown", back: back}, back); // any input takes us back
} }
@ -195,8 +195,8 @@ function showMusicMessage(msg) {
var wasNew = msg.new; var wasNew = msg.new;
msg.new = false; msg.new = false;
layout = undefined; layout = undefined;
if (wasNew) checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:0,openMusic:0}); if (wasNew) checkMessages({clockIfNoMsg:1,clockIfAllRead:1,ignoreUnread:1,openMusic:0});
else checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); else returnToMain();
} }
function updateLabels() { function updateLabels() {
trackName = reduceStringAndPad(msg.track, trackScrollOffset, 13); trackName = reduceStringAndPad(msg.track, trackScrollOffset, 13);
@ -283,7 +283,7 @@ function showMessageSettings(msg) {
}; };
if (msg.id!="music") if (msg.id!="music")
msg[/*LANG*/"View Message"] = () => showMessageScroller(msg); menu[/*LANG*/"View Message"] = () => showMessageScroller(msg);
if (msg.reply && reply) { if (msg.reply && reply) {
menu[/*LANG*/"Reply"] = () => { menu[/*LANG*/"Reply"] = () => {
@ -304,7 +304,7 @@ function showMessageSettings(msg) {
menu = Object.assign(menu, { menu = Object.assign(menu, {
/*LANG*/"Delete" : () => { /*LANG*/"Delete" : () => {
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id); MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); returnToMain();
}, },
}); });
@ -315,25 +315,25 @@ function showMessageSettings(msg) {
Bangle.messageIgnore(msg); Bangle.messageIgnore(msg);
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id); MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
} }
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); returnToMain();
}); });
}; };
menu = Object.assign(menu, { menu = Object.assign(menu, {
/*LANG*/"Mark Unread" : () => { /*LANG*/"Mark Unread" : () => {
msg.new = true; msg.new = true;
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); returnToMain();
}, },
/*LANG*/"Mark all read" : () => { /*LANG*/"Mark all read" : () => {
MESSAGES.forEach(msg => msg.new = false); MESSAGES.forEach(msg => msg.new = false);
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); returnToMain();
}, },
/*LANG*/"Delete all messages" : () => { /*LANG*/"Delete all messages" : () => {
E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Delete All Messages"}).then(isYes => { E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Delete All Messages"}).then(isYes => {
if (isYes) { if (isYes) {
MESSAGES = []; MESSAGES = [];
} }
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0}); returnToMain();
}); });
}, },
}); });
@ -349,7 +349,7 @@ function showMessage(msgid, persist) {
clearInterval(updateLabelsInterval); clearInterval(updateLabelsInterval);
updateLabelsInterval=undefined; updateLabelsInterval=undefined;
} }
if (!msg) return checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic}); // go home if no message found if (!msg) return returnToClockIfEmpty(); // go home if no message found
if (msg.id=="music") { if (msg.id=="music") {
cancelReloadTimeout(); // don't auto-reload to clock now cancelReloadTimeout(); // don't auto-reload to clock now
return showMusicMessage(msg); return showMusicMessage(msg);
@ -399,7 +399,7 @@ function showMessage(msgid, persist) {
layout = undefined; layout = undefined;
msg.new = false; // read mail msg.new = false; // read mail
cancelReloadTimeout(); // don't auto-reload to clock now cancelReloadTimeout(); // don't auto-reload to clock now
checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic}); returnToClockIfEmpty();
} }
var negHandler,posHandler,footer = [ ]; var negHandler,posHandler,footer = [ ];
if (msg.negative) { if (msg.negative) {
@ -407,7 +407,7 @@ function showMessage(msgid, persist) {
msg.new = false; msg.new = false;
cancelReloadTimeout(); // don't auto-reload to clock now cancelReloadTimeout(); // don't auto-reload to clock now
Bangle.messageResponse(msg,false); Bangle.messageResponse(msg,false);
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic}); returnToCheckMessages();
}; footer.push({type:"img",src:atob("PhAB4A8AAAAAAAPAfAMAAAAAD4PwHAAAAAA/H4DwAAAAAH78B8AAAAAA/+A/AAAAAAH/Af//////w/gP//////8P4D///////H/Af//////z/4D8AAAAAB+/AfAAAAAA/H4DwAAAAAPg/AcAAAAADwHwDAAAAAA4A8AAAAAAAA=="),col:"#f00",cb:negHandler}); }; footer.push({type:"img",src:atob("PhAB4A8AAAAAAAPAfAMAAAAAD4PwHAAAAAA/H4DwAAAAAH78B8AAAAAA/+A/AAAAAAH/Af//////w/gP//////8P4D///////H/Af//////z/4D8AAAAAB+/AfAAAAAA/H4DwAAAAAPg/AcAAAAADwHwDAAAAAA4A8AAAAAAAA=="),col:"#f00",cb:negHandler});
} }
footer.push({fillx:1}); // push images to left/right footer.push({fillx:1}); // push images to left/right
@ -421,7 +421,7 @@ function showMessage(msgid, persist) {
Bluetooth.println(JSON.stringify(result)); Bluetooth.println(JSON.stringify(result));
replying = false; replying = false;
layout.render(); layout.render();
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic}); returnToCheckMessages();
}) })
.catch(() => { .catch(() => {
replying = false; replying = false;
@ -435,7 +435,7 @@ function showMessage(msgid, persist) {
msg.new = false; msg.new = false;
cancelReloadTimeout(); // don't auto-reload to clock now cancelReloadTimeout(); // don't auto-reload to clock now
Bangle.messageResponse(msg,true); Bangle.messageResponse(msg,true);
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic}); returnToCheckMessages();
}; footer.push({type:"img",src:atob("QRABAAAAAAAAAAOAAAAABgAAA8AAAAADgAAD4AAAAAHgAAPgAAAAAPgAA+AAAAAAfgAD4///////gAPh///////gA+D///////AD4H//////8cPgAAAAAAPw8+AAAAAAAfB/4AAAAAAA8B/gAAAAAABwB+AAAAAAADAB4AAAAAAAAABgAA=="),col:"#0f0",cb:posHandler}); }; footer.push({type:"img",src:atob("QRABAAAAAAAAAAOAAAAABgAAA8AAAAADgAAD4AAAAAHgAAPgAAAAAPgAA+AAAAAAfgAD4///////gAPh///////gA+D///////AD4H//////8cPgAAAAAAPw8+AAAAAAAfB/4AAAAAAA8B/gAAAAAABwB+AAAAAAADAB4AAAAAAAAABgAA=="),col:"#0f0",cb:posHandler});
} }
@ -447,7 +447,7 @@ function showMessage(msgid, persist) {
]}, ]},
{ type:"btn", { type:"btn",
src:require("messageicons").getImage(msg), src:require("messageicons").getImage(msg),
col:require("messageicons").getColor(msg, {settings:settings, default:g.theme.fg2}), col:require("messageicons").getColor(msg, {settings, default:g.theme.fg2}),
pad: 3, cb:()=>{ pad: 3, cb:()=>{
cancelReloadTimeout(); // don't auto-reload to clock now cancelReloadTimeout(); // don't auto-reload to clock now
showMessageSettings(msg); showMessageSettings(msg);
@ -476,7 +476,7 @@ function showMessage(msgid, persist) {
/* options = { /* options = {
clockIfNoMsg : bool clockIfNoMsg : bool
clockIfAllRead : bool clockIfAllRead : bool
showMsgIfUnread : bool ignoreUnread : bool // don't automatically navigate to the first unread message
openMusic : bool // open music if it's playing openMusic : bool // open music if it's playing
dontStopBuzz : bool // don't stuf buzzing (any time other than the first this is undefined/false) dontStopBuzz : bool // don't stuf buzzing (any time other than the first this is undefined/false)
} }
@ -500,7 +500,7 @@ function checkMessages(options) {
// we have >0 messages // we have >0 messages
var newMessages = MESSAGES.filter(m=>m.new&&m.id!="music"); var newMessages = MESSAGES.filter(m=>m.new&&m.id!="music");
// If we have a new message, show it // If we have a new message, show it
if (options.showMsgIfUnread && newMessages.length) { if (!options.ignoreUnread && newMessages.length) {
delete newMessages[0].show; // stop us getting stuck here if we're called a second time delete newMessages[0].show; // stop us getting stuck here if we're called a second time
showMessage(newMessages[0].id, false); showMessage(newMessages[0].id, false);
// buzz after showMessage, so being busy during layout doesn't affect the buzz pattern // buzz after showMessage, so being busy during layout doesn't affect the buzz pattern
@ -538,7 +538,7 @@ function checkMessages(options) {
} }
if (img) { if (img) {
var fg = g.getColor(), var fg = g.getColor(),
col = require("messageicons").getColor(msg, {settings:settings, default:fg}); col = require("messageicons").getColor(msg, {settings, default:fg});
g.setColor(col).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering g.setColor(col).drawImage(img, x+24, r.y+24, {rotate:0}) // force centering
.setColor(fg); // only color the icon .setColor(fg); // only color the icon
x += 50; x += 50;
@ -570,6 +570,17 @@ function checkMessages(options) {
}); });
} }
function returnToCheckMessages(clock) {
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,ignoreUnread:settings.ignoreUnread,openMusic});
}
function returnToMain() {
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,ignoreUnread:1,openMusic:0});
}
function returnToClockIfEmpty() {
checkMessages({clockIfNoMsg:1,clockIfAllRead:0,ignoreUnread:1,openMusic});
}
function cancelReloadTimeout() { function cancelReloadTimeout() {
if (!unreadTimeout) return; if (!unreadTimeout) return;
@ -594,7 +605,7 @@ setTimeout(() => {
// only openMusic on launch if music is new, or state=="show" (set by messagesmusic) // only openMusic on launch if music is new, or state=="show" (set by messagesmusic)
var musicMsg = MESSAGES.find(m => m.id === "music"); var musicMsg = MESSAGES.find(m => m.id === "music");
checkMessages({ checkMessages({
clockIfNoMsg: 0, clockIfAllRead: 0, showMsgIfUnread: 1, clockIfNoMsg: 0, clockIfAllRead: 0, ignoreUnread: settings.ignoreUnread,
openMusic: ((musicMsg&&musicMsg.new) && settings.openMusic) || (musicMsg&&musicMsg.state=="show"), openMusic: ((musicMsg&&musicMsg.new) && settings.openMusic) || (musicMsg&&musicMsg.state=="show"),
dontStopBuzz: 1 }); dontStopBuzz: 1 });
}, 10); // if checkMessages wants to 'load', do that }, 10); // if checkMessages wants to 'load', do that

View File

@ -2,7 +2,7 @@
"id": "messagegui", "id": "messagegui",
"name": "Message UI", "name": "Message UI",
"shortName": "Messages", "shortName": "Messages",
"version": "0.82", "version": "0.84",
"description": "Default app to display notifications from iOS and Gadgetbridge/Android", "description": "Default app to display notifications from iOS and Gadgetbridge/Android",
"icon": "app.png", "icon": "app.png",
"type": "app", "type": "app",

View File

@ -5,4 +5,7 @@
0.59: fixes message timeout by using setinterval, as it was intended. So the buzz is triggered every x seconds until the timeout occours. 0.59: fixes message timeout by using setinterval, as it was intended. So the buzz is triggered every x seconds until the timeout occours.
0.60: Bump version to allow new buzz.js module to be loaded - fixes memory/performance hog when buzz called 0.60: Bump version to allow new buzz.js module to be loaded - fixes memory/performance hog when buzz called
0.61: Add repeatCalls option to allow different repeat settings for messages vs calls 0.61: Add repeatCalls option to allow different repeat settings for messages vs calls
0.62: Add option for driving on left (affects roundabout icons in navigation) 0.62: Add option for driving on left (affects roundabout icons in navigation)
0.63: Add option to not open the first unread message
0.64: Only load from storage once in settings
0.65: Fix settings error introduced by two conflicting changes

View File

@ -1,7 +1,7 @@
{ {
"id": "messages", "id": "messages",
"name": "Messages", "name": "Messages",
"version": "0.62", "version": "0.65",
"description": "Library to handle, load and store message events received from Android/iOS", "description": "Library to handle, load and store message events received from Android/iOS",
"icon": "app.png", "icon": "app.png",
"type": "module", "type": "module",

View File

@ -1,7 +1,7 @@
(function(back) { (function(back) {
const iconColorModes = ['color', 'mono']; const iconColorModes = ['color', 'mono'];
function settings() { function loadSettings() {
let settings = require('Storage').readJSON("messages.settings.json", true) || {}; let settings = require('Storage').readJSON("messages.settings.json", true) || {};
if (settings.vibrate===undefined) settings.vibrate=":"; if (settings.vibrate===undefined) settings.vibrate=":";
if (settings.vibrateCalls===undefined) settings.vibrateCalls=":"; if (settings.vibrateCalls===undefined) settings.vibrateCalls=":";
@ -11,6 +11,7 @@
if (settings.unreadTimeout===undefined) settings.unreadTimeout=60; if (settings.unreadTimeout===undefined) settings.unreadTimeout=60;
if (settings.maxMessages===undefined) settings.maxMessages=3; if (settings.maxMessages===undefined) settings.maxMessages=3;
if (settings.iconColorMode === undefined) settings.iconColorMode = iconColorModes[0]; if (settings.iconColorMode === undefined) settings.iconColorMode = iconColorModes[0];
if (settings.ignoreUnread === undefined) settings.ignoreUnread = 0;
settings.unlockWatch=!!settings.unlockWatch; settings.unlockWatch=!!settings.unlockWatch;
settings.openMusic=!!settings.openMusic; settings.openMusic=!!settings.openMusic;
settings.maxUnreadTimeout=240; settings.maxUnreadTimeout=240;
@ -18,80 +19,84 @@
return settings; return settings;
} }
function updateSetting(setting, value) { function updateSetting(setting, value) {
let settings = require('Storage').readJSON("messages.settings.json", true) || {};
settings[setting] = value; settings[setting] = value;
require('Storage').writeJSON("messages.settings.json", settings); require('Storage').writeJSON("messages.settings.json", settings);
} }
var settings = loadSettings();
var mainmenu = { var mainmenu = {
"" : { "title" : /*LANG*/"Messages" }, "" : { "title" : /*LANG*/"Messages" },
"< Back" : back, "< Back" : back,
/*LANG*/'Vibrate': require("buzz_menu").pattern(settings().vibrate, v => updateSetting("vibrate", v)), /*LANG*/'Vibrate': require("buzz_menu").pattern(settings.vibrate, v => updateSetting("vibrate", v)),
/*LANG*/'Vibrate for calls': require("buzz_menu").pattern(settings().vibrateCalls, v => updateSetting("vibrateCalls", v)), /*LANG*/'Vibrate for calls': require("buzz_menu").pattern(settings.vibrateCalls, v => updateSetting("vibrateCalls", v)),
/*LANG*/'Repeat': { /*LANG*/'Repeat': {
value: settings().repeat, value: settings.repeat,
min: 0, max: 10, min: 0, max: 10,
format: v => v?v+"s":/*LANG*/"Off", format: v => v?v+"s":/*LANG*/"Off",
onchange: v => updateSetting("repeat", v) onchange: v => updateSetting("repeat", v)
}, },
/*LANG*/'Repeat for calls': { /*LANG*/'Repeat for calls': {
value: settings().repeatCalls, value: settings.repeatCalls,
min: 0, max: 10, min: 0, max: 10,
format: v => v?v+"s":/*LANG*/"Off", format: v => v?v+"s":/*LANG*/"Off",
onchange: v => updateSetting("repeatCalls", v) onchange: v => updateSetting("repeatCalls", v)
}, },
/*LANG*/'Vibrate timer': { /*LANG*/'Vibrate timer': {
value: settings().vibrateTimeout, value: settings.vibrateTimeout,
min: 0, max: settings().maxUnreadTimeout, step : 10, min: 0, max: settings.maxUnreadTimeout, step : 10,
format: v => v?v+"s":/*LANG*/"Off", format: v => v?v+"s":/*LANG*/"Off",
onchange: v => updateSetting("vibrateTimeout", v) onchange: v => updateSetting("vibrateTimeout", v)
}, },
/*LANG*/'Unread timer': { /*LANG*/'Unread timer': {
value: settings().unreadTimeout, value: settings.unreadTimeout,
min: 0, max: settings().maxUnreadTimeout, step : 10, min: 0, max: settings.maxUnreadTimeout, step : 10,
format: v => v?v+"s":/*LANG*/"Off", format: v => v?v+"s":/*LANG*/"Off",
onchange: v => updateSetting("unreadTimeout", v) onchange: v => updateSetting("unreadTimeout", v)
}, },
/*LANG*/'Min Font': { /*LANG*/'Min Font': {
value: 0|settings().fontSize, value: 0|settings.fontSize,
min: 0, max: 1, min: 0, max: 1,
format: v => [/*LANG*/"Small",/*LANG*/"Medium"][v], format: v => [/*LANG*/"Small",/*LANG*/"Medium"][v],
onchange: v => updateSetting("fontSize", v) onchange: v => updateSetting("fontSize", v)
}, },
/*LANG*/'Auto-Open Unread Msg': {
value: !!settings.ignoreUnread,
onchange: v => updateSetting("ignoreUnread", v)
},
/*LANG*/'Auto-Open Music': { /*LANG*/'Auto-Open Music': {
value: !!settings().openMusic, value: !!settings.openMusic,
onchange: v => updateSetting("openMusic", v) onchange: v => updateSetting("openMusic", v)
}, },
/*LANG*/'Unlock Watch': { /*LANG*/'Unlock Watch': {
value: !!settings().unlockWatch, value: !!settings.unlockWatch,
onchange: v => updateSetting("unlockWatch", v) onchange: v => updateSetting("unlockWatch", v)
}, },
/*LANG*/'Flash Icon': { /*LANG*/'Flash Icon': {
value: !!settings().flash, value: !!settings.flash,
onchange: v => updateSetting("flash", v) onchange: v => updateSetting("flash", v)
}, },
/*LANG*/'Quiet mode disables auto-open': { /*LANG*/'Quiet mode disables auto-open': {
value: !!settings().quietNoAutOpn, value: !!settings.quietNoAutOpn,
onchange: v => updateSetting("quietNoAutOpn", v) onchange: v => updateSetting("quietNoAutOpn", v)
}, },
/*LANG*/'Disable auto-open': { /*LANG*/'Disable auto-open': {
value: !!settings().noAutOpn, value: !!settings.noAutOpn,
onchange: v => updateSetting("noAutOpn", v) onchange: v => updateSetting("noAutOpn", v)
}, },
/*LANG*/'Widget messages': { /*LANG*/'Widget messages': {
value:0|settings().maxMessages, value:0|settings.maxMessages,
min: 0, max: 5, min: 0, max: 5,
format: v => v ? v :/*LANG*/"Hide", format: v => v ? v :/*LANG*/"Hide",
onchange: v => updateSetting("maxMessages", v) onchange: v => updateSetting("maxMessages", v)
}, },
/*LANG*/'Icon color mode': { /*LANG*/'Icon color mode': {
value: Math.max(0,iconColorModes.indexOf(settings().iconColorMode)), value: Math.max(0,iconColorModes.indexOf(settings.iconColorMode)),
min: 0, max: iconColorModes.length - 1, min: 0, max: iconColorModes.length - 1,
format: v => iconColorModes[v], format: v => iconColorModes[v],
onchange: v => updateSetting("iconColorMode", iconColorModes[v]) onchange: v => updateSetting("iconColorMode", iconColorModes[v])
}, },
/*LANG*/'Car driver pos': { // used by messagegui /*LANG*/'Car driver pos': { // used by messagegui
value:!!settings().carIsRHD, value:!!settings.carIsRHD,
format: v => v ? /*LANG*/"Right" :/*LANG*/"Left", format: v => v ? /*LANG*/"Right" :/*LANG*/"Left",
onchange: v => updateSetting("carIsRHD", v) onchange: v => updateSetting("carIsRHD", v)
}, },

View File

@ -2,6 +2,7 @@
<head> <head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.3/dist/leaflet.css" /> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.0.3/dist/leaflet.css" />
<link rel="stylesheet" href="../../css/spectre.min.css"> <link rel="stylesheet" href="../../css/spectre.min.css">
<link rel="stylesheet" href="../../css/spectre-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet-geosearch@3.6.0/dist/geosearch.css"/> <link rel="stylesheet" href="https://unpkg.com/leaflet-geosearch@3.6.0/dist/geosearch.css"/>
</head> </head>
<style> <style>
@ -34,7 +35,9 @@
</div> </div>
<div id="controls"> <div id="controls">
<span id="select-hint">Click the map to select a location</span> <span id="select-hint">Click the map to select a location</span>
<button id="select" class="btn btn-primary" style="display:none">Save</button><br/> <button id="locate-me" class="btn" title="Locate me">&#x26ef;</button>
<button id="locate-marker" class="btn" style="display:none" title="Locate marker"><i class="icon icon-location"></i></button>
<button id="select" class="btn btn-primary" style="display:none" title="Save to device">Save</button><br/>
</div> </div>
<script src="../../core/lib/interface.js"></script> <script src="../../core/lib/interface.js"></script>
@ -76,18 +79,36 @@
map.removeLayer(marker); map.removeLayer(marker);
} }
marker = new L.marker(latlon).addTo(map); marker = new L.marker(latlon).addTo(map);
document.getElementById("select-hint").style.display="none"; document.getElementById("select-hint").style.display="none";
document.getElementById("select").style.display=""; document.getElementById("select").style.display="";
document.getElementById("locate-marker").style.display="";
} }
map.on('click', function(e){ map.on('click', function(e){
setPosition(e.latlng); setPosition(e.latlng);
}); });
function convertMapToFile(map) {
return {lat: map.lat, lon: map.lng};
}
function convertFileToMap(file) {
return {lat: file.lat, lng: file.lon};
}
document.getElementById("locate-me").addEventListener("click", function() {
map.locate({setView: true, maxZoom: 16, enableHighAccuracy:true});
});
document.getElementById("locate-marker").addEventListener("click", function() {
if (latlon && latlon.lng != null && latlon.lat != null) {
map.setView(latlon);
}
});
document.getElementById("select").addEventListener("click", function() { document.getElementById("select").addEventListener("click", function() {
let settings = {}; // {"lat":48.8566,"lon":2.3522,"location":"Paris"} let settings = convertMapToFile(latlon); // {"lat":48.8566,"lon":2.3522,"location":"Paris"}
settings.lat = latlon.lat;
settings.lon = latlon.lng;
settings.location = "custom"; settings.location = "custom";
Util.showModal("Saving..."); Util.showModal("Saving...");
Util.writeStorage("mylocation.json", JSON.stringify(settings), ()=>{ Util.writeStorage("mylocation.json", JSON.stringify(settings), ()=>{
@ -101,7 +122,7 @@
Util.readStorageJSON("mylocation.json", function(data) { Util.readStorageJSON("mylocation.json", function(data) {
if (data===undefined) return; // no file if (data===undefined) return; // no file
try { try {
setPosition(data); setPosition(convertFileToMap(data));
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }

View File

@ -15,3 +15,4 @@
0.11: Skip double buffering, use 240x240 size 0.11: Skip double buffering, use 240x240 size
0.12: Fix swipe direction (#800) 0.12: Fix swipe direction (#800)
0.13: Bangle.js 2 support 0.13: Bangle.js 2 support
0.14: Brighter Christmas tree on Bangle.js 2

View File

@ -16,7 +16,7 @@
</div> </div>
<p><button id="try" class="btn">Try in Emulator</button></p> <p><button id="try" class="btn">Try in Emulator</button></p>
<p><button id="upload" class="btn btn-primary">Upload</button></p> <p><button id="upload" class="btn btn-primary">Upload</button></p>
<p>This is currently Christmas-themed, but more themes will be added in the future.</p> <p>To turn the Bangle off so the Welcome screen runs at startup go to <code>Settings -> Apps -> My Welcome</code> and choose <code>Turn off & run next</code></p>
<script src="../../core/lib/customize.js"></script> <script src="../../core/lib/customize.js"></script>
@ -76,7 +76,7 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/
// if (style=="Christmas") // if (style=="Christmas")
return `(function() { return `(function() {
var isnow = require("heatshrink").decompress(atob("jEagQWTgfAAocf+gFDh4FDiARBggVB3AFBl3Agf8jfkn/AgX/v/9/+Agfv/2//YrBgfwh4wCgfghYFJCIYdFFIw1EIIpNFL44FFOIoAP")); var isnow = require("heatshrink").decompress(atob("jEagQWTgfAAocf+gFDh4FDiARBggVB3AFBl3Agf8jfkn/AgX/v/9/+Agfv/2//YrBgfwh4wCgfghYFJCIYdFFIw1EIIpNFL44FFOIoAP"));
var itree = require("heatshrink").decompress(atob("mtWxH+ADHHDTI0aGuXH5vNGmhqvTYIzBGtoxF6fTG4g4oGgQyBAAZssGoI0Ga1g1FGdo01ZgIAEGmHHNoLSuAAN/rdb0YFBGlgCBGYIABA4YArGYY1CGn4znAAM6GeVd5PQ5Iyurc/vQ0oGZFAn+d4XC3d5GddiGYIEBy+7zoEBGlFhoEcsQ9GT08+oFk1mkGdaVBMgNArnJ6/KzswGs/J6GlrlbqtbvPC5PCy8wGohniMIPJvIpCqmX3e7vI0BqhqlMIY0DqhtBqoEBa0xgBMIIoEqoABGQwzfsIhBv4qHABM50vQGjg1CGaN66DoBGt1ioGd5LoBGjo1PGYNhvLoCa7wnBqgvGA4YzCAgN5GUAsCqoDBmAHCAYU/wPQ0oSDGcBiDqkwAYcxoFd5PX6GdGjrIIqtUAAc3jk5vPC4fCy5pef5I2BTQMcnAHBy+7y95T0oADnFk1ekBpI2aGRUin7NGAA9hsIzVsIgHTAKZBZoPJ5LNDGhBpXGolcwOsrtcA4TNB3bNDGb/+sVin9AoGe6HX5InEvN/TkP+5XQwM/sRsBzqWB4QuKGjvC6HQ4QdDvKWBZYMwmAuHmFUCYNbqibX3fD5O7qolEZQQ0FBwgKDqgJBGiphEDwNUEgJbBFIQqCAgYOCB4IzCnE6GyhYFGoQnDABYzGAAQ1UAAo2NBoQSBnOB0t/Gjo2EABIPCoGe6HX4QzTGRIAEqtVF4QEBBQc4oE4y/J5PCvIxeABk/oADBvO73eXTyAyZMwM/Awd5vIOFGslAr2Av4PLNcU/jmA6HX5I1KasFcn8dTIOd5PJ4SZGGiNhAAIyNn0ckU+ZYe7AAJpJEYJnNGZk+n9kw9cBAcwGoN5aZg1JJJQABm8/oEjoDKC5ALCrUwqh/NrvQ6HDGp04n9doEdoE/sQJBZQZhCqgABGZk6zw0K/1dnVAoNAFwOlCYL1FubJBy4GCGh1AnOX4XC3YzHFYOeCgdV5PQ5OdD4rKBqqYNGYlbv+X3edGY3CGgKMDAAO7JAJgDAClcr2BEYgADaIZ0DL4uXGbDuB6HX5I1GsP+sNhOgWXIhBmWd4Od5PK4TwFGIJoBAYI2BAD0/jlcQoO7AAJaEGQQADGr0/sjNEvOdAoZmDGgw2ZsVAkeAZpQACGZI2VsU/kVGn1bZoPJZogpGGhA4GfRYwBoGC1mlBQbNFFoo0JNxAGCEod/wM6oFAn9iv/J6/Kzo1Ey9/MZQAKCg4GCFgTDEvPCSwI0BC5I0RN4ocEYYPQ5OdHgeXSwTFKGaJyKFYPC3f+MIdbpzFLAD4zB/1OqtbqtOGgYArGAIADGl9UAAI0wGQN5GoQ0vvIABGoI0uGYQABqo0zNOg0uaQY0/GllOGn40//w=")); var itree = require("heatshrink").decompress(atob("mtWwcBkmSpICFnAIHARV2CKFJk1sEyNO7cSEyFt22EEx2d23bCgPYChsnCIIUBxI7OEyKJCEyMk9o7BO6A7CEx+TEwKzQndhwxiQuMpklxHaGGjBiQkoDBEx+So0YsOZEyNJ//JJp9hy/+KBs5suWpO5kmEEx/bjVJwJNMEwNN0uWrI7NrN3/8ZsuRMRmW7d7/+CrNkExdP/lhw+ekOWggmLz/8hMpk0IkDIMn15O4QCCxIUKsmZkGCEwVYWBYjDAQWDzgUJqxxBAwdZnP/HZOVywmEw96vDsLEYLIBrM30mQKB+XvNgTxWUyUJOgMJn/+pLvKJoUIsGDj/5wwmJ8mSpCeBhM3k+RkgmJnBNDycYpMGjBiJpwDBEwP8wVZkuWogUHkomCsnf/wFCR4LFKydttPkDYKhBsTFJr9140SoImLpMpm3n/tky0JZAVkEw+f4dZtPmd4YUBrAmHz/1y2SomGd4OQjMgEw+Tk+YEYUhy0ZsAFBMQ8mpMChEgwEJsECgDLBprvGpxKDBwIXBAoNg4zIHdgcIgEACgOCv9keIIUFdgYCEs//zA7FyYDCHAQCDt1/gJNFrAmIm/GyVhxLXFrIbEhAwBtMl0zIFktlEw0Z/IFDZAq/ByxXE73/8oyBso7EybjBEweHttp/4FBCgJ3EnNkdIQpBm3WrVJCIMly1EEwkYCIMYsnfrDsBzAICsOBEwVJsmSjMgyaYB61IEYNJsoPB//JCgNGcYPXtu1w2ePoMZQAef/xQCy3btv/8UJlMkrLFBeoImB3MkHYX+OgOf8LIHsObjVIEwOZ/sZkMkSYLsDQgf9PQLIBqV5DoNJEw9g4/8UINY8uUzLFGAQdN5fkVQORnIRBYogCEs3WiVBBwLsEAQYmCkuTvtl0nZd4TFBCIQmFjux/6hBd4Y7CFI0m7dT/LsB8uWGINWooRFunf/o4ByA4BkqJBkniKANkp1ZtrLBt/+rNpk1ZsR/EKANEAQMf+OWj15BAdESgopBktln+xHAKnBOIOUVJFJrVJh1ZsBEBOIIRIAQUwyGCgAOKCg1hgIROAQNEiECEx8k8OGgg7QfYMJEx+Td4J0NAQdJ0hiQp0YTwIUP4MmyBiQjFhwJiQgmaoA7QsOGiA7PpkStLaIoAA="));
var W=g.getWidth(),H=g.getHeight(); var W=g.getWidth(),H=g.getHeight();
var titleFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2"; var titleFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
var flakes = []; var flakes = [];
@ -112,6 +112,8 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/
setInterval(draw,50); setInterval(draw,50);
})(); })();
`; `;
// or an 8 bit tree, but 3 bit (above) renders better on Bangle.js 2
// var itree = require("heatshrink").decompress(atob("mtWxH+ADHHDTI0aGuXH5vNGmhqvTYIzBGtoxF6fTG4g4oGgQyBAAZssGoI0Ga1g1FGdo01ZgIAEGmHHNoLSuAAN/rdb0YFBGlgCBGYIABA4YArGYY1CGn4znAAM6GeVd5PQ5Iyurc/vQ0oGZFAn+d4XC3d5GddiGYIEBy+7zoEBGlFhoEcsQ9GT08+oFk1mkGdaVBMgNArnJ6/KzswGs/J6GlrlbqtbvPC5PCy8wGohniMIPJvIpCqmX3e7vI0BqhqlMIY0DqhtBqoEBa0xgBMIIoEqoABGQwzfsIhBv4qHABM50vQGjg1CGaN66DoBGt1ioGd5LoBGjo1PGYNhvLoCa7wnBqgvGA4YzCAgN5GUAsCqoDBmAHCAYU/wPQ0oSDGcBiDqkwAYcxoFd5PX6GdGjrIIqtUAAc3jk5vPC4fCy5pef5I2BTQMcnAHBy+7y95T0oADnFk1ekBpI2aGRUin7NGAA9hsIzVsIgHTAKZBZoPJ5LNDGhBpXGolcwOsrtcA4TNB3bNDGb/+sVin9AoGe6HX5InEvN/TkP+5XQwM/sRsBzqWB4QuKGjvC6HQ4QdDvKWBZYMwmAuHmFUCYNbqibX3fD5O7qolEZQQ0FBwgKDqgJBGiphEDwNUEgJbBFIQqCAgYOCB4IzCnE6GyhYFGoQnDABYzGAAQ1UAAo2NBoQSBnOB0t/Gjo2EABIPCoGe6HX4QzTGRIAEqtVF4QEBBQc4oE4y/J5PCvIxeABk/oADBvO73eXTyAyZMwM/Awd5vIOFGslAr2Av4PLNcU/jmA6HX5I1KasFcn8dTIOd5PJ4SZGGiNhAAIyNn0ckU+ZYe7AAJpJEYJnNGZk+n9kw9cBAcwGoN5aZg1JJJQABm8/oEjoDKC5ALCrUwqh/NrvQ6HDGp04n9doEdoE/sQJBZQZhCqgABGZk6zw0K/1dnVAoNAFwOlCYL1FubJBy4GCGh1AnOX4XC3YzHFYOeCgdV5PQ5OdD4rKBqqYNGYlbv+X3edGY3CGgKMDAAO7JAJgDAClcr2BEYgADaIZ0DL4uXGbDuB6HX5I1GsP+sNhOgWXIhBmWd4Od5PK4TwFGIJoBAYI2BAD0/jlcQoO7AAJaEGQQADGr0/sjNEvOdAoZmDGgw2ZsVAkeAZpQACGZI2VsU/kVGn1bZoPJZogpGGhA4GfRYwBoGC1mlBQbNFFoo0JNxAGCEod/wM6oFAn9iv/J6/Kzo1Ey9/MZQAKCg4GCFgTDEvPCSwI0BC5I0RN4ocEYYPQ5OdHgeXSwTFKGaJyKFYPC3f+MIdbpzFLAD4zB/1OqtbqtOGgYArGAIADGl9UAAI0wGQN5GoQ0vvIABGoI0uGYQABqo0zNOg0uaQY0/GllOGn40//w="))
} }
// when 'try' is clicked, load the emulator... // when 'try' is clicked, load the emulator...
document.getElementById("try").addEventListener("click", function() { document.getElementById("try").addEventListener("click", function() {

View File

@ -2,11 +2,12 @@
"id": "mywelcome", "id": "mywelcome",
"name": "Customised Welcome", "name": "Customised Welcome",
"shortName": "My Welcome", "shortName": "My Welcome",
"version": "0.13", "version": "0.14",
"description": "Appears at first boot and explains how to use Bangle.js. Like 'Welcome', but can be customised with a greeting", "description": "Appears at first boot and explains how to use Bangle.js. Like 'Welcome', but can be customised with a greeting for Christmas or Birthdays!",
"icon": "app.png", "icon": "app.png",
"tags": "start,welcome", "tags": "start,welcome,birthday,christmas,xmas",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"provides_features" : ["welcome"],
"custom": "custom.html", "custom": "custom.html",
"screenshots": [{"url":"bangle1-customized-welcome-screenshot.png"}], "screenshots": [{"url":"bangle1-customized-welcome-screenshot.png"}],
"storage": [ "storage": [

View File

@ -1,3 +1,4 @@
0.01: New App! 0.01: New App!
0.02: Keep advertising when connected 0.02: Keep advertising when connected
0.03: Use ble_advert module to work with other BLE advert apps 0.03: Use ble_advert module to work with other BLE advert apps
0.04: Fix installation after broken in 0.03 (fix #3667)

View File

@ -46,7 +46,7 @@ require("ble_advert").push(adv, {whenConnected: true, interval: 1000}); // adver
// send finished app // send finished app
sendCustomizedApp({ sendCustomizedApp({
storage:[ storage:[
{name:"openhaystack.boot.js", content:appJS}, {name:"openhaystack.boot.js", content:appJS, url:"openhaystack.boot.js"/* not a real URL but this lets the App Loader know it's a JS file which should be parsed */},
] ]
}); });
}); });

View File

@ -1,7 +1,7 @@
{ "id": "openhaystack", { "id": "openhaystack",
"name": "OpenHaystack (AirTag)", "name": "OpenHaystack (AirTag)",
"icon": "icon.png", "icon": "icon.png",
"version":"0.03", "version":"0.04",
"description": "Copy a base64 key from https://github.com/seemoo-lab/openhaystack and make your Bangle.js trackable as if it's an AirTag", "description": "Copy a base64 key from https://github.com/seemoo-lab/openhaystack and make your Bangle.js trackable as if it's an AirTag",
"tags": "openhaystack,bluetooth,ble,tracking,airtag", "tags": "openhaystack,bluetooth,ble,tracking,airtag",
"type": "bootloader", "type": "bootloader",
@ -9,6 +9,6 @@
"readme": "README.md", "readme": "README.md",
"supports": ["BANGLEJS","BANGLEJS2"], "supports": ["BANGLEJS","BANGLEJS2"],
"storage": [ "storage": [
{"name":"openhaystack.boot.js"} {"name":"openhaystack.boot.js" }
] ]
} }

View File

@ -36,3 +36,4 @@
0.29: Keep exit at bottom of menu 0.29: Keep exit at bottom of menu
Speed up latLonToXY for track rendering Speed up latLonToXY for track rendering
0.30: Minor code improvements 0.30: Minor code improvements
0.31: Reset draw colours before rendering (to allow black and white maps)

View File

@ -45,20 +45,22 @@
<div id="map"> <div id="map">
</div> </div>
<div id="controls"> <div id="controls">
<div style="display:inline-block;text-align:left;vertical-align: top;" id="3bitdiv"> <div class="form-group" style="display:inline-block;">
<input type="checkbox" id="3bit"></input><span>3 bit</span> <select class="form-select" id="mapStyle">
<br> <option value="3bit" selected>3 bit</option>
<input type="checkbox" id="preview"><span>Preview</span> <option value="8bit">8 bit</option>
<option value="1bit">1 bit</option>
</select>
</div> </div>
<div class="form-group" style="display:inline-block;"> <div class="form-group" style="display:inline-block;">
<select class="form-select" id="mapSize"> <select class="form-select" id="mapSize">
<option value="4">Small (4x4)</option> <option value="4">Small (4x4)</option>
<option value="5" selected>Medium (5x5)</option> <option value="5" selected>Medium (5x5)</option>
<option value="7">Large (7x7)</option> <option value="7">Large (7x7)</option>
<option value="10">XL (10x10)</option> <option value="10">XL (10x10)</option>
<option value="15">XXL (15x15)</option> <option value="15">XXL (15x15)</option>
</select> </select>
</div> </div>
<button id="getmap" class="btn btn-primary">Get Map</button><button class="btn" onclick="showLoadedMaps()">Map List</button><br/> <button id="getmap" class="btn btn-primary">Get Map</button><button class="btn" onclick="showLoadedMaps()">Map List</button><br/>
<canvas id="maptiles" style="display:none"></canvas> <canvas id="maptiles" style="display:none"></canvas>
<div id="uploadbuttons" style="display:none"><button id="upload" class="btn btn-primary">Upload</button> <div id="uploadbuttons" style="display:none"><button id="upload" class="btn btn-primary">Upload</button>
@ -94,20 +96,47 @@ TODO:
var TILELAYER = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; var TILELAYER = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
var PREVIEWTILELAYER = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png'; var PREVIEWTILELAYER = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';
MAPSTYLES = {
"3bit" : {
layer : 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: 'Map data &copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap</a> contributors</a>',
img : { compression:false, output:"raw", mode:"3bit",diffusion:"bayer2"}
}, "8bit" : {
layer : 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
attribution: 'Map data &copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap</a> contributors</a>',
img : { compression:false, output:"raw", mode:"web" }
}, "1bit" : {
layer : 'https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}{r}.png',
attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
img : { compression:false, output:"raw", mode:"1bit",inverted:true }
}
};
var loadedMaps = []; var loadedMaps = [];
// Tiles used for Bangle.js itself // Tiles used for Bangle.js itself
var bangleTileLayer = L.tileLayer(TILELAYER, { var bangleTileLayer;
maxZoom: 18,
attribution: 'Map data &copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap</a> contributors</a>'
});
// Tiles used for the may the user sees (faster) // Tiles used for the may the user sees (faster)
var previewTileLayer = L.tileLayer(PREVIEWTILELAYER, { var previewTileLayer;
maxZoom: 18, // Currently selected version of MAPSTYLES
attribution: 'Map data &copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap</a> contributors</a>' var currentStyle;
});
// Could optionally overlay trails: https://wiki.openstreetmap.org/wiki/Tiles // Could optionally overlay trails: https://wiki.openstreetmap.org/wiki/Tiles
function createMapLayers(style) {
currentStyle = style;
bangleTileLayer = L.tileLayer(style.layer, {
maxZoom: 18,
attribution: style.attribution
});
previewTileLayer = L.tileLayer(style.layer, {
maxZoom: 18,
attribution: style.attribution
});
}
createMapLayers(MAPSTYLES["3bit"]);
// Create map and try and set the location to where the browser thinks we are // Create map and try and set the location to where the browser thinks we are
var map = L.map('map').locate({setView: true, maxZoom: 16, enableHighAccuracy:true}); var map = L.map('map').locate({setView: true, maxZoom: 16, enableHighAccuracy:true});
previewTileLayer.addTo(map); previewTileLayer.addTo(map);
@ -130,10 +159,11 @@ TODO:
if (device && device.info && device.info.g) { if (device && device.info && device.info.g) {
// On 3 bit devices, 3 bit is the best way // On 3 bit devices, 3 bit is the best way
// still allow 8 bit as it makes zoom out much nicer // still allow 8 bit as it makes zoom out much nicer
if (device.info.g.bpp==3) { // ... but lets just default to 3 bit anyway now
/*if (device.info.g.bpp==3) {
document.getElementById("3bit").checked = true; document.getElementById("3bit").checked = true;
//document.getElementById("3bitdiv").style = "display:none"; //document.getElementById("3bitdiv").style = "display:none";
} }*/
} }
showLoadedMaps(); showLoadedMaps();
@ -257,17 +287,7 @@ TODO:
// convert canvas into an actual tiled image file // convert canvas into an actual tiled image file
function tilesLoaded(ctx, width, height, mapImageFile) { function tilesLoaded(ctx, width, height, mapImageFile) {
var options = { var options = currentStyle.img; // compression options
compression:false, output:"raw",
mode:"web"
};
if (document.getElementById("3bit").checked) {
options = {
compression:false, output:"raw",
mode:"3bit",
diffusion:"bayer2"
};
}
/* Go through all the data beforehand and /* Go through all the data beforehand and
turn the saturation up to maximum, so if thresholded to 3 bits it turn the saturation up to maximum, so if thresholded to 3 bits it
works a lot better */ works a lot better */
@ -289,9 +309,9 @@ TODO:
options.width = TILESIZE; options.width = TILESIZE;
options.height = TILESIZE; options.height = TILESIZE;
var imgstr = imageconverter.RGBAtoString(rgba, options); var imgstr = imageconverter.RGBAtoString(rgba, options);
if (document.getElementById("preview").checked) { /*if (document.getElementById("preview").checked) {
ctx.putImageData(imageData,x*TILESIZE, y*TILESIZE); // write preview ctx.putImageData(imageData,x*TILESIZE, y*TILESIZE); // write preview
} }*/
/*var compress = 'require("heatshrink").decompress(' /*var compress = 'require("heatshrink").decompress('
if (!imgstr.startsWith(compress)) throw "Data in wrong format"; if (!imgstr.startsWith(compress)) throw "Data in wrong format";
imgstr = imgstr.slice(compress.length,-1);*/ imgstr = imgstr.slice(compress.length,-1);*/
@ -408,6 +428,14 @@ TODO:
}); });
}); });
document.getElementById("mapStyle").addEventListener("click", function() {
var style = document.getElementById("mapStyle").value;
if (!style in MAPSTYLES) return;
map.removeLayer(previewTileLayer);
createMapLayers(MAPSTYLES[style]);
previewTileLayer.addTo(map);
});
document.getElementById("upload").addEventListener("click", function() { document.getElementById("upload").addEventListener("click", function() {
Util.showModal("Uploading..."); Util.showModal("Uploading...");
let promise = Promise.resolve(); let promise = Promise.resolve();

View File

@ -2,7 +2,7 @@
"id": "openstmap", "id": "openstmap",
"name": "OpenStreetMap", "name": "OpenStreetMap",
"shortName": "OpenStMap", "shortName": "OpenStMap",
"version": "0.30", "version": "0.31",
"description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps", "description": "Loads map tiles from OpenStreetMap onto your Bangle.js and displays a map of where you are. Once installed this also adds map functionality to `GPS Recorder` and `Recorder` apps",
"readme": "README.md", "readme": "README.md",
"icon": "app.png", "icon": "app.png",

View File

@ -73,6 +73,7 @@ exports.draw = function() {
} }
var mx = g.getWidth(); var mx = g.getWidth();
var my = g.getHeight(); var my = g.getHeight();
g.setColor(g.theme.fg).setBgColor(g.theme.bg); // reset draw colours
for (var x=ox,ttx=tx; x<mx && ttx<map.w; x+=s,ttx++) { for (var x=ox,ttx=tx; x<mx && ttx<map.w; x+=s,ttx++) {
for (var y=oy,tty=ty;y<my && tty<map.h;y+=s,tty++) { for (var y=oy,tty=ty;y<my && tty<map.h;y+=s,tty++) {
o.frame = ttx+(tty*map.w); o.frame = ttx+(tty*map.w);

View File

@ -1,3 +1,4 @@
0.01: attempt to import 0.01: attempt to import
0.02: Minor code improvements 0.02: Minor code improvements
0.03: Minor code improvements 0.03: Minor code improvements
0.10: Restart, start from "andark" adding astronomical features to it

View File

@ -5,21 +5,4 @@ Astronomical clock.
Written by: [Pavel Machek](https://github.com/pavelmachek) Written by: [Pavel Machek](https://github.com/pavelmachek)
The plan is to have an (analog) astronomical clock with a lot of The plan is to have an (analog) astronomical clock with a lot of
information on single dial. information on single dial. Thanks a lot to "Dark Analog Clock".
It continuously displays information that can be obtained "cheaply",
that is current time, sunset/sunrise times, battery status and
altitude. One-second updates with useful compass can be activated by
tapping bottom right corner.
Display is split in three rings. Outside ring is for time-based data
with base of one week, and for non time-based data. Black dot
indicates day of week. Green foot indicates number of steps taken, red
battery symbol indicates remaining charge, black thermometer symbol
represents temperature, and black ruler symbol indicates
altitude. Number in bottom left corner is day of month.
In the middle ring, hour-based data are displayed. Black dot indicates
current hour, yellow symbols indicate sunset and sunrise, and black
symbols indicate moonset and moonrise.

View File

@ -1,405 +1,277 @@
const SunCalc = require("suncalc"); // from modules folder /* sun version 0.0.3 */
let sun = {
SunCalc: null,
lat: 50,
lon: 14,
rise: 0, /* Unix time of sunrise/sunset */
set: 0,
init: function() {
try {
this.SunCalc = require("suncalc"); // from modules folder
} catch (e) {
print("Require error", e);
}
print("Have suncalc: ", this.SunCalc);
},
sunPos: function() {
let d = new Date();
if (!this.SunCalc) {
let sun = {};
sun.azimuth = 175;
sun.altitude = 15;
return sun;
}
let sun = this.SunCalc.getPosition(d, this.lat, this.lon);
print(sun.azimuth, sun.altitude);
return sun;
},
sunTime: function() {
let d = new Date();
if (!this.SunCalc) {
let sun = {};
sun.sunrise = d;
sun.sunset = d;
return sun;
}
let sun = this.SunCalc.getTimes(d, this.lat, this.lon);
return sun;
},
adj: function (x) {
if (x < 0)
return x + 24*60*60;
return x;
},
toSunrise: function () {
return this.adj(this.rise - getTime());
},
toSunset: function () {
return this.adj(this.set - getTime());
},
update: function () {
let t = this.sunTime();
this.rise = t.sunrise.getTime() / 1000;
this.set = t.sunset.getTime() / 1000;
},
// < 0 : next is sunrise, in abs(ret) seconds
// > 0
getNext: function () {
let rise = this.toSunrise();
let set = this.toSunset();
if (rise < set) {
return -rise;
}
return set;
// set = set / 60;
// return s + (set / 60).toFixed(0) + ":" + (set % 60).toFixed(0);
},
};
// ################################################################################ sun.init();
let ScreenWidth = g.getWidth(), CenterX = ScreenWidth/2; const defaultSettings = {
let ScreenHeight = g.getHeight(), CenterY = ScreenHeight/2; loadWidgets : false,
let outerRadius = Math.min(CenterX,CenterY) * 0.9; textAboveHands : false,
shortHrHand : true
};
const white = 0;
const settings = Object.assign(defaultSettings, require('Storage').readJSON('andark.json',1)||{});
const lat = 50.1; const c={"x":g.getWidth()/2,"y":g.getHeight()/2};
const lon = 14.45;
const h = g.getHeight(); const zahlpos=(function() {
const w = g.getWidth(); let z=[];
const sm = 15; let sk=1;
var altitude, temperature; for(let i=-10;i<50;i+=5){
let win=i*2*Math.PI/60;
let xsk =c.x+2+Math.cos(win)*(c.x-10),
ysk =c.y+2+Math.sin(win)*(c.x-10);
if(sk==3){xsk-=10;}
if(sk==6){ysk-=10;}
if(sk==9){xsk+=10;}
if(sk==12){ysk+=10;}
if(sk==10){xsk+=3;}
z.push([sk,xsk,ysk]);
sk+=1;
}
return z;
})();
var img_north = Graphics.createImage(` let unlock = false;
X
XXX
XXX
X XXX
X XXX
X XXXX
X XXXX
X XXXXX
X XXXXX
XXXXXXXXX
`);
var img_sunrise = Graphics.createImage(` function zeiger(len,dia,tim){
XXX const x=c.x+ Math.cos(tim)*len/2,
XXXXX y=c.y + Math.sin(tim)*len/2,
XXXXXXXXX d={"d":3,"x":dia/2*Math.cos(tim+Math.PI/2),"y":dia/2*Math.sin(tim+Math.PI/2)},
`); pol=[c.x-d.x,c.y-d.y,c.x+d.x,c.y+d.y,x+d.x,y+d.y,x-d.x,y-d.y];
return pol;
var img_moonrise = Graphics.createImage(`
XXX
XX X
XXXXXXXXX
`);
var img_altitude = Graphics.createImage(`
X X
X X X
XXXXXXXXX
X X X
X X
`);
var img_temperature = Graphics.createImage(`
XX
XXXXXXXX
X XX
XXXXXXXX
XX
`);
var img_battery = Graphics.createImage(`
XXXXXXXX
XXX X
XXXX XX
XXXXX X
XXXXXXXX
`);
var img_step = Graphics.createImage(`
XXX
XX XXXXX
XXX XXXXX
XXX XXXXX
XX XXXX
`);
var img_sun = Graphics.createImage(`
X X
XXX
XXXXXXX
XXXXXXXXX
XXXXXXXXX
XXXXXXXXX
XXXXXXX
XXX
X X
`);
var img_moon = Graphics.createImage(`
XXX
XX XXX
X XXXX
X XXX
X XXX
X XXX
X XXXX
X XXX
XXX
`);
let use_compass = 0;
function draw() {
drawBorders();
queueDraw();
} }
function radA(p) { return p*(Math.PI*2); } function drawHands(d) {
function radD(d) { return d*(h/2); } let m=d.getMinutes(), h=d.getHours(), s=d.getSeconds();
g.setColor(white,white,white);
function radX(p, d) { if(h>12){
let a = radA(p); h=h-12;
return h/2 + Math.sin(a)*radD(d); }
//calculates the position of the minute, second and hour hand
h=2*Math.PI/12*(h+m/60)-Math.PI/2;
//more accurate
//m=2*Math.PI/60*(m+s/60)-Math.PI/2;
m=2*Math.PI/60*(m)-Math.PI/2;
s=2*Math.PI/60*s-Math.PI/2;
//g.setColor(1,0,0);
const hz = zeiger(settings.shortHrHand?88:100,5,h);
g.fillPoly(hz,true);
//g.setColor(1,1,1);
const minz = zeiger(150,5,m);
g.fillPoly(minz,true);
if (unlock){
const sekz = zeiger(150,2,s);
g.fillPoly(sekz,true);
}
g.fillCircle(c.x,c.y,4);
} }
function radY(p, d) { function setColor() {
let a = radA(p); g.setBgColor(!white,!white,!white);
return w/2 - Math.cos(a)*radD(d); g.setColor(white,white,white);
} }
function fracHour(d) { function drawText(d) {
g.setFont("Vector",20);
//let dateStr = require("locale").date(d);
//g.drawString(dateStr, c.x, c.y+20, true);
let bat = E.getBattery();
let batStr = Math.round(bat/5)*5+"%";
if (Bangle.isCharging()) {
g.setBgColor(1,0,0);
}
if (bat < 30)
g.drawString(batStr, c.x, c.y+40, true);
}
function drawNumbers(d) {
let hour = d.getHours(); let hour = d.getHours();
let min = d.getMinutes(); if (d.getMinutes() > 30) {
hour = hour + min/60; hour += 1;
if (hour > 12)
hour -= 12;
return hour;
}
let HourHandLength = outerRadius * 0.5;
let HourHandWidth = 2*3, halfHourHandWidth = HourHandWidth/2;
let MinuteHandLength = outerRadius * 0.7;
let MinuteHandWidth = 2*2, halfMinuteHandWidth = MinuteHandWidth/2;
let SecondHandLength = outerRadius * 0.9;
let SecondHandOffset = 6;
let twoPi = 2*Math.PI;
let Pi = Math.PI;
let sin = Math.sin, cos = Math.cos;
let HourHandPolygon = [
-halfHourHandWidth,halfHourHandWidth,
-halfHourHandWidth,halfHourHandWidth-HourHandLength,
halfHourHandWidth,halfHourHandWidth-HourHandLength,
halfHourHandWidth,halfHourHandWidth,
];
let MinuteHandPolygon = [
-halfMinuteHandWidth,halfMinuteHandWidth,
-halfMinuteHandWidth,halfMinuteHandWidth-MinuteHandLength,
halfMinuteHandWidth,halfMinuteHandWidth-MinuteHandLength,
halfMinuteHandWidth,halfMinuteHandWidth,
];
/**** drawClockFace ****/
function drawClockFace () {
g.setColor(g.theme.fg);
g.setFont('Vector', 22);
g.setFontAlign(0,-1);
g.drawString('12', CenterX,CenterY-outerRadius);
g.setFontAlign(1,0);
g.drawString('3', CenterX+outerRadius,CenterY);
g.setFontAlign(0,1);
g.drawString('6', CenterX,CenterY+outerRadius);
g.setFontAlign(-1,0);
g.drawString('9', CenterX-outerRadius,CenterY);
} }
let day = d.getDate();
/**** transforme polygon ****/ if (day > 12) {
day = day % 10;
let transformedPolygon = new Array(HourHandPolygon.length); if (!day)
day = 10;
function transformPolygon (originalPolygon, OriginX,OriginY, Phi) {
let sPhi = sin(Phi), cPhi = cos(Phi), x,y;
for (let i = 0, l = originalPolygon.length; i < l; i+=2) {
x = originalPolygon[i];
y = originalPolygon[i+1];
transformedPolygon[i] = OriginX + x*cPhi + y*sPhi;
transformedPolygon[i+1] = OriginY + x*sPhi - y*cPhi;
}
} }
//draws the numbers on the screen
/**** draw clock hands ****/ for(let i = 0;i<12;i++){
let on = false;
function drawClockHands () { let j = i+1;
let now = new Date(); g.setFont("Vector",20);
if (j == day) {
let Hours = now.getHours() % 12; on = true;
let Minutes = now.getMinutes(); g.setFont("Vector",29);
let Seconds = now.getSeconds(); }
if ((j % 12) == (hour % 12))
let HoursAngle = (Hours+(Minutes/60))/12 * twoPi - Pi; on = true;
let MinutesAngle = (Minutes/60) * twoPi - Pi; setColor();
let SecondsAngle = (Seconds/60) * twoPi - Pi; if (!on)
g.setColor(white/2, !white, white);
g.setColor(g.theme.fg); if (1 || on)
g.drawString(zahlpos[i][0],zahlpos[i][1],zahlpos[i][2],true);
transformPolygon(HourHandPolygon, CenterX,CenterY, HoursAngle);
g.fillPoly(transformedPolygon);
transformPolygon(MinuteHandPolygon, CenterX,CenterY, MinutesAngle);
g.fillPoly(transformedPolygon);
let sPhi = Math.sin(SecondsAngle), cPhi = Math.cos(SecondsAngle);
g.setColor(g.theme.fg2);
g.drawLine(
CenterX + SecondHandOffset*sPhi,
CenterY - SecondHandOffset*cPhi,
CenterX - SecondHandLength*sPhi,
CenterY + SecondHandLength*cPhi
);
g.setFont('Vector', 22);
g.setFontAlign(-1, 1);
g.drawString(now.getDate(), CenterX-outerRadius,CenterY+outerRadius);
}
function drawTimeIcon(time, icon, options) {
let h = fracHour(time);
let x = radX(h/12, 0.7);
let y = radY(h/12, 0.7);
g.drawImage(icon, x,y, options);
}
function drawOutsideIcon(h, icon, options) {
let x = radX(h, 0.95);
let y = radY(h, 0.95);
g.drawImage(icon, x,y, options);
}
function drawBorders() {
g.reset();
g.setColor(0);
g.fillRect(Bangle.appRect);
g.setColor(-1);
g.fillCircle(w/2, h/2, h/2 - 2);
if (0) {
g.fillCircle(sm+1, sm+1, sm);
g.fillCircle(sm+1, h-sm-1, sm);
g.fillCircle(w-sm-1, h-sm-1, sm);
g.fillCircle(h-sm-1, sm+1, sm);
}
g.setColor(0, 1, 0);
g.drawCircle(h/2, w/2, radD(0.7));
g.drawCircle(h/2, w/2, radD(0.5));
outerRadius = radD(0.7);
drawClockHands();
let d = new Date();
let hour = fracHour(d);
let min = d.getMinutes();
let day = d.getDay();
day = day + hour/24;
{
let x = radX(hour/12, 0.7);
let y = radY(hour/12, 0.7);
g.setColor(0, 0, 0);
g.fillCircle(x,y, 5);
}
{
let x = radX(min/60, 0.5);
let y = radY(min/60, 0.5);
g.setColor(0, 0, 0);
g.drawLine(h/2, w/2, x, y);
}
{
let x = radX(hour/12, 0.3);
let y = radY(hour/12, 0.3);
g.setColor(0, 0, 0);
g.drawLine(h/2, w/2, x, y);
}
{
let km = 0.001 * 0.719 * Bangle.getHealthStatus("day").steps;
let x = radX(km/12 + 0, 0.95);
let y = radY(km/12 + 0, 0.95);
g.setColor(0, 0.7, 0);
g.drawImage(img_step, x,y, { scale: 2, rotate: Math.PI*0.0 } );
}
{
let bat = E.getBattery();
let x = radX(bat/100, 0.95);
let y = radY(bat/100, 0.95);
g.setColor(0.7, 0, 0);
g.drawImage(img_battery, x,y, { scale: 2, rotate: Math.PI*0.0 } );
}
{
d = new Date();
const sun = SunCalc.getTimes(d, lat, lon);
g.setColor(0.5, 0.5, 0);
print("sun", sun);
drawTimeIcon(sun.sunset, img_sunrise, { rotate: Math.PI, scale: 2 });
drawTimeIcon(sun.sunrise, img_sunrise, { scale: 2 });
g.setColor(0, 0, 0);
const moon = SunCalc.getMoonTimes(d, lat, lon);
print("moon", moon);
drawTimeIcon(moon.set, img_moonrise, { rotate: Math.PI, scale: 2 });
drawTimeIcon(moon.rise, img_sunrise, { scale: 2 });
let pos = SunCalc.getPosition(d, lat, lon);
print("sun:", pos);
if (pos.altitude > -0.1) {
g.setColor(0.5, 0.5, 0);
az = pos.azimuth;
drawOutsideIcon(az / (2*Math.PI), img_sun, { scale: 2 });
}
pos = SunCalc.getMoonPosition(d, lat, lon);
print("moon:", pos);
if (pos.altitude > -0.05) {
g.setColor(0, 0, 0);
az = pos.azimuth;
drawOutsideIcon(az / (2*Math.PI), img_moon, { scale: 2 });
}
}
{
Bangle.getPressure().then((x) =>
{ altitude = x.altitude; temperature = x.temperature; },
print);
print(altitude, temperature);
drawOutsideIcon(altitude / 120, img_altitude, { scale: 2 });
drawOutsideIcon(temperature / 12, img_temperature, { scale: 2 });
}
if (use_compass) {
let obj = Bangle.getCompass();
if (obj) {
let h = 360-obj.heading;
let x = radX(h/360, 0.7);
let y = radY(h/360, 0.7);
g.setColor(0, 0, 1);
g.drawImage(img_north, x,y, {scale:2});
}
}
{
let x = radX(day/7, 0.95);
let y = radY(day/7, 0.95);
g.setColor(0, 0, 0);
g.fillCircle(x,y, 5);
} }
} }
function drawEmpty() { function draw(){
g.reset(); // draw black rectangle in the middle to clear screen from scale and hands
g.setColor(g.theme.bg); g.setColor(!white,!white,!white);
g.fillRect(Bangle.appRect); g.fillRect(10,10,2*c.x-10,2*c.x-10);
// prepare for drawing the text
g.setFontAlign(0,0);
// do drawing
const d=new Date();
drawScale(d); // FIXME: it is enough to do once in 12 hours or so
drawNumbers(d);
if (settings.textAboveHands) {
drawHands(d); drawText(d);
} else {
drawText(d); drawHands(d);
}
} }
Bangle.on('touch', function(button, xy) { /* 0..12 -> angle suitable for drawScale */
var x = xy.x; function conv(m) { return -15 + (m / 12) * 60; }
var y = xy.y; /* datetime -> 0..12 float */
if (y > h) y = h; function hour12(d) {
if (y < 0) y = 0; let h = d.getHours() + d.getMinutes() / 60;
if (x > w) x = w; if (h > 12)
if (x < 0) x = 0; h = h - 12;
}); return h;
// if we get a step then we are not idle
Bangle.on('step', s => {
});
// timeout used to update every minute
var drawTimeout;
// schedule a draw for the next minute
function queueDraw() {
if (drawTimeout) clearTimeout(drawTimeout);
next = 60000;
if (use_compass) next = 250;
drawTimeout = setTimeout(function() {
drawTimeout = undefined;
draw();
}, next - (Date.now() % next));
} }
//draws the scale once the app is started
function drawScale(d){
// clear the screen
g.setBgColor(!white,!white,!white);
g.clear();
// Display month as a wider mark
let m = conv(d.getMonth() + 1);
print(m);
let pos = sun.sunPos().azimuth;
pos = conv(12*(pos/360));
let t = sun.sunTime();
// FIXME
let set = conv(hour12(t.sunset));
let dark = conv(hour12(t.sunset) + 0.25);
print(set, dark, pos);
// draw the ticks of the scale
for(let i=-14;i<47;i++){
const win=i*2*Math.PI/60;
let d=2;
if(i%5==0){d=5;}
if(i==m){d=10;}
if (i>=pos && i<=(pos+2))
g.setColor(!white,!white,white/2);
else if (i>=set && i<=dark)
g.setColor(white/2,!white,white/2);
else
g.setColor(white,white,white);
g.fillPoly(zeiger(300,d,win),true);
g.setColor(!white,!white,!white);
g.fillRect(10,10,2*c.x-10,2*c.x-10);
}
}
//// main running sequence ////
// Show launcher when middle button pressed, and widgets that we're clock
Bangle.setUI("clock");
// Load widgets if needed, and make them show swipeable
if (settings.loadWidgets) {
Bangle.loadWidgets();
require("widget_utils").swipeOn();
} else if (global.WIDGETS) require("widget_utils").hide();
// Clear the screen once, at startup
drawScale(new Date());
draw();
let secondInterval = setInterval(draw, 1000);
// Stop updates when LCD is off, restart when on // Stop updates when LCD is off, restart when on
Bangle.on('lcdPower',on=>{ Bangle.on('lcdPower',on=>{
if (secondInterval) clearInterval(secondInterval);
secondInterval = undefined;
if (on) { if (on) {
draw(); // draw immediately, queue redraw secondInterval = setInterval(draw, 1000);
} else { // stop draw timer draw(); // draw immediately
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
} }
}); });
Bangle.on('lock',on=>{
Bangle.setUI("clockupdown", btn=> { unlock = !on;
if (btn<0) use_compass = 0; if (secondInterval) clearInterval(secondInterval);
if (btn>0) use_compass = 1; secondInterval = setInterval(draw, unlock ? 1000 : 60000);
Bangle.setCompassPower(use_compass, 'orloj'); draw(); // draw immediately
draw();
}); });
Bangle.on('charging',on=>{draw();});
if (use_compass)
Bangle.setCompassPower(true, 'orloj');
g.clear();
draw();

View File

@ -1,6 +1,6 @@
{ "id": "orloj", { "id": "orloj",
"name": "Orloj", "name": "Orloj",
"version": "0.03", "version": "0.10",
"description": "Astronomical clock", "description": "Astronomical clock",
"icon": "app.png", "icon": "app.png",
"readme": "README.md", "readme": "README.md",

View File

@ -4,3 +4,6 @@
0.04: Minor code improvements 0.04: Minor code improvements
0.05: Upgrade OWM to One Call API 3.0. Add pressure to weather.json 0.05: Upgrade OWM to One Call API 3.0. Add pressure to weather.json
0.06: Fix One Call API 3.0 not returning city names, which are required by the weather app 0.06: Fix One Call API 3.0 not returning city names, which are required by the weather app
0.07: Update weather after reconnecting bluetooth if update is due, refactor code
0.08: Undo change to One Call API 3.0
0.09: Fix infinite loop when settings.updated is not defined

View File

@ -1,33 +1,47 @@
{ {
let waiting = false; let loading = false;
let timeoutRef = null;
let settings = Object.assign( let settings = Object.assign(
require('Storage').readJSON("owmweather.default.json", true) || {}, require('Storage').readJSON("owmweather.default.json", true) || {},
require('Storage').readJSON("owmweather.json", true) || {} require('Storage').readJSON("owmweather.json", true) || {}
); );
let completion = function(){ let refreshMillis = function () {
waiting = false; return settings.refresh * 1000 * 60 + 1; // +1 <- leave some slack
};
let onCompleted = function () {
loading = false;
settings.updated = Date.now(); settings.updated = Date.now();
require('Storage').writeJSON("owmweather.json", settings); require('Storage').writeJSON("owmweather.json", settings);
} if (timeoutRef) clearTimeout(timeoutRef);
timeoutRef = setTimeout(loadIfDueAndReschedule, refreshMillis());
};
if (settings.enabled) { let loadIfDueAndReschedule = function () {
// also check if the weather.json file has been updated (e.g. force refresh)
let weather = require("Storage").readJSON('weather.json') || {}; let weather = require("Storage").readJSON('weather.json') || {};
if (weather && weather.weather && weather.weather.time) lastUpdate = weather.weather.time; let lastWeatherUpdate = weather && weather.weather && weather.weather.time && weather.weather.time || 0;
if (lastWeatherUpdate > settings.updated) {
if (!settings.updated || settings.updated + settings.refresh * 1000 * 60 < Date.now()){ settings.updated = lastWeatherUpdate;
setTimeout(() => {
if (!waiting){
waiting = true;
require("owmweather").pull(completion);
}
}, 5000);
} }
setInterval(() => {
if (!waiting && NRF.getSecurityStatus().connected){ let MillisUntilDue = settings.updated + refreshMillis() - Date.now();
waiting = true; if (!MillisUntilDue || MillisUntilDue <= 0) {
require("owmweather").pull(completion); if (!loading) {
loading = true;
require("owmweather").pull(onCompleted);
} }
}, settings.refresh * 1000 * 60); } else {
// called to early, reschedule
// console.log('Weather data is not due yet, rescheduling in ' + (MillisUntilDue || 0) + 'ms');
if (timeoutRef) clearTimeout(timeoutRef);
timeoutRef = setTimeout(loadIfDueAndReschedule, MillisUntilDue + 1);
}
};
if (settings.enabled) {
setTimeout(loadIfDueAndReschedule, 5000); // run 5 seconds after boot
NRF.on('connect', loadIfDueAndReschedule); // after reconnect, fetch the weather data right away if it's due
} }
} }

View File

@ -1,23 +1,20 @@
function parseWeather(response) { function parseWeather(response) {
let owmData = JSON.parse(response); let owmData = JSON.parse(response);
let isOwmData = false; let isOwmData = owmData.coord && owmData.weather && owmData.main;
try {
isOwmData = (owmData.lat && owmData.lon) && owmData.current.weather && owmData.current;
} catch (_e) {}
if (isOwmData) { if (isOwmData) {
let json = require("Storage").readJSON('weather.json') || {}; let json = require("Storage").readJSON('weather.json') || {};
let weather = {}; let weather = {};
weather.time = Date.now(); weather.time = Date.now();
weather.hum = owmData.current.humidity; weather.hum = owmData.main.humidity;
weather.temp = owmData.current.temp; weather.temp = owmData.main.temp;
weather.code = owmData.current.weather[0].id; weather.code = owmData.weather[0].id;
weather.wdir = owmData.current.wind_deg; weather.wdir = owmData.wind.deg;
weather.wind = owmData.current.wind_speed; weather.wind = owmData.wind.speed;
weather.loc = owmData.name || ""; weather.loc = owmData.name;
weather.txt = owmData.current.weather[0].main; weather.txt = owmData.weather[0].main;
weather.hpa = owmData.current.pressure || 0; weather.hpa = owmData.main.pressure || 0;
if (weather.wdir != null) { if (weather.wdir != null) {
let deg = weather.wdir; let deg = weather.wdir;
@ -43,7 +40,7 @@ exports.pull = function(completionCallback) {
"location": "London" "location": "London"
}; };
let settings = require("Storage").readJSON("owmweather.json", 1); let settings = require("Storage").readJSON("owmweather.json", 1);
let uri = "https://api.openweathermap.org/data/3.0/onecall?lat=" + location.lat.toFixed(2) + "&lon=" + location.lon.toFixed(2) + "&exclude=minutely,hourly,daily,alerts&appid=" + settings.apikey; let uri = "https://api.openweathermap.org/data/2.5/weather?lat=" + location.lat.toFixed(2) + "&lon=" + location.lon.toFixed(2) + "&exclude=hourly,daily&appid=" + settings.apikey;
if (Bangle.http){ if (Bangle.http){
Bangle.http(uri, {timeout:10000}).then(event => { Bangle.http(uri, {timeout:10000}).then(event => {
let result = parseWeather(event.resp); let result = parseWeather(event.resp);

View File

@ -1,7 +1,7 @@
{ "id": "owmweather", { "id": "owmweather",
"name": "OpenWeatherMap weather provider", "name": "OpenWeatherMap weather provider",
"shortName":"OWM Weather", "shortName":"OWM Weather",
"version": "0.06", "version": "0.09",
"description": "Pulls weather from OpenWeatherMap (OWM) API", "description": "Pulls weather from OpenWeatherMap (OWM) API",
"icon": "app.png", "icon": "app.png",
"type": "bootloader", "type": "bootloader",

View File

@ -1 +1,2 @@
0.01: New app! 0.01: New app!
0.02: Show elapsed time on pause screen

View File

@ -85,26 +85,30 @@
var h = g.getHeight(); var h = g.getHeight();
var max = splits_1.reduce(function (a, s) { return Math.max(a, s.time); }, 0); var max = splits_1.reduce(function (a, s) { return Math.max(a, s.time); }, 0);
g.setFont("6x8", 2).setFontAlign(-1, -1); g.setFont("6x8", 2).setFontAlign(-1, -1);
var y = Bangle.appRect.y + barSpacing / 2;
g
.setColor(g.theme.fg)
.drawString(formatDuration_1(exs_1.state.duration), 0, y);
var i = 0; var i = 0;
for (;; i++) { for (;; i++) {
var split = splits_1[i + splitOffset_1]; var split = splits_1[i + splitOffset_1];
if (split == null) if (split == null)
break; break;
var y_1 = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2; var y_1 = Bangle.appRect.y + (i + 1) * (barSize + barSpacing) + barSpacing / 2;
if (y_1 > h) if (y_1 > h)
break; break;
var size = w * split.time / max; var size = w * split.time / max;
g.setColor("#00f").fillRect(0, y_1, size, y_1 + barSize); g.setColor("#00f").fillRect(0, y_1, size, y_1 + barSize);
var splitPace = calculatePace_1(split); var splitPace = calculatePace_1(split);
g.setColor(g.theme.fg);
drawSplit_1(i, y_1, splitPace); drawSplit_1(i, y_1, splitPace);
} }
var pace = exs_1.stats.pacec.getString(); var pace = exs_1.stats.pacec.getString();
var y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2; y = Bangle.appRect.y + (i + 1) * (barSize + barSpacing) + barSpacing / 2;
drawSplit_1(i, y, pace); drawSplit_1(i, y, pace);
}; };
var drawSplit_1 = function (i, y, pace) { var drawSplit_1 = function (i, y, pace) {
g return g
.setColor(g.theme.fg)
.drawString("".concat(i + 1 + splitOffset_1, " ").concat(typeof pace === "number" ? pace.toFixed(2) : pace), 0, y); .drawString("".concat(i + 1 + splitOffset_1, " ").concat(typeof pace === "number" ? pace.toFixed(2) : pace), 0, y);
}; };
var pauseRun_1 = function () { var pauseRun_1 = function () {

View File

@ -114,36 +114,40 @@ const drawSplits = () => {
g.setFont("6x8", 2).setFontAlign(-1, -1); g.setFont("6x8", 2).setFontAlign(-1, -1);
let y = Bangle.appRect.y + barSpacing / 2;
g
.setColor(g.theme.fg)
.drawString(formatDuration(exs.state.duration), 0, y);
let i = 0; let i = 0;
for(; ; i++) { for(; ; i++) {
const split = splits[i + splitOffset]; const split = splits[i + splitOffset];
if (split == null) break; if (split == null) break;
const y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2; const y = Bangle.appRect.y + (i + 1) * (barSize + barSpacing) + barSpacing / 2;
if (y > h) break; if (y > h) break;
const size = w * split.time / max; // Scale bar height based on pace const size = w * split.time / max; // Scale bar height based on pace
g.setColor("#00f").fillRect(0, y, size, y + barSize); g.setColor("#00f").fillRect(0, y, size, y + barSize);
const splitPace = calculatePace(split); // Pace per km const splitPace = calculatePace(split); // Pace per km
g.setColor(g.theme.fg)
drawSplit(i, y, splitPace); drawSplit(i, y, splitPace);
} }
const pace = exs.stats.pacec.getString(); const pace = exs.stats.pacec.getString();
const y = Bangle.appRect.y + i * (barSize + barSpacing) + barSpacing / 2; y = Bangle.appRect.y + (i + 1) * (barSize + barSpacing) + barSpacing / 2;
drawSplit(i, y, pace); drawSplit(i, y, pace);
}; };
const drawSplit = (i: number, y: number, pace: number | string) => { const drawSplit = (i: number, y: number, pace: number | string) =>
g g
.setColor(g.theme.fg)
.drawString( .drawString(
`${i + 1 + splitOffset} ${typeof pace === "number" ? pace.toFixed(2) : pace}`, `${i + 1 + splitOffset} ${typeof pace === "number" ? pace.toFixed(2) : pace}`,
0, 0,
y y
); );
};
const pauseRun = () => { const pauseRun = () => {
exs.stop(); exs.stop();

View File

@ -1,7 +1,7 @@
{ {
"id": "pace", "id": "pace",
"name": "Pace", "name": "Pace",
"version": "0.01", "version": "0.02",
"description": "Show pace and time running splits", "description": "Show pace and time running splits",
"icon": "app.png", "icon": "app.png",
"tags": "run,running,fitness,outdoors", "tags": "run,running,fitness,outdoors",

View File

@ -8,6 +8,6 @@ Addict.
0.07: Remove just the specific listeners to not interfere with Quick Launch 0.07: Remove just the specific listeners to not interfere with Quick Launch
when fastloading. when fastloading.
0.08: Issue newline before GB commands (solves issue with console.log and ignored commands) 0.08: Issue newline before GB commands (solves issue with console.log and ignored commands)
0.09: Don't send the gadgetbridge wake command twice. Once should do since we 0.09: Don't send the gadgetbridge wake command twice. Once should do since we issue newline before GB commands.
issue newline before GB commands.
0.10: Minor code improvements 0.10: Minor code improvements
0.11: Fix a warning from the linter.

View File

@ -127,7 +127,7 @@ The functions for interacting with Android and the Podcast Addict app
let pkg = "com.bambuna.podcastaddict"; let pkg = "com.bambuna.podcastaddict";
let standardCls = pkg + ".receiver.PodcastAddictPlayerReceiver"; let standardCls = pkg + ".receiver.PodcastAddictPlayerReceiver";
let updateCls = pkg + ".receiver.PodcastAddictBroadcastReceiver"; let updateCls = pkg + ".receiver.PodcastAddictBroadcastReceiver";
let speed = 1.0; //let speed = 1.0;
let simpleSearch = ""; let simpleSearch = "";
@ -269,15 +269,15 @@ let speedMenu = {
} }
}, },
"Regular Speed": () => { "Regular Speed": () => {
speed = 1.0; //speed = 1.0;
btMsg("service", standardCls, "player.1xspeed"); btMsg("service", standardCls, "player.1xspeed");
}, },
"1.5x Regular Speed": () => { "1.5x Regular Speed": () => {
speed = 1.5; //speed = 1.5;
btMsg("service", standardCls, "player.1.5xspeed"); btMsg("service", standardCls, "player.1.5xspeed");
}, },
"2x Regular Speed": () => { "2x Regular Speed": () => {
speed = 2.0; //speed = 2.0;
btMsg("service", standardCls, "player.2xspeed"); btMsg("service", standardCls, "player.2xspeed");
}, },
//"Faster" : ()=>{speed+=0.1; speed=((speed>5.0)?5.0:speed); btMsg("service",standardCls,"player.customspeed",{arg1:speed});}, //"Faster" : ()=>{speed+=0.1; speed=((speed>5.0)?5.0:speed); btMsg("service",standardCls,"player.customspeed",{arg1:speed});},

View File

@ -2,7 +2,7 @@
"id": "podadrem", "id": "podadrem",
"name": "Podcast Addict Remote", "name": "Podcast Addict Remote",
"shortName": "PA Remote", "shortName": "PA Remote",
"version": "0.10", "version": "0.11",
"description": "Control Podcast Addict on your android device.", "description": "Control Podcast Addict on your android device.",
"readme": "README.md", "readme": "README.md",
"type": "app", "type": "app",

View File

@ -0,0 +1,6 @@
0.01: Packaged app
0.02: Fix alert buzz time, Indicate when paused
0.03: Start app with paused timer
0.04: Added 20-second warning buzz
0.05: Added screenshots
0.06: Fix bug when play/pause during alert

9
apps/pokertimer/LICENSE Normal file
View File

@ -0,0 +1,9 @@
The MIT License (MIT)
Copyright © 2024 Keith Irwin
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

55
apps/pokertimer/README.md Normal file
View File

@ -0,0 +1,55 @@
# Poker Timer
*v.0.06*
A blinds timer for poker. Don't know what that means? See [Wikipedia: Blind (poker)](https://en.wikipedia.org/wiki/Blind_(poker)) and [Wikipedia: Texas hold 'em](https://en.wikipedia.org/wiki/Texas_hold_%27em).
![Screenshot showing countdown paused on start](screenshots/01_paused-start.png)
![Screenshot showing active countdown](screenshots/02_counting-down.png)
![Screenshot showing blinds up alert](screenshots/03_blinds-up.png)
The blinds are hardcoded and go up every ten minutes:
- 1, 2
- 2, 4
- 4, 8
- 5, 10
- 10, 20
- 20, 40
- 40, 80
... and so on, doubling each round.
## Features
- Starts paused
- Button to pause/resume
- 20-second warning buzz
- Auto-exit after round 25
## Usage
The timer will start as soon as you open the app. Time left in the round is on the top of the screen, currnt small and big blinds are shown below. After ten minutes, it will vibrate and flash and show the new blind. Then it starts over.
### Auto-exit
The program will automatically exit after the 25 round. This is not a bug. If the blinds double again, it will perform some kind of overflow and convert the blind values to floats.
The blinds in round 25 are `20971520 / 41943040`. You probably aren't still playing poker at that point and just forgot to exit the program.
## Controls
- **Pause/Resume:** Press the button
- **Exit:** hold down the button
## Roadmap
- Set settings
- Better graphics
## Requests
[Contact Keith Irwin](https://www.ki9.us/contact/)
## Creator
[Keith Irwin](https://www.ki9.us)

View File

@ -0,0 +1 @@
require("heatshrink").decompress(atob("kso4cB7nW///7fWm2Vw8p7fOgH6lM6hEqgQCDkNCgmSpMkyUQqkEAQsJBYIOBkGohACD8dIiQaCpFBhUQAQVD+9qDQeQwWgkGCikg1lxknZBwUhgkooICCnMjpO1qIOBoUKwGCAQXt32TtMkwUkwkVgIdFg3SGQOBNYoCChuklfAgBQCPokqjdBluCNAJTBkN4UwVBm2C5ckxFBqEQq/+BYKMBtstz1JiBuC/+T2gjC7VYlmSkMFoFOQQON0mShO0iFNkhuC8iCBk4aBwdpFgNENwMOVgft1Ms9uiwNJLIM/VgWTxUFjdrtsUiUQ0NPXYe0qE27XRoLdC/wpCpxvBtuzpMiboX8DQXtQYPbtto0jdCm87zvOIwMh23YgDdBNwNBh/bYoL7BtuapDdE2VGWYMgw3brUpbocF/mcIYMIlu10WCpL4FihEB3PboBaBiFUGQOhAQcbqkgLQL1CdIYCBm2oIgNIru36VJkD+BboUgxMkyGcyOk1VIkGFboWhiRuBrHiqpBBIgUVboJEBoUf3dSK4IpBhUTtEiIgOEjlpGolBg3QoiuBJoZWCAQMN0kJFIIyCIIQCBkNb1JEBc4Ul2zKBAQW7qLzDBw7pBBYYCDwgpCoUEBYoCC0A7CBY4CCykooALIBwcRAoQ"))

142
apps/pokertimer/app.js Normal file
View File

@ -0,0 +1,142 @@
const BLIND_INTERVAL = 600; // 10 minutes in seconds
const BLINDSUP_ALERT_DURATION = 10000; // 10 seconds in ms
// Convert seconds to mm:ss
const secondsToMinutes = (s) => {
const mm = Math.floor(s/60);
const ss = s - mm * 60;
return `${mm}:${String(ss).padStart(2,'0')}`;
};
// Format screen
const fmtDark = () => {
g.clear();
g.setFontAlign(0,0);
g.setBgColor(0,0.5,0);
g.setColor(1,1,1);
};
const fmtLight = () => {
g.clear();
g.setFontAlign(0,0);
g.setBgColor(0.5,1,0.5);
g.setColor(0,0,0);
};
// Start/stop/pause/resume timer
const startTimer = () => {
timer_running = true; tick();
timer = setInterval(tick, 1000);
};
const stopTimer = () => {
clearInterval(timer);
timer_running = false;
};
const pauseResume = () => {
if (is_alerting) return;
if (timer_running) {
stopTimer();
g.setFont('Vector',15);
g.drawString('(PAUSED)',
g.getWidth()/2, g.getHeight()*7/8);
}
else startTimer();
};
// Calculate blinds for a round
const getBlinds = (i) => {
let small;
if (i===0) small = 1;
else if (i===1) small = 2;
else if (i===2) small = 4;
else small = 5*(Math.pow(2,(i-3)));
return [small, small*2];
};
// Sound the alarm
const blindsUp = () => {
is_alerting = true;
// Display message
const showMessage = () => {
g.clear();
g.setFont('Vector',34);
g.drawString('Blinds Up!',
g.getWidth()/2, g.getHeight()/3);
g.setFont('Vector',40);
g.drawString(`${blinds[0]}/${blinds[1]}`,
g.getWidth()/2, g.getHeight()*2/3);
};
stopTimer();
// Increase blinds
b++;
// TODO: Kill program between round 25 and 26
blinds = getBlinds(b);
console.log(`Blinds for round ${b} are ${blinds[0]} / ${blinds[1]}`);
// Buzz and light up every second
const buzzInterval = setInterval(() => {
Bangle.buzz(500);
Bangle.setLCDPower(1);
}, 1000);
// Invert colors every second
fmtLight(); showMessage(); let dark = false;
const flashInterval = setInterval(() => {
if (dark) {
fmtLight();
dark = false;
} else {
fmtDark();
dark = true;
} showMessage();
}, 500);
// Restart timer
setTimeout(() => {
is_alerting = false;
fmtDark(); tick();
clearInterval(buzzInterval);
clearInterval(flashInterval);
time_left = BLIND_INTERVAL + 1;
startTimer();
}, BLINDSUP_ALERT_DURATION);
};
// Tick every second
const tick = () => {
if (!timer_running) return;
time_left--;
// 20-second warning buzz
if (time_left==20) {
const buzzInterval = setInterval(Bangle.buzz, 500);
setTimeout(() => {
clearInterval(buzzInterval);
}, 5000);
}
if (time_left<=0) blindsUp();
else {
g.clear();
g.setFont('Vector',40);
g.drawString(
secondsToMinutes(time_left),
g.getWidth()/2, g.getHeight()/3);
g.drawString(
`${blinds[0]}/${blinds[1]}`,
g.getWidth()/2, g.getHeight()*2/3);
}
return;
};
// button listener
Bangle.setUI({
mode: 'custom',
btn: pauseResume,
});
// RUNTIME
fmtDark();
let time_left = BLIND_INTERVAL + 1;
let b = 0;
let blinds = getBlinds(b);
let timer_running = true;
let is_alerting = false;
let timer = setInterval(tick, 1000);
tick();
// Start paused
pauseResume();

BIN
apps/pokertimer/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,15 @@
{
"id": "pokertimer",
"name": "Poker Timer",
"shortName":"Poker Timer",
"readme":"README.md",
"icon": "app.png",
"version":"0.06",
"description": "A blinds timer for use with Texas Hold 'Em",
"tags": "poker",
"supports": ["BANGLEJS2"],
"storage": [
{"name":"pokertimer.app.js","url":"app.js"},
{"name":"pokertimer.img","url":"app-icon.js","evaluate":true}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -11,3 +11,4 @@
0.07: Fix bug with alarms app (scroller) and correctly show images 0.07: Fix bug with alarms app (scroller) and correctly show images
0.08: Fix bug with modifying menu - allows hadash to save scroll positions 0.08: Fix bug with modifying menu - allows hadash to save scroll positions
0.09: Don't show "..." if a string isn't truncated (i.e. scrolled) 0.09: Don't show "..." if a string isn't truncated (i.e. scrolled)
0.10: Trigger `remove` callbacks when ending the menu

View File

@ -193,9 +193,11 @@ E.showMenu = function (items) {
mode: "updown", mode: "updown",
back: back, back: back,
remove: function () { remove: function () {
var _a;
if (nameScroller) if (nameScroller)
clearInterval(nameScroller); clearInterval(nameScroller);
Bangle.removeListener("swipe", onSwipe); Bangle.removeListener("swipe", onSwipe);
(_a = options.remove) === null || _a === void 0 ? void 0 : _a.call(options);
}, },
}, function (dir) { }, function (dir) {
if (dir) if (dir)

View File

@ -240,6 +240,7 @@ E.showMenu = (items?: Menu): MenuInstance => {
remove: () => { remove: () => {
if (nameScroller) clearInterval(nameScroller); if (nameScroller) clearInterval(nameScroller);
Bangle.removeListener("swipe", onSwipe); Bangle.removeListener("swipe", onSwipe);
options.remove?.();
}, },
} as SetUIArg<"updown">, } as SetUIArg<"updown">,
dir => { dir => {

View File

@ -1,7 +1,7 @@
{ {
"id": "promenu", "id": "promenu",
"name": "Pro Menu", "name": "Pro Menu",
"version": "0.09", "version": "0.10",
"description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.", "description": "Replace the built in menu function. Supports Bangle.js 1 and Bangle.js 2.",
"icon": "icon.png", "icon": "icon.png",
"type": "bootloader", "type": "bootloader",

View File

@ -7,12 +7,10 @@
0.07: Revert version 0.06. This version is the same as 0.05. 0.07: Revert version 0.06. This version is the same as 0.05.
0.08: Respect appRect on touch events 0.08: Respect appRect on touch events
0.09: Do not react if clkinfo is focused 0.09: Do not react if clkinfo is focused
0.10: Extend the functionality via a quicklaunch.app.js file that can be launched 0.10: Extend the functionality via a quicklaunch.app.js file that can be launched with quicklaunch itself.
with quicklaunch itself.
0.11: Add hints to the extension app. Tweak remove function. 0.11: Add hints to the extension app. Tweak remove function.
0.12: Stackable extension screens. After updating, please visit the quicklaunch 0.12: Stackable extension screens. After updating, please visit the quicklaunch settings page to prompt an automatic update of the quicklaunch.json settings file with new key names.
settings page to prompt an automatic update of the quicklaunch.json settings file with
new key names.
0.13: Touch and hold to pause the timeout to clock temporarily. 0.13: Touch and hold to pause the timeout to clock temporarily.
0.14: Extension: Don't go down a path if nothing waits at the end. Revisit the current intersection instead. 0.14: Extension: Don't go down a path if nothing waits at the end. Revisit the current intersection instead.
0.15: Extension: Compatibility with "Fastload Utils" app history feature. 0.15: Extension: Compatibility with "Fastload Utils" app history feature.
0.16: Snappier. Fewer storage interactions.

View File

@ -1,31 +1,40 @@
{ {
const R = Bangle.appRect;
g.clearRect(R); // clear immediately to increase perceived snappiness.
const storage = require("Storage"); const storage = require("Storage");
let settings = storage.readJSON("quicklaunch.json", true) || {}; let settings = storage.readJSON("quicklaunch.json", true) || {};
let trace = (settings[settings.trace+"app"].src=="quicklaunch.app.js") ? settings.trace : settings.trace.substring(0, settings.trace.length-1); // If the stored trace leads beyond extension screens, walk back to the last extension screen. Compatibility with "Fastload Utils" App History feature.
let reset = function(name){ const draw = () => {
if (!settings[name]) settings[name] = {"name":"(none)"}; // Draw app hints
if (!storage.read(settings[name].src)) settings[name] = {"name":"(none)"}; g.reset().clearRect(R).setFont("Vector", 11)
storage.write("quicklaunch.json", settings); .setFontAlign(0,1,3).drawString(settings[trace+"lapp"].name, R.x2, R.y+R.h/2)
.setFontAlign(0,1,1).drawString(settings[trace+"rapp"].name, R.x, R.y+R.h/2)
.setFontAlign(0,1,0).drawString(settings[trace+"uapp"].name, R.x+R.w/2, R.y2)
.setFontAlign(0,-1,0).drawString(settings[trace+"dapp"].name, R.x+R.w/2, R.y)
.setFontAlign(0,0,0).drawString(settings[trace+"tapp"].name, R.x+R.w/2, R.y+R.h/2);
}; };
draw(); // draw asap to increase perceived snappiness.
let leaveTrace = function(trace) { let leaveTrace = function(trace) {
if (settings[trace+"app"].name != "") { if (settings[trace+"app"].name != "") {
settings.trace = trace; settings.trace = trace;
storage.writeJSON("quicklaunch.json", settings);
} else { trace = trace.substring(0, trace.length-1); } } else { trace = trace.substring(0, trace.length-1); }
return trace; return trace;
}; };
let launchApp = function(trace) { let launchApp = function(trace) {
if (settings[trace+"app"]) { if (settings[trace+"app"] && settings[trace+"app"].src) {
if (settings[trace+"app"].src){ if (settings[trace+"app"].name == "Extension") draw();
if (settings[trace+"app"].name == "Show Launcher") Bangle.showLauncher(); else if (!storage.read(settings[trace+"app"].src)) reset(trace+"app"); else load(settings[trace+"app"].src); else if (settings[trace+"app"].name == "Show Launcher") Bangle.showLauncher();
} else if (!storage.read(settings[trace+"app"].src)) {
E.showMessage(settings[trace+"app"].src+"\n"+/*LANG*/"was not found"+".", "Quick Launch");
settings[trace+"app"] = {"name":"(none)"}; // reset entry.
} else load(settings[trace+"app"].src);
} }
}; };
let trace = (settings[settings.trace+"app"].src=="quicklaunch.app.js") ? settings.trace : settings.trace.substring(0, settings.trace.length-1); // If the stored trace leads beyond extension screens, walk back to the last extension screen. Compatibility with "Fastload Utils" App History feature.
let touchHandler = (_,e) => { let touchHandler = (_,e) => {
if (e.type == 2) return; if (e.type == 2) return;
let R = Bangle.appRect; let R = Bangle.appRect;
@ -47,15 +56,22 @@
if (e.b == 0 && !timeoutToClock) updateTimeoutToClock(); if (e.b == 0 && !timeoutToClock) updateTimeoutToClock();
}; };
let saveAndClear = ()=> {
storage.writeJSON("quicklaunch.json", settings);
E.removeListener("kill", saveAndClear);
if (timeoutToClock) clearTimeout(timeoutToClock); // Compatibility with Fastload Utils.
}
Bangle.setUI({ Bangle.setUI({
mode: "custom", mode: "custom",
touch: touchHandler, touch: touchHandler,
swipe : swipeHandler, swipe : swipeHandler,
drag : onLongTouchDoPause, drag : onLongTouchDoPause,
remove: ()=>{if (timeoutToClock) clearTimeout(timeoutToClock);} // Compatibility with Fastload Utils. remove: saveAndClear
}); });
g.clearRect(Bangle.appRect); E.on("kill", saveAndClear)
"Bangle.loadWidgets()"; // Hack: Fool Fastload Utils that we call Bangle.loadWidgets(). This way we get the fastest possibe loading in whichever environment we find ourselves. "Bangle.loadWidgets()"; // Hack: Fool Fastload Utils that we call Bangle.loadWidgets(). This way we get the fastest possibe loading in whichever environment we find ourselves.
// taken from Icon Launcher with some alterations // taken from Icon Launcher with some alterations
@ -67,13 +83,4 @@
}; };
updateTimeoutToClock(); updateTimeoutToClock();
let R = Bangle.appRect;
// Draw app hints
g.setFont("Vector", 11)
.setFontAlign(0,1,3).drawString(settings[trace+"lapp"].name, R.x2, R.y+R.h/2)
.setFontAlign(0,1,1).drawString(settings[trace+"rapp"].name, R.x, R.y+R.h/2)
.setFontAlign(0,1,0).drawString(settings[trace+"uapp"].name, R.x+R.w/2, R.y2)
.setFontAlign(0,-1,0).drawString(settings[trace+"dapp"].name, R.x+R.w/2, R.y)
.setFontAlign(0,0,0).drawString(settings[trace+"tapp"].name, R.x+R.w/2, R.y+R.h/2);
} }

View File

@ -1,24 +1,29 @@
{ {
const storage = require("Storage"); const storage = require("Storage");
let settings = storage.readJSON("quicklaunch.json", true) || {}; let settings;
let reset = function(name){
if (!settings[name]) settings[name] = {"name":"(none)"};
if (!storage.read(settings[name].src)) settings[name] = {"name":"(none)"};
storage.write("quicklaunch.json", settings);
};
let leaveTrace = function(trace) { let leaveTrace = function(trace) {
if (!settings) settings = storage.readJSON("quicklaunch.json", true) || {};
settings.trace = trace; settings.trace = trace;
storage.writeJSON("quicklaunch.json", settings); storage.writeJSON("quicklaunch.json", settings);
return trace; return trace;
}; };
let launchApp = function(trace) { let launchApp = function(trace) {
if (settings[trace+"app"].src){ if (!settings) settings = storage.readJSON("quicklaunch.json", true) || {};
if (settings[trace+"app"].name == "Show Launcher") Bangle.showLauncher(); else if (!storage.read(settings[trace+"app"].src)) reset(trace+"app"); else load(settings[trace+"app"].src);
if (settings[trace+"app"].src) {
if (settings[trace+"app"].name == "Show Launcher") {
Bangle.showLauncher();
} else if (!storage.read(settings[trace+"app"].src)) {
E.showMessage(settings[trace+"app"].src+"\n"+/*LANG*/"was not found"+".", "Quick Launch");
settings[trace+"app"] = {"name":"(none)"}; // reset entry.
storage.write("quicklaunch.json", settings);
setTimeout(load, 2000);
} else {load(settings[trace+"app"].src);}
} }
} };
let trace; let trace;

View File

@ -2,7 +2,7 @@
"id": "quicklaunch", "id": "quicklaunch",
"name": "Quick Launch", "name": "Quick Launch",
"icon": "app.png", "icon": "app.png",
"version": "0.15", "version": "0.16",
"description": "Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice. Configurations can be accessed through Settings->Apps.", "description": "Tap or swipe left/right/up/down on your clock face to launch up to five apps of your choice. Configurations can be accessed through Settings->Apps.",
"type": "bootloader", "type": "bootloader",
"tags": "tools, system", "tags": "tools, system",

View File

@ -1 +1,2 @@
0.01: First Release 0.01: First Release
0.02: Use built-in rounded-rect draw function, faster UI

View File

@ -1,7 +1,7 @@
{ "id": "rest", { "id": "rest",
"name": "Rest - Workout Timer App", "name": "Rest - Workout Timer App",
"shortName":"Rest", "shortName":"Rest",
"version": "0.01", "version": "0.02",
"description": "Rest timer and Set counter for workout, fitness and lifting things.", "description": "Rest timer and Set counter for workout, fitness and lifting things.",
"icon": "app.png", "icon": "app.png",
"screenshots": [{"url": "screenshot1.png"}, {"url": "screenshot2.png"}, {"url": "screenshot3.png"}], "screenshots": [{"url": "screenshot1.png"}, {"url": "screenshot2.png"}, {"url": "screenshot3.png"}],

View File

@ -1,18 +1,6 @@
function roundRect (x1, y1, x2, y2, halfrad) { function roundRect (x1, y1, x2, y2, halfrad) {
const fullrad = halfrad + halfrad g.fillRect({x:x1, y:y1, x2:x2, y2:y2, r: halfrad});
const bgColor = g.getBgColor();
const fgColor = g.getColor();
g.fillRect(x1, y1, x2, y2);
g.setColor(bgColor).fillRect(x1, y1, x1 + halfrad, y1 + halfrad);
g.setColor(fgColor).fillEllipse(x1, y1, x1 + fullrad, y1 + fullrad);
g.setColor(bgColor).fillRect(x2 - halfrad, y1, x2, y1 + halfrad);
g.setColor(fgColor).fillEllipse(x2 - fullrad, y1, x2, y1 + fullrad);
g.setColor(bgColor).fillRect(x1, y2-halfrad, x1 + halfrad, y2);
g.setColor(fgColor).fillEllipse(x1, y2-fullrad, x1 + fullrad, y2);
g.setColor(bgColor).fillRect(x2 - halfrad, y2-halfrad, x2, y2);
g.setColor(fgColor).fillEllipse(x2 - fullrad, y2-fullrad, x2, y2);
} }
function center(r) { function center(r) {

View File

@ -4,7 +4,7 @@ Ring based watchface, read from the outside in. When the watch is unlocked the c
Contributors: Amos Blanton, pinq-. Inspired by Rinkulainen by Julio Kallio. Contributors: Amos Blanton, pinq-. Inspired by Rinkulainen by Julio Kallio.
![](screenshot1.png) ![](screenshot1.png)
View when watch is locked. View when watch is locked. From outside: Hours, minutes, battery.
![](screenshot5.png) ![](screenshot5.png)
View when watch is locked with numbers and bubble settings on View when watch is locked with numbers and bubble settings on

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