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: "/"
schedule:
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
"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_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)
"readme": "README.md", // if supplied, a link to a markdown-style text file
// 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.14: Fixed "Today" and "Yesterday" wrongly displayed for allDay events on some time zones
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);
function getDate(timestamp) {
return new Date(timestamp*1000);
function getDate(timestamp, allDay) {
// 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) {
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) {
return formattedDate;
}
@ -57,8 +62,9 @@ function formatDateLong(date, includeDay, allDay) {
}
return shortTime;
}
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 = [];
@ -69,16 +75,19 @@ function showEvent(ev) {
//var lines = [];
if (ev.title) lines = g.wrapString(ev.title, g.getWidth()-10);
var titleCnt = lines.length;
var start = getDate(ev.timestamp);
var end = getDate((+ev.timestamp) + (+ev.durationInSeconds));
var start = getDate(ev.timestamp, ev.allDay);
// 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;
if (titleCnt) lines.push(""); // add blank line after title
if(start.getDay() == end.getDay() && start.getMonth() == end.getMonth())
includeDay = false;
if(includeDay && ev.allDay) {
//single day all day (average to avoid getting previous day)
if(!includeDay && ev.allDay) {
//single day all day
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) {
lines = lines.concat(
/*LANG*/"Start"+":",
@ -137,7 +146,7 @@ function showList() {
if (!ev) return;
var isPast = false;
var x = r.x+2, title = ev.title;
var body = formatDateShort(getDate(ev.timestamp),ev.allDay)+"\n"+(ev.location?ev.location:/*LANG*/"No location");
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 (title) g.setFontAlign(-1,-1).setFont(fontBig)
.setColor(isPast ? "#888" : g.theme.fg).drawString(title, x+4,r.y+2);

View File

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

View File

@ -4,11 +4,24 @@ App that performs GPS monitoring to track and display position relative to a giv
![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
---------
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.
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)
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:
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.
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 +
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.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 = {
loadWidgets : false,
textAboveHands : false,
@ -25,18 +26,15 @@ const zahlpos=(function() {
return z;
})();
let unlock = false;
function zeiger(len,dia,tim){
const zeiger = function(len,dia,tim) {
const x=c.x+ Math.cos(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)},
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;
};
}
function drawHands(d) {
const drawHands = function(d) {
let m=d.getMinutes(), h=d.getHours(), s=d.getSeconds();
g.setColor(1,1,1);
@ -61,22 +59,22 @@ function drawHands(d) {
g.fillPoly(sekz,true);
}
g.fillCircle(c.x,c.y,4);
}
};
function drawText(d) {
const drawText = function(d) {
g.setFont("Vector",10);
g.setBgColor(0,0,0);
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);
let batStr = Math.round(E.getBattery()/5)*5+"%";
const batStr = Math.round(E.getBattery()/5)*5+"%";
if (Bangle.isCharging()) {
g.setBgColor(1,0,0);
}
g.drawString(batStr, c.x, c.y+40, true);
}
};
function drawNumbers() {
const drawNumbers = function() {
//draws the numbers on the screen
g.setFont("Vector",20);
g.setColor(1,1,1);
@ -84,9 +82,37 @@ function drawNumbers() {
for(let i = 0;i<12;i++){
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
g.setColor(0,0,0);
g.fillRect(10,10,2*c.x-10,2*c.x-10);
@ -100,10 +126,11 @@ function draw(){
} else {
drawText(d); drawHands(d);
}
}
queueDraw();
};
//draws the scale once the app is startet
function drawScale(){
const drawScale = function() {
// clear the screen
g.setBgColor(0,0,0);
g.clear();
@ -117,36 +144,35 @@ function drawScale(){
g.fillRect(10,10,2*c.x-10,2*c.x-10);
g.setColor(1,1,1);
}
}
};
//// main running sequence ////
// 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
if (settings.loadWidgets) {
Bangle.loadWidgets();
require("widget_utils").swipeOn();
} 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
Bangle.on('lcdPower',on=>{
if (secondInterval) clearInterval(secondInterval);
secondInterval = undefined;
if (on) {
secondInterval = setInterval(draw, 1000);
draw(); // draw immediately
}
});
Bangle.on('lock',on=>{
unlock = !on;
if (secondInterval) clearInterval(secondInterval);
secondInterval = setInterval(draw, unlock ? 1000 : 60000);
draw(); // draw immediately
});
Bangle.on('charging',on=>{draw();});
Bangle.on('lcdPower', updateState);
Bangle.on('lock', updateState);
Bangle.on('charging', draw); // Immediately redraw when charger (dis)connected
updateState();
drawScale();
draw();
}

View File

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

View File

@ -49,7 +49,7 @@
<input type="radio" name="gnss_select" value="6"><i class="form-icon"></i> BDS+GLONASS
</label>
<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>
</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.02: Don't fire if the app uses swipes already.
0.03: Only count defined handlers in the handler array.
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.
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.
0.05: React on swipes before the active app (for the most part) by using `prependListener`.

View File

@ -38,6 +38,7 @@
// 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) {
global.BACK();
E.stopEventPropagation();
}
}
}
@ -56,5 +57,5 @@
}
// Listen to left to right swipe
Bangle.on("swipe", goBack);
Bangle.prependListener("swipe", goBack);
})();

View File

@ -1,7 +1,7 @@
{ "id": "backswipe",
"name": "Back Swipe",
"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",
"icon": "app.png",
"tags": "back,gesture,swipe",

View File

@ -8,3 +8,4 @@
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.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();
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
MODE24: true, //24h mode vs 12h mode
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 today = d.getDate();
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();
g.setFontAlign(1, 1);
for (var y = 1; y <= s.CAL_ROWS; y++) {
@ -187,7 +188,7 @@ function drawWatch() {
bottomrightY = y * CELL_H + CAL_Y;
g.setFont("Vector", 16);
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.fillRect(bottomrightX - CELL_W + 1, bottomrightY - CELL_H - 1, bottomrightX, bottomrightY - 2);
g.setColor('#000');

View File

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

View File

@ -1,7 +1,8 @@
(function (back) {
var FILE = "clockcal.json";
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
MODE24: true, //24h mode vs 12h mode
FIRSTDAY: 6, //First day of the week: mo, tu, we, th, fr, sa, su
@ -39,6 +40,14 @@
writeSettings();
}
},
'#Cal Rows Prior': {
value: settings.CAL_ROWS_PRIOR,
min: 0, max: 4,
onchange: v => {
settings.CAL_ROWS_PRIOR = v;
writeSettings();
}
},
'Clock mode': {
value: settings.MODE24,
format: v => v ? "24h" : "12h",

View File

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

View File

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

View File

@ -31,3 +31,4 @@ when moving pages. Add caching for faster startups.
0.24: Add buzz-on-interaction setting
0.25: Minor code improvements
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)
## Controls- Bangle
## Controls
### Bangle 1
**BTN1** - move backward through app icons on a page
@ -35,10 +37,28 @@ Bangle 2:
**Touch Middle(1+2) area** - run the selected app
## Controls- Bangle 2
### Bangle 2
**Touch** - icon to select, second touch launches app
**Swipe Left/Up** - move to next 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,
timeOut: "Off",
interactionBuzz: false,
rememberPage: false,
}, require('Storage').readJSON("dtlaunch.json", true) || {});
let s = require("Storage");
@ -33,7 +34,17 @@
s.writeJSON("launch.cache.json", launchCache);
}
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)
apps[i].icon = s.read(apps[i].icon); // should just be a link to a memory area
}
@ -43,7 +54,6 @@
let maxPage = Npages-1;
let selected = -1;
//let oldselected = -1;
let page = 0;
const XOFF = 24;
const YOFF = 30;
@ -99,13 +109,32 @@
Bangle.drawWidgets(); // To immediately update widget field to follow current theme - remove leftovers if previous app set custom theme.
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)
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){
updateTimeoutToClock();
selected = -1;
@ -142,6 +171,7 @@
drawIcon(page,selected,false);
} else {
buzzLong();
global.dtlaunch.handlePagePersist(page);
load(apps[page*4+i].src);
}
}
@ -162,7 +192,10 @@
back : Bangle.showClock,
swipe : swipeListenerDt,
touch : touchListenerDt,
remove : ()=>{if (timeoutToClock) clearTimeout(timeoutToClock);}
remove : ()=>{
if (timeoutToClock) {clearTimeout(timeoutToClock);}
global.dtlaunch.handlePagePersist(page);
}
});
// taken from Icon Launcher with minor alterations
@ -177,4 +210,3 @@
updateTimeoutToClock();
} // end of app scope

View File

@ -1,7 +1,7 @@
{
"id": "dtlaunch",
"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.",
"screenshots": [{"url":"shot1.png"},{"url":"shot2.png"},{"url":"shot3.png"}],
"icon": "icon.png",

View File

@ -8,6 +8,7 @@
swipeExit: false,
timeOut: "Off",
interactionBuzz: false,
rememberPage: false,
}, require('Storage').readJSON(FILE, true) || {});
function writeSettings() {
@ -64,5 +65,12 @@
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

@ -17,7 +17,8 @@
</ul>
<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
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">
<p>The currently available Espruino firmware releases are:</p>
<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>
</div>
<p><a href="#" id="info-btn">What is DFU? ▼</a></p>
<div id="info-div" style="display:none">
<p><b>What is DFU?</b></p>
@ -33,7 +35,7 @@
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
<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
with the Bootloader app in the app loader (which prepares Bangle.js for running apps).</p>
</div>
@ -51,7 +53,7 @@
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>
</div>
<p><button id="upload" class="btn btn-primary" style="display:none">Upload</button></p>
</div>
@ -107,6 +109,7 @@ function onInit(device) {
else if (crcs[0] == 3329616485) version = "2v22";
else if (crcs[0] == 1569433504) version = "2v23";
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
var crc = crcs[1];
if (crc==1339551013) { version = "2v10.219"; ok = false; }
@ -127,7 +130,7 @@ function onInit(device) {
}
document.getElementById("boot-version").innerHTML = version;
var versionNumber = parseFloat(version.replace(".","").replace("v","."));
if (versionNumber>=2.20)
if (versionNumber>=2.25)
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)
hexJS += 'f.erasePage(0x'+i.toString(16)+');\n';
hexJS += `f.write(_fw,${startAddress});\n`;
hexJS += 'E.showMessage("Update Complete.")\n';
hexJS += `})()\n`;
log("DFU ready for upload");
}

View File

@ -1,6 +1,6 @@
## 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.
@ -13,11 +13,11 @@ The large digits are the course and speed. The top of the display is a linear co
![](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
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)
@ -25,7 +25,7 @@ Bearing and distance are both zero as WP1 has currently no GPS location associat
![](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

View File

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

View File

@ -50,11 +50,15 @@ used. These settings will remain for all apps that use the GPS.
open country an update once every 60 seconds is adequate to put
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.
- search - the time between two acquisition attempts if the receiver
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
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.update = settings.update||120;
settings.search = settings.search||5;
settings.fix_req = settings.fix_req||1;
settings.power_mode = settings.power_mode||"SuperE";
log_debug(settings);
}
@ -85,6 +86,16 @@ function showMainMenu() {
settings.search = v;
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 BANGLE_VER = process.env.HWVERSION; //BangleJS2 support
function log_debug(o) {
//let timestamp = new Date().getTime();
//console.log(timestamp + " : " + o);
@ -106,6 +106,8 @@ function delay(ms) {
function setupSuperE() {
log_debug("setupGPS() Super-E");
switch(BANGLE_VER){
case(1): {
return Promise.resolve().then(function() {
UBX_CFG_RESET();
return delay(100);
@ -124,10 +126,19 @@ function setupSuperE() {
*/
return delay(20);
});
}
case(2):{
//nothing more to do.
return;
}
}
}
function setupPSMOO(settings) {
log_debug("setupGPS() PSMOO");
switch(BANGLE_VER){
case(1):{
return Promise.resolve().then(function() {
UBX_CFG_RESET();
return delay(100);
@ -149,6 +160,43 @@ function setupPSMOO(settings) {
*/
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.
@ -161,16 +209,21 @@ require("gpssetup").setPowerMode({power_mode:"SuperE"}) // <-- Super E mode
See the README for more information
*/
exports.setPowerMode = function(options) {
settings = require("Storage").readJSON(SETTINGS_FILE,1)||{};
var settings = require("Storage").readJSON(SETTINGS_FILE,1)||{};
if (options) {
if (options.update) settings.update = options.update;
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.appName) settings.appName = options.appName;
}
settings.update = settings.update||120;
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.appName = settings.appName || "gpssetup";
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") {
return setupPSMOO(settings);
} else {

View File

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

View File

@ -1,3 +1,7 @@
0.01: New app!
0.02: Add sensor icons
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

@ -22,3 +22,29 @@ You need to fill out these fields:
Currently creates these sensors:
* `<sensor id>_battery_level`: Your watch battery level as percentage
* `<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 () {
const sb = () => require("hasensors").sendBattery();
Bangle.on("charging", sb);
NRF.on("connect", () => setTimeout(sb, 2000));
setInterval(sb, 10 * 60 * 1000);
const su = () => require("hasensors").sendUpdate();
Bangle.on("charging", su);
NRF.on("connect", () => setTimeout(su, 2000));
su();
setInterval(su, 10 * 60 * 1000);
Bangle.on('HRM', h=>require("hasensors").sendHRM(h));
})();

View File

@ -12,8 +12,7 @@ function post(sensor, data) {
});
}
exports.sendBattery = function () {
if (!NRF.getSecurityStatus().connected) return;
function sendBattery() {
const b = E.getBattery(),
c = Bangle.isCharging();
let i = "mdi:battery";
@ -41,3 +40,74 @@ exports.sendBattery = function () {
}
});
}
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",
"name": "Home Assistant Sensors",
"shortName": "HA sensors",
"version": "0.02",
"version": "0.03",
"description": "Send sensor values to Home Assistant using Android Integration/Gadgetbridge",
"icon": "ha.png",
"type": "bootloader",

View File

@ -1,2 +1,3 @@
0.01: New App!
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) => {
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",
"name": "EdgeWrite keyboard",
"version":"0.02",
"version":"0.03",
"description": "A library for text input via EdgeWrite swipe gestures",
"icon": "app.png",
"type":"textinput",

View File

@ -111,3 +111,5 @@
Make sure play button image is transparent
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.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
msg.new = false;
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
}
@ -195,8 +195,8 @@ function showMusicMessage(msg) {
var wasNew = msg.new;
msg.new = false;
layout = undefined;
if (wasNew) checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:0,openMusic:0});
else checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
if (wasNew) checkMessages({clockIfNoMsg:1,clockIfAllRead:1,ignoreUnread:1,openMusic:0});
else returnToMain();
}
function updateLabels() {
trackName = reduceStringAndPad(msg.track, trackScrollOffset, 13);
@ -283,7 +283,7 @@ function showMessageSettings(msg) {
};
if (msg.id!="music")
msg[/*LANG*/"View Message"] = () => showMessageScroller(msg);
menu[/*LANG*/"View Message"] = () => showMessageScroller(msg);
if (msg.reply && reply) {
menu[/*LANG*/"Reply"] = () => {
@ -304,7 +304,7 @@ function showMessageSettings(msg) {
menu = Object.assign(menu, {
/*LANG*/"Delete" : () => {
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);
MESSAGES = MESSAGES.filter(m=>m.id!=msg.id);
}
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
returnToMain();
});
};
menu = Object.assign(menu, {
/*LANG*/"Mark Unread" : () => {
msg.new = true;
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
returnToMain();
},
/*LANG*/"Mark all read" : () => {
MESSAGES.forEach(msg => msg.new = false);
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
returnToMain();
},
/*LANG*/"Delete all messages" : () => {
E.showPrompt(/*LANG*/"Are you sure?", {title:/*LANG*/"Delete All Messages"}).then(isYes => {
if (isYes) {
MESSAGES = [];
}
checkMessages({clockIfNoMsg:0,clockIfAllRead:0,showMsgIfUnread:0,openMusic:0});
returnToMain();
});
},
});
@ -349,7 +349,7 @@ function showMessage(msgid, persist) {
clearInterval(updateLabelsInterval);
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") {
cancelReloadTimeout(); // don't auto-reload to clock now
return showMusicMessage(msg);
@ -399,7 +399,7 @@ function showMessage(msgid, persist) {
layout = undefined;
msg.new = false; // read mail
cancelReloadTimeout(); // don't auto-reload to clock now
checkMessages({clockIfNoMsg:1,clockIfAllRead:0,showMsgIfUnread:0,openMusic:openMusic});
returnToClockIfEmpty();
}
var negHandler,posHandler,footer = [ ];
if (msg.negative) {
@ -407,7 +407,7 @@ function showMessage(msgid, persist) {
msg.new = false;
cancelReloadTimeout(); // don't auto-reload to clock now
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({fillx:1}); // push images to left/right
@ -421,7 +421,7 @@ function showMessage(msgid, persist) {
Bluetooth.println(JSON.stringify(result));
replying = false;
layout.render();
checkMessages({clockIfNoMsg:1,clockIfAllRead:1,showMsgIfUnread:1,openMusic:openMusic});
returnToCheckMessages();
})
.catch(() => {
replying = false;
@ -435,7 +435,7 @@ function showMessage(msgid, persist) {
msg.new = false;
cancelReloadTimeout(); // don't auto-reload to clock now
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});
}
@ -447,7 +447,7 @@ function showMessage(msgid, persist) {
]},
{ type:"btn",
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:()=>{
cancelReloadTimeout(); // don't auto-reload to clock now
showMessageSettings(msg);
@ -476,7 +476,7 @@ function showMessage(msgid, persist) {
/* options = {
clockIfNoMsg : bool
clockIfAllRead : bool
showMsgIfUnread : bool
ignoreUnread : bool // don't automatically navigate to the first unread message
openMusic : bool // open music if it's playing
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
var newMessages = MESSAGES.filter(m=>m.new&&m.id!="music");
// 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
showMessage(newMessages[0].id, false);
// buzz after showMessage, so being busy during layout doesn't affect the buzz pattern
@ -538,7 +538,7 @@ function checkMessages(options) {
}
if (img) {
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
.setColor(fg); // only color the icon
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() {
if (!unreadTimeout) return;
@ -594,7 +605,7 @@ setTimeout(() => {
// only openMusic on launch if music is new, or state=="show" (set by messagesmusic)
var musicMsg = MESSAGES.find(m => m.id === "music");
checkMessages({
clockIfNoMsg: 0, clockIfAllRead: 0, showMsgIfUnread: 1,
clockIfNoMsg: 0, clockIfAllRead: 0, ignoreUnread: settings.ignoreUnread,
openMusic: ((musicMsg&&musicMsg.new) && settings.openMusic) || (musicMsg&&musicMsg.state=="show"),
dontStopBuzz: 1 });
}, 10); // if checkMessages wants to 'load', do that

View File

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

View File

@ -6,3 +6,6 @@
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.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",
"name": "Messages",
"version": "0.62",
"version": "0.65",
"description": "Library to handle, load and store message events received from Android/iOS",
"icon": "app.png",
"type": "module",

View File

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

View File

@ -2,6 +2,7 @@
<head>
<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-icons.min.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet-geosearch@3.6.0/dist/geosearch.css"/>
</head>
<style>
@ -34,7 +35,9 @@
</div>
<div id="controls">
<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>
<script src="../../core/lib/interface.js"></script>
@ -76,18 +79,36 @@
map.removeLayer(marker);
}
marker = new L.marker(latlon).addTo(map);
document.getElementById("select-hint").style.display="none";
document.getElementById("select").style.display="";
document.getElementById("locate-marker").style.display="";
}
map.on('click', function(e){
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() {
let settings = {}; // {"lat":48.8566,"lon":2.3522,"location":"Paris"}
settings.lat = latlon.lat;
settings.lon = latlon.lng;
let settings = convertMapToFile(latlon); // {"lat":48.8566,"lon":2.3522,"location":"Paris"}
settings.location = "custom";
Util.showModal("Saving...");
Util.writeStorage("mylocation.json", JSON.stringify(settings), ()=>{
@ -101,7 +122,7 @@
Util.readStorageJSON("mylocation.json", function(data) {
if (data===undefined) return; // no file
try {
setPosition(data);
setPosition(convertFileToMap(data));
} catch (e) {
console.error(e);
}

View File

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

View File

@ -16,7 +16,7 @@
</div>
<p><button id="try" class="btn">Try in Emulator</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>
@ -76,7 +76,7 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/
// if (style=="Christmas")
return `(function() {
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 titleFont = g.getFonts().includes("12x20") ? "12x20" : "6x8:2";
var flakes = [];
@ -112,6 +112,8 @@ var ig = require("heatshrink").decompress(atob("jk0ggGDg93AAVwCYwMEBxAMFAAIaHuc/
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...
document.getElementById("try").addEventListener("click", function() {

View File

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

View File

@ -1,3 +1,4 @@
0.01: New App!
0.02: Keep advertising when connected
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
sendCustomizedApp({
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",
"name": "OpenHaystack (AirTag)",
"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",
"tags": "openhaystack,bluetooth,ble,tracking,airtag",
"type": "bootloader",
@ -9,6 +9,6 @@
"readme": "README.md",
"supports": ["BANGLEJS","BANGLEJS2"],
"storage": [
{"name":"openhaystack.boot.js"}
{"name":"openhaystack.boot.js" }
]
}

View File

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

View File

@ -45,10 +45,12 @@
<div id="map">
</div>
<div id="controls">
<div style="display:inline-block;text-align:left;vertical-align: top;" id="3bitdiv">
<input type="checkbox" id="3bit"></input><span>3 bit</span>
<br>
<input type="checkbox" id="preview"><span>Preview</span>
<div class="form-group" style="display:inline-block;">
<select class="form-select" id="mapStyle">
<option value="3bit" selected>3 bit</option>
<option value="8bit">8 bit</option>
<option value="1bit">1 bit</option>
</select>
</div>
<div class="form-group" style="display:inline-block;">
<select class="form-select" id="mapSize">
@ -94,20 +96,47 @@ TODO:
var TILELAYER = '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 = [];
// Tiles used for Bangle.js itself
var bangleTileLayer = L.tileLayer(TILELAYER, {
maxZoom: 18,
attribution: 'Map data &copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap</a> contributors</a>'
});
var bangleTileLayer;
// Tiles used for the may the user sees (faster)
var previewTileLayer = L.tileLayer(PREVIEWTILELAYER, {
maxZoom: 18,
attribution: 'Map data &copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap</a> contributors</a>'
});
var previewTileLayer;
// Currently selected version of MAPSTYLES
var currentStyle;
// 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
var map = L.map('map').locate({setView: true, maxZoom: 16, enableHighAccuracy:true});
previewTileLayer.addTo(map);
@ -130,10 +159,11 @@ TODO:
if (device && device.info && device.info.g) {
// On 3 bit devices, 3 bit is the best way
// 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("3bitdiv").style = "display:none";
}
}*/
}
showLoadedMaps();
@ -257,17 +287,7 @@ TODO:
// convert canvas into an actual tiled image file
function tilesLoaded(ctx, width, height, mapImageFile) {
var options = {
compression:false, output:"raw",
mode:"web"
};
if (document.getElementById("3bit").checked) {
options = {
compression:false, output:"raw",
mode:"3bit",
diffusion:"bayer2"
};
}
var options = currentStyle.img; // compression options
/* Go through all the data beforehand and
turn the saturation up to maximum, so if thresholded to 3 bits it
works a lot better */
@ -289,9 +309,9 @@ TODO:
options.width = TILESIZE;
options.height = TILESIZE;
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
}
}*/
/*var compress = 'require("heatshrink").decompress('
if (!imgstr.startsWith(compress)) throw "Data in wrong format";
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() {
Util.showModal("Uploading...");
let promise = Promise.resolve();

View File

@ -2,7 +2,7 @@
"id": "openstmap",
"name": "OpenStreetMap",
"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",
"readme": "README.md",
"icon": "app.png",

View File

@ -73,6 +73,7 @@ exports.draw = function() {
}
var mx = g.getWidth();
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 y=oy,tty=ty;y<my && tty<map.h;y+=s,tty++) {
o.frame = ttx+(tty*map.w);

View File

@ -1,3 +1,4 @@
0.01: attempt to import
0.02: 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)
The plan is to have an (analog) astronomical clock with a lot of
information on single dial.
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.
information on single dial. Thanks a lot to "Dark Analog Clock".

View File

@ -1,405 +1,277 @@
const SunCalc = require("suncalc"); // from modules folder
// ################################################################################
let ScreenWidth = g.getWidth(), CenterX = ScreenWidth/2;
let ScreenHeight = g.getHeight(), CenterY = ScreenHeight/2;
let outerRadius = Math.min(CenterX,CenterY) * 0.9;
const lat = 50.1;
const lon = 14.45;
const h = g.getHeight();
const w = g.getWidth();
const sm = 15;
var altitude, temperature;
var img_north = Graphics.createImage(`
X
XXX
XXX
X XXX
X XXX
X XXXX
X XXXX
X XXXXX
X XXXXX
XXXXXXXXX
`);
var img_sunrise = Graphics.createImage(`
XXX
XXXXX
XXXXXXXXX
`);
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 radD(d) { return d*(h/2); }
function radX(p, d) {
let a = radA(p);
return h/2 + Math.sin(a)*radD(d);
}
function radY(p, d) {
let a = radA(p);
return w/2 - Math.cos(a)*radD(d);
}
function fracHour(d) {
let hour = d.getHours();
let min = d.getMinutes();
hour = hour + min/60;
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);
/* 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);
}
/**** transforme polygon ****/
let transformedPolygon = new Array(HourHandPolygon.length);
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;
}
}
/**** draw clock hands ****/
function drawClockHands () {
let now = new Date();
let Hours = now.getHours() % 12;
let Minutes = now.getMinutes();
let Seconds = now.getSeconds();
let HoursAngle = (Hours+(Minutes/60))/12 * twoPi - Pi;
let MinutesAngle = (Minutes/60) * twoPi - Pi;
let SecondsAngle = (Seconds/60) * twoPi - Pi;
g.setColor(g.theme.fg);
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();
print("Have suncalc: ", this.SunCalc);
},
sunPos: function() {
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);
if (!this.SunCalc) {
let sun = {};
sun.azimuth = 175;
sun.altitude = 15;
return sun;
}
{
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 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 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 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;
}
{
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 } );
return set;
// set = set / 60;
// return s + (set / 60).toFixed(0) + ":" + (set % 60).toFixed(0);
},
};
sun.init();
const defaultSettings = {
loadWidgets : false,
textAboveHands : false,
shortHrHand : true
};
const white = 0;
const settings = Object.assign(defaultSettings, require('Storage').readJSON('andark.json',1)||{});
const c={"x":g.getWidth()/2,"y":g.getHeight()/2};
const zahlpos=(function() {
let z=[];
let sk=1;
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;
})();
let unlock = false;
function zeiger(len,dia,tim){
const x=c.x+ Math.cos(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)},
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;
}
function drawHands(d) {
let m=d.getMinutes(), h=d.getHours(), s=d.getSeconds();
g.setColor(white,white,white);
if(h>12){
h=h-12;
}
//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 setColor() {
g.setBgColor(!white,!white,!white);
g.setColor(white,white,white);
}
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 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 } );
let batStr = Math.round(bat/5)*5+"%";
if (Bangle.isCharging()) {
g.setBgColor(1,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 });
if (bat < 30)
g.drawString(batStr, c.x, c.y+40, true);
}
function drawNumbers(d) {
let hour = d.getHours();
if (d.getMinutes() > 30) {
hour += 1;
}
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 });
let day = d.getDate();
if (day > 12) {
day = day % 10;
if (!day)
day = 10;
}
//draws the numbers on the screen
for(let i = 0;i<12;i++){
let on = false;
let j = i+1;
g.setFont("Vector",20);
if (j == day) {
on = true;
g.setFont("Vector",29);
}
{
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);
if ((j % 12) == (hour % 12))
on = true;
setColor();
if (!on)
g.setColor(white/2, !white, white);
if (1 || on)
g.drawString(zahlpos[i][0],zahlpos[i][1],zahlpos[i][2],true);
}
}
function drawEmpty() {
g.reset();
g.setColor(g.theme.bg);
g.fillRect(Bangle.appRect);
function draw(){
// draw black rectangle in the middle to clear screen from scale and hands
g.setColor(!white,!white,!white);
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) {
var x = xy.x;
var y = xy.y;
if (y > h) y = h;
if (y < 0) y = 0;
if (x > w) x = w;
if (x < 0) x = 0;
});
// 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));
/* 0..12 -> angle suitable for drawScale */
function conv(m) { return -15 + (m / 12) * 60; }
/* datetime -> 0..12 float */
function hour12(d) {
let h = d.getHours() + d.getMinutes() / 60;
if (h > 12)
h = h - 12;
return h;
}
//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
Bangle.on('lcdPower',on=>{
if (secondInterval) clearInterval(secondInterval);
secondInterval = undefined;
if (on) {
draw(); // draw immediately, queue redraw
} else { // stop draw timer
if (drawTimeout) clearTimeout(drawTimeout);
drawTimeout = undefined;
secondInterval = setInterval(draw, 1000);
draw(); // draw immediately
}
});
Bangle.setUI("clockupdown", btn=> {
if (btn<0) use_compass = 0;
if (btn>0) use_compass = 1;
Bangle.setCompassPower(use_compass, 'orloj');
draw();
Bangle.on('lock',on=>{
unlock = !on;
if (secondInterval) clearInterval(secondInterval);
secondInterval = setInterval(draw, unlock ? 1000 : 60000);
draw(); // draw immediately
});
if (use_compass)
Bangle.setCompassPower(true, 'orloj');
g.clear();
draw();
Bangle.on('charging',on=>{draw();});

View File

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

View File

@ -4,3 +4,6 @@
0.04: Minor code improvements
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.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(
require('Storage').readJSON("owmweather.default.json", true) || {},
require('Storage').readJSON("owmweather.json", true) || {}
);
let completion = function(){
waiting = false;
let refreshMillis = function () {
return settings.refresh * 1000 * 60 + 1; // +1 <- leave some slack
};
let onCompleted = function () {
loading = false;
settings.updated = Date.now();
require('Storage').writeJSON("owmweather.json", settings);
if (timeoutRef) clearTimeout(timeoutRef);
timeoutRef = setTimeout(loadIfDueAndReschedule, refreshMillis());
};
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 lastWeatherUpdate = weather && weather.weather && weather.weather.time && weather.weather.time || 0;
if (lastWeatherUpdate > settings.updated) {
settings.updated = lastWeatherUpdate;
}
let MillisUntilDue = settings.updated + refreshMillis() - Date.now();
if (!MillisUntilDue || MillisUntilDue <= 0) {
if (!loading) {
loading = true;
require("owmweather").pull(onCompleted);
}
} 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) {
let weather = require("Storage").readJSON('weather.json') || {};
if (weather && weather.weather && weather.weather.time) lastUpdate = weather.weather.time;
if (!settings.updated || settings.updated + settings.refresh * 1000 * 60 < Date.now()){
setTimeout(() => {
if (!waiting){
waiting = true;
require("owmweather").pull(completion);
}
}, 5000);
}
setInterval(() => {
if (!waiting && NRF.getSecurityStatus().connected){
waiting = true;
require("owmweather").pull(completion);
}
}, settings.refresh * 1000 * 60);
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) {
let owmData = JSON.parse(response);
let isOwmData = false;
try {
isOwmData = (owmData.lat && owmData.lon) && owmData.current.weather && owmData.current;
} catch (_e) {}
let isOwmData = owmData.coord && owmData.weather && owmData.main;
if (isOwmData) {
let json = require("Storage").readJSON('weather.json') || {};
let weather = {};
weather.time = Date.now();
weather.hum = owmData.current.humidity;
weather.temp = owmData.current.temp;
weather.code = owmData.current.weather[0].id;
weather.wdir = owmData.current.wind_deg;
weather.wind = owmData.current.wind_speed;
weather.loc = owmData.name || "";
weather.txt = owmData.current.weather[0].main;
weather.hpa = owmData.current.pressure || 0;
weather.hum = owmData.main.humidity;
weather.temp = owmData.main.temp;
weather.code = owmData.weather[0].id;
weather.wdir = owmData.wind.deg;
weather.wind = owmData.wind.speed;
weather.loc = owmData.name;
weather.txt = owmData.weather[0].main;
weather.hpa = owmData.main.pressure || 0;
if (weather.wdir != null) {
let deg = weather.wdir;
@ -43,7 +40,7 @@ exports.pull = function(completionCallback) {
"location": "London"
};
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){
Bangle.http(uri, {timeout:10000}).then(event => {
let result = parseWeather(event.resp);

View File

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

View File

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

View File

@ -85,26 +85,30 @@
var h = g.getHeight();
var max = splits_1.reduce(function (a, s) { return Math.max(a, s.time); }, 0);
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;
for (;; i++) {
var split = splits_1[i + splitOffset_1];
if (split == null)
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)
break;
var size = w * split.time / max;
g.setColor("#00f").fillRect(0, y_1, size, y_1 + barSize);
var splitPace = calculatePace_1(split);
g.setColor(g.theme.fg);
drawSplit_1(i, y_1, splitPace);
}
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);
};
var drawSplit_1 = function (i, y, pace) {
g
.setColor(g.theme.fg)
return g
.drawString("".concat(i + 1 + splitOffset_1, " ").concat(typeof pace === "number" ? pace.toFixed(2) : pace), 0, y);
};
var pauseRun_1 = function () {

View File

@ -114,36 +114,40 @@ const drawSplits = () => {
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;
for(; ; i++) {
const split = splits[i + splitOffset];
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;
const size = w * split.time / max; // Scale bar height based on pace
g.setColor("#00f").fillRect(0, y, size, y + barSize);
const splitPace = calculatePace(split); // Pace per km
g.setColor(g.theme.fg)
drawSplit(i, y, splitPace);
}
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);
};
const drawSplit = (i: number, y: number, pace: number | string) => {
const drawSplit = (i: number, y: number, pace: number | string) =>
g
.setColor(g.theme.fg)
.drawString(
`${i + 1 + splitOffset} ${typeof pace === "number" ? pace.toFixed(2) : pace}`,
0,
y
);
};
const pauseRun = () => {
exs.stop();

View File

@ -1,7 +1,7 @@
{
"id": "pace",
"name": "Pace",
"version": "0.01",
"version": "0.02",
"description": "Show pace and time running splits",
"icon": "app.png",
"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
when fastloading.
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
issue newline before GB commands.
0.09: Don't send the gadgetbridge wake command twice. Once should do since we issue newline before GB commands.
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 standardCls = pkg + ".receiver.PodcastAddictPlayerReceiver";
let updateCls = pkg + ".receiver.PodcastAddictBroadcastReceiver";
let speed = 1.0;
//let speed = 1.0;
let simpleSearch = "";
@ -269,15 +269,15 @@ let speedMenu = {
}
},
"Regular Speed": () => {
speed = 1.0;
//speed = 1.0;
btMsg("service", standardCls, "player.1xspeed");
},
"1.5x Regular Speed": () => {
speed = 1.5;
//speed = 1.5;
btMsg("service", standardCls, "player.1.5xspeed");
},
"2x Regular Speed": () => {
speed = 2.0;
//speed = 2.0;
btMsg("service", standardCls, "player.2xspeed");
},
//"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",
"name": "Podcast Addict Remote",
"shortName": "PA Remote",
"version": "0.10",
"version": "0.11",
"description": "Control Podcast Addict on your android device.",
"readme": "README.md",
"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.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.10: Trigger `remove` callbacks when ending the menu

View File

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

View File

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

View File

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

View File

@ -7,12 +7,10 @@
0.07: Revert version 0.06. This version is the same as 0.05.
0.08: Respect appRect on touch events
0.09: Do not react if clkinfo is focused
0.10: Extend the functionality via a quicklaunch.app.js file that can be launched
with quicklaunch itself.
0.10: Extend the functionality via a quicklaunch.app.js file that can be launched with quicklaunch itself.
0.11: Add hints to the extension app. Tweak remove function.
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.
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.
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.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");
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){
if (!settings[name]) settings[name] = {"name":"(none)"};
if (!storage.read(settings[name].src)) settings[name] = {"name":"(none)"};
storage.write("quicklaunch.json", settings);
const draw = () => {
// Draw app hints
g.reset().clearRect(R).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);
};
draw(); // draw asap to increase perceived snappiness.
let leaveTrace = function(trace) {
if (settings[trace+"app"].name != "") {
settings.trace = trace;
storage.writeJSON("quicklaunch.json", settings);
} else { trace = trace.substring(0, trace.length-1); }
return trace;
};
let launchApp = function(trace) {
if (settings[trace+"app"]) {
if (settings[trace+"app"].src){
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"] && settings[trace+"app"].src) {
if (settings[trace+"app"].name == "Extension") draw();
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) => {
if (e.type == 2) return;
let R = Bangle.appRect;
@ -47,15 +56,22 @@
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({
mode: "custom",
touch: touchHandler,
swipe : swipeHandler,
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.
// taken from Icon Launcher with some alterations
@ -67,13 +83,4 @@
};
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");
let settings = storage.readJSON("quicklaunch.json", true) || {};
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 settings;
let leaveTrace = function(trace) {
if (!settings) settings = storage.readJSON("quicklaunch.json", true) || {};
settings.trace = trace;
storage.writeJSON("quicklaunch.json", settings);
return trace;
};
let launchApp = function(trace) {
if (settings[trace+"app"].src){
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) settings = storage.readJSON("quicklaunch.json", true) || {};
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;

View File

@ -2,7 +2,7 @@
"id": "quicklaunch",
"name": "Quick Launch",
"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.",
"type": "bootloader",
"tags": "tools, system",

View File

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

View File

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

View File

@ -1,18 +1,6 @@
function roundRect (x1, y1, x2, y2, halfrad) {
const fullrad = halfrad + 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);
g.fillRect({x:x1, y:y1, x2:x2, y2:y2, r: halfrad});
}
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.
![](screenshot1.png)
View when watch is locked.
View when watch is locked. From outside: Hours, minutes, battery.
![](screenshot5.png)
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